diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 8e2069c..240a8d7 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -30,6 +30,8 @@ "default_app", "column_break_jevx", "navbar_text_color", + "navbar_toggler_border_color", + "navbar_breadcrumb_disabled_color", "hide_search", "buttons_tab", "primary_button_section", @@ -86,7 +88,10 @@ "sticky_footer", "column_break_footer", "footer_powered_by", - "footer_text_color" + "footer_text_color", + "desk_behavior_tab", + "fixed_sidebar", + "redirect_to_sidebar_on_login" ], "fields": [ { @@ -103,6 +108,16 @@ "fieldtype": "Color", "label": "Text Color" }, + { + "fieldname": "navbar_toggler_border_color", + "fieldtype": "Color", + "label": "Toggler Border Color" + }, + { + "fieldname": "navbar_breadcrumb_disabled_color", + "fieldtype": "Color", + "label": "Breadcrumb Disabled Color" + }, { "fieldname": "column_break_jevx", "fieldtype": "Column Break" @@ -515,6 +530,25 @@ "fieldname": "sidebar_hover_text_color", "fieldtype": "Color", "label": "Hover Text Color" + }, + { + "fieldname": "desk_behavior_tab", + "fieldtype": "Tab Break", + "label": "Desk Behavior" + }, + { + "description": "Fixed workspace sidebar to show for all modules", + "fieldname": "fixed_sidebar", + "fieldtype": "Link", + "label": "Fixed Workspace Sidebar", + "options": "Workspace Sidebar" + }, + { + "default": "0", + "description": "If enabled, after login redirect to the first item from the fixed sidebar instead of the default desktop screen", + "fieldname": "redirect_to_sidebar_on_login", + "fieldtype": "Check", + "label": "Redirect to Sidebar After Login" } ], "grid_page_length": 50, diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 020ba77..869e32a 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -154,10 +154,10 @@ label.control-label { } /* ======================================== - NAVIGATION BAR STYLING + NAVIGATION BAR STYLING (Desk Theme > Navbar) ======================================== */ -/* Main navigation bar - top-level container with custom background */ +/* Main navigation bar - Background Color, Text Color, Toggler Border, Breadcrumb Disabled */ .navbar { background-color: var(--navbar-bg, #fff); } @@ -177,24 +177,46 @@ label.control-label { stroke-width: 0; } -/* Mobile menu toggle button - border color for hamburger menu */ +/* Mobile menu toggle button - border color for hamburger menu (theme: Navbar > Toggler Border Color) */ button.navbar-toggler { - border-color: var(--navbar-color) !important; + border-color: var(--navbar-toggler-border, var(--navbar-color, #dee2e6)) !important; } -/* Breadcrumb navigation links - maintains consistent color scheme */ -#navbar-breadcrumbs li a { - color: var(--navbar-color); +/* Page head bar - navbar background only (override any body-bg inheritance or other rules) */ +.page-head, +.page-head.flex { + background-color: var(--navbar-bg, #fff) !important; } -/* Breadcrumb separators - adds arrow separators between breadcrumb items */ -#navbar-breadcrumbs li a::before { - content: '›'; +/* Page head breadcrumbs - override Frappe's ink-gray vars; use Desk Theme Navbar colors */ +.page-head .navbar-breadcrumbs li a { + color: var(--navbar-color, #555) !important; } -/* Disabled breadcrumb items - styling for non-clickable breadcrumb elements */ -#navbar-breadcrumbs li.disabled a { - color: var(--navbar-color) !important; +.page-head .navbar-breadcrumbs li a svg, +.page-head .navbar-breadcrumbs li a svg use { + fill: var(--navbar-color, #555) !important; + stroke: var(--navbar-color, #555) !important; +} + +/* Breadcrumb separators - match navbar color (Frappe uses "/", we keep theme color) */ +.page-head .navbar-breadcrumbs li a::before { + color: var(--navbar-color, #555) !important; +} + +/* Last/current breadcrumb item - Desk Theme Breadcrumb Disabled Color (overrides Frappe's li:last-child) */ +.page-head .navbar-breadcrumbs li.disabled a, +.page-head .navbar-breadcrumbs li:last-child a { + color: var(--breadcrumb-disabled-color, var(--navbar-color, #6c757d)) !important; +} + +/* Sidebar toggle in page head - match navbar color */ +.page-head .sidebar-toggle-btn.navbar-brand, +.page-head .sidebar-toggle-btn.navbar-brand svg, +.page-head .sidebar-toggle-btn.navbar-brand svg use { + color: var(--navbar-color, #555) !important; + fill: var(--navbar-color, #555) !important; + stroke: var(--navbar-color, #555) !important; } /* Navigation link buttons - text and icon color consistency */ @@ -210,10 +232,10 @@ button.navbar-toggler { } /* ======================================== - BUTTON STYLING + BUTTON STYLING (Desk Theme > Buttons) ======================================== */ -/* Primary buttons - main action buttons throughout the application */ +/* Primary buttons - Background/Text/Hover from theme */ .btn-primary, .btn-primary:active { background-color: var(--btn-primary-bg,#171717) !important; @@ -225,14 +247,14 @@ button.navbar-toggler { color: var(--btn-primary-color) !important; } -/* Primary button hover state - provides visual feedback on mouse over */ +/* Primary button hover state - fallback to primary when not set in theme */ .btn-primary:hover { - background-color: var(--btn-primary-hover-bg); + background-color: var(--btn-primary-hover-bg, var(--btn-primary-bg, #171717)); } -/* Primary button hover text - maintains readability during hover state */ +/* Primary button hover text - fallback to primary color when not set in theme */ .btn-primary:hover span { - color: var(--btn-primary-hover-color); + color: var(--btn-primary-hover-color, var(--btn-primary-color, #fff)); } /* Secondary/default buttons - alternative action buttons with distinct styling */ @@ -243,11 +265,11 @@ button.navbar-toggler { color: var(--btn-secondary-color); } -/* Secondary button hover effects - consistent interaction feedback */ +/* Secondary button hover effects - fallback to secondary when not set in theme */ .btn.btn-default.ellipsis:hover, .btn-default:hover { - background-color: var(--btn-secondary-hover-bg,#f3f3f3); - color: var(--btn-secondary-hover-color); + background-color: var(--btn-secondary-hover-bg, var(--btn-secondary-bg, #f3f3f3)); + color: var(--btn-secondary-hover-color, var(--btn-secondary-color, inherit)); } /* ======================================== @@ -260,8 +282,7 @@ body { } /* Page containers - main content wrapper background */ -.content.page-container, -.page-head { +.content.page-container { background-color: var(--body-bg,#fff); } diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index 3672609..192aa95 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -16,6 +16,8 @@ class FrappeDeskTheme { this.footerHtmlCache = null; this.footerCacheKey = null; // Track what theme data the footer was cached for this.stickyFooterListenerSetup = false; + // Internal flag to ensure we only perform one redirect from sidebar after login + this.didInitialSidebarLoginRedirect = false; this.init(); } @@ -525,6 +527,15 @@ class FrappeDeskTheme { if (theme.navbar_text_color) { root.style.setProperty("--navbar-color", theme.navbar_text_color); } + if (theme.navbar_toggler_border_color) { + root.style.setProperty("--navbar-toggler-border", theme.navbar_toggler_border_color); + } + if (theme.navbar_breadcrumb_disabled_color) { + root.style.setProperty( + "--breadcrumb-disabled-color", + theme.navbar_breadcrumb_disabled_color + ); + } if (theme.hide_help_button !== undefined) { root.style.setProperty("--hide-help", theme.hide_help_button ? "none" : "block"); } @@ -539,7 +550,7 @@ class FrappeDeskTheme { ); } - // Primary button styling + // Primary button styling (hover fallbacks to normal when not set) if (theme.button_background_color) { root.style.setProperty("--btn-primary-bg", theme.button_background_color); } @@ -548,9 +559,13 @@ class FrappeDeskTheme { } if (theme.button_hover_background_color) { root.style.setProperty("--btn-primary-hover-bg", theme.button_hover_background_color); + } else if (theme.button_background_color) { + root.style.setProperty("--btn-primary-hover-bg", theme.button_background_color); } if (theme.button_hover_text_color) { root.style.setProperty("--btn-primary-hover-color", theme.button_hover_text_color); + } else if (theme.button_text_color) { + root.style.setProperty("--btn-primary-hover-color", theme.button_text_color); } // Secondary button styling @@ -565,12 +580,22 @@ class FrappeDeskTheme { "--btn-secondary-hover-bg", theme.secondary_button_hover_background_color ); + } else if (theme.secondary_button_background_color) { + root.style.setProperty( + "--btn-secondary-hover-bg", + theme.secondary_button_background_color + ); } if (theme.secondary_button_hover_text_color) { root.style.setProperty( "--btn-secondary-hover-color", theme.secondary_button_hover_text_color ); + } else if (theme.secondary_button_text_color) { + root.style.setProperty( + "--btn-secondary-hover-color", + theme.secondary_button_text_color + ); } // Main body and content area styling @@ -656,6 +681,8 @@ class FrappeDeskTheme { this.toggleSidebar(); this.toggleSearchBar(); this.setDefaultApp(); + this.applyFixedSidebarBehavior(); + this.performInitialSidebarLoginRedirect(); if ( this.themeData.carousel && this.themeData.carousel.images && @@ -736,6 +763,161 @@ class FrappeDeskTheme { } } + /** + * Force Desk to always use a single, fixed workspace sidebar + * Uses the 'fixed_sidebar' link field from the Desk Theme doctype + */ + applyFixedSidebarBehavior() { + // Only applicable inside Desk (not on login page) + if ( + document.body.classList.contains("login-page") || + document.querySelector("#page-login") + ) { + return; + } + + if (!this.themeData || !this.themeData.fixed_sidebar) { + return; + } + + if (typeof frappe === "undefined" || !frappe.app || !frappe.app.sidebar) { + return; + } + + const sidebar = frappe.app.sidebar; + + // Only patch once + if (sidebar.__fixed_sidebar_patched) { + return; + } + sidebar.__fixed_sidebar_patched = true; + + const fixedSidebarLabel = this.themeData.fixed_sidebar; + + // Override sidebar switching methods to always use the configured sidebar + sidebar.set_workspace_sidebar = function () { + this.setup(fixedSidebarLabel); + this.set_active_workspace_item(); + }; + + sidebar.show_sidebar_for_module = function () { + // No-op: keep using the fixed sidebar + return; + }; + + sidebar.set_sidebar_for_page = function () { + this.setup(fixedSidebarLabel); + }; + + // Apply immediately for current page if possible + try { + sidebar.setup(fixedSidebarLabel); + sidebar.set_active_workspace_item(); + } catch (e) { + // Silent fail – sidebar might not be fully initialised yet + } + } + + /** + * On first Desk load after login, redirect user directly to a page + * derived from the fixed sidebar (first link item), instead of the + * default desktop / workspace landing. + */ + performInitialSidebarLoginRedirect() { + // Only run once per browser tab (use sessionStorage so it persists across reloads) + const redirectFlagKey = "frappe_desk_theme_sidebar_redirect_done"; + try { + if (sessionStorage.getItem(redirectFlagKey) === "1") { + return; + } + } catch (e) { + // If sessionStorage is unavailable, fall back to in-memory flag + if (this.didInitialSidebarLoginRedirect) { + return; + } + } + + // Must be enabled in theme and have a fixed sidebar configured + if ( + !this.themeData || + !this.themeData.redirect_to_sidebar_on_login || + !this.themeData.fixed_sidebar + ) { + return; + } + + // Not applicable on the login page + if ( + document.body.classList.contains("login-page") || + document.querySelector("#page-login") + ) { + return; + } + + // Only act inside Desk + if (!window.location.pathname.startsWith("/desk")) { + return; + } + + if (typeof frappe === "undefined" || !frappe.get_route || !frappe.boot) { + return; + } + + const route = frappe.get_route() || []; + + // Heuristic: only redirect from generic initial desk routes + // We *don't* treat specific workspace routes as initial, so reloads + // on "Workspaces / " won't be redirected. + const isInitialDeskRoute = route.length === 0 || route[0] === "desktop"; + + if (!isInitialDeskRoute) { + return; + } + + const fixedSidebarLabel = this.themeData.fixed_sidebar; + const sidebarBoot = frappe.boot.workspace_sidebar_item || {}; + const sidebarKey = (fixedSidebarLabel || "").toLowerCase(); + const sidebarData = sidebarBoot[sidebarKey]; + + if (!sidebarData || !Array.isArray(sidebarData.items) || !sidebarData.items.length) { + return; + } + + // Choose first link-type item from the configured sidebar + const firstLink = sidebarData.items.find((item) => item.type === "Link" && item.link_to); + if (!firstLink) { + return; + } + + this.didInitialSidebarLoginRedirect = true; + try { + sessionStorage.setItem(redirectFlagKey, "1"); + } catch (e) { + // Ignore storage errors + } + + try { + const linkType = (firstLink.link_type || "").toLowerCase(); + + if (linkType === "workspace") { + // Open workspace from sidebar link + frappe.set_route("Workspaces", firstLink.link_to); + } else if (linkType === "doctype") { + // Go to list view for the DocType + frappe.set_route("List", firstLink.link_to); + } else if (linkType === "page") { + // Use desk/page-name instead of desk/Page/page-name + frappe.set_route(firstLink.link_to); + } else if (linkType === "report") { + frappe.set_route("query-report", firstLink.link_to); + } else if (linkType === "url") { + window.location.href = firstLink.link_to; + } + } catch (e) { + // Silent fail – don't break desk if redirect fails + } + } + /** * Create and display footer in desk view using HTML template * Much more efficient than creating DOM elements dynamically @@ -931,6 +1113,8 @@ class FrappeDeskTheme { let footerTimeout; const observer = new MutationObserver(() => { this.toggleSearchBar(); + this.applyFixedSidebarBehavior(); + this.performInitialSidebarLoginRedirect(); // Debounce footer creation to avoid performance issues clearTimeout(footerTimeout); @@ -1071,8 +1255,43 @@ if (document.readyState === "loading") { // DOM is still loading, wait for DOMContentLoaded event document.addEventListener("DOMContentLoaded", () => { window.frappeDeskTheme = new FrappeDeskTheme(); + // Hook into Frappe's reload / clear cache button to also refresh theme + attachFrappeReloadThemeHook(); }); } else { // DOM is already loaded, initialize immediately window.frappeDeskTheme = new FrappeDeskTheme(); + attachFrappeReloadThemeHook(); +} + +/** + * Attach a hook so that when the user clicks Frappe's + * "Reload / Clear Cache" button, the desk theme cache + * is cleared and refreshed as well. + */ +function attachFrappeReloadThemeHook() { + if (typeof frappe === "undefined") return; + if (!frappe.ui || !frappe.ui.toolbar || !frappe.ui.toolbar.clear_cache) return; + + // Avoid double-wrapping + if (frappe.ui.toolbar.__theme_reload_hooked) { + return; + } + frappe.ui.toolbar.__theme_reload_hooked = true; + + const originalClearCache = frappe.ui.toolbar.clear_cache; + + frappe.ui.toolbar.clear_cache = function () { + try { + if (window.frappeDeskTheme) { + // Clear local theme cache and force a fresh fetch + window.frappeDeskTheme.clearCache(); + window.frappeDeskTheme.refreshTheme(); + } + } catch (e) { + // Ignore theme errors; still allow core clear_cache to run + } + + return originalClearCache.apply(this, arguments); + }; } diff --git a/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js index 8546ca2..ddd99a4 100644 --- a/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js +++ b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js @@ -10,8 +10,9 @@ frappe.ui.Sidebar = class CustomSidebar extends frappe.ui.Sidebar { const current_route = frappe.get_route(); if (!current_route || !current_route.length) return; - const current_item = current_route[1]; - if (!current_item) return; + // For workspaces: route is ["Workspaces", workspace_name]. For doctype list: ["List", "DocType", ...]. + // For Page doctype: route is ["page-name"] (desk/page-name) or ["Page", "page-name"] (desk/Page/page-name). + const current_item = current_route[1] || current_route[0]; const $match = this.$sidebar.find(`.sidebar-item-container[item-name="${current_item}"]`); if ($match.length) { @@ -37,6 +38,15 @@ frappe.ui.Sidebar = class CustomSidebar extends frappe.ui.Sidebar { this.prepare_sidebar(root_pages, sidebar_section, this.wrapper.find(".sidebar-items")); + // Rewrite Page links from desk/Page/page-name or desk/page/page-name to desk/page-name + sidebar_section.find(".item-anchor[href]").each(function () { + const href = $(this).attr("href") || ""; + const match = href.match(/^\/desk\/(?:Page|page)\/([^/?#]+)/); + if (match) { + $(this).attr("href", "/desk/" + match[1]); + } + }); + if (Object.keys(root_pages).length === 0) { sidebar_section.addClass("hidden"); } diff --git a/pyproject.toml b/pyproject.toml index 79f8dc5..c60b79f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,9 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -# These dependencies are only installed when developer mode is enabled -[tool.bench.dev-dependencies] -# package_name = "~=1.1.0" +[tool.bench.frappe-dependencies] +frappe = ">=16.0.0,<17.0.0" + [tool.ruff] line-length = 110