fix(sidebar): render fallback sidebar when no workspace matches route

Frappe v16's Sidebar.set_workspace_sidebar() leaves .sidebar-items empty
when a List/Form route's doctype is referenced by 2+ workspace sidebars
but none is the doctype's module home workspace — it falls through without
calling setup(). Symptom: sidebar goes blank on hard reload (e.g. after
clear-cache/migrate/build) and "self-heals" only via in-app navigation.

Repro: hard-reload /app/price-list (module Stock, linked only from the
Selling and Buying sidebars).

Patch the prototype so that if the items container is still empty after the
core method runs, it falls back to the first sidebar referencing the route's
doctype, else the route's module home workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-11 10:55:57 +00:00
parent 1c9c67f5d9
commit c65eeb741a
1 changed files with 76 additions and 0 deletions

View File

@ -2499,3 +2499,79 @@
init();
}
})();
/* ==========================================================================
Sidebar empty-state fix (works around a Frappe v16 core bug)
frappe.ui.Sidebar.set_workspace_sidebar() decides which workspace sidebar to
render for a List/Form route. When the route's doctype is referenced by MORE
THAN ONE workspace sidebar but none of them is the doctype's own module
workspace, the core method falls through without calling setup() leaving
.sidebar-items empty (no items, not even the "No Sidebar Items" placeholder,
because create_sidebar() never runs). It appears to "self-heal" only because
navigating in from a matching workspace sets this.sidebar_title, which an
early return in the method then preserves; a hard reload (e.g. after
clear-cache / migrate / build) loses that state and the sidebar goes blank.
Repro: hard-reload on /app/price-list. Price List's module is "Stock", but it
is only linked from the "Selling" and "Buying" sidebars, so
get_workspace_for_module("Stock") yields "Stock" which is not in
["Selling","Buying"] so setup() is never called.
Fix: after the core method runs, if the items container is still empty, fall
back to the first sidebar that references the route's doctype, otherwise to
the route's module home workspace. The fallback calls setup() directly (never
set_workspace_sidebar), so it can't recurse; an empty workspace still renders
the "No Sidebar Items" placeholder (one child), so isEmpty stays false.
========================================================================== */
(function () {
function patch() {
if (
!window.frappe ||
!frappe.ui ||
!frappe.ui.Sidebar ||
!frappe.ui.Sidebar.prototype ||
frappe.ui.Sidebar.prototype.__jeySidebarFallbackPatched
) {
return false;
}
var proto = frappe.ui.Sidebar.prototype;
var original = proto.set_workspace_sidebar;
proto.__jeySidebarFallbackPatched = true;
proto.set_workspace_sidebar = function (router) {
original.call(this, router);
try {
var container = this.$items_container;
if (container && container.children().length > 0) return;
// Fallback 1: first sidebar that references the current doctype.
if (this.preferred_sidebars && this.preferred_sidebars.length) {
this.setup(this.preferred_sidebars[0]);
return;
}
// Fallback 2: the home workspace for the route's module.
var module = router && router.meta && router.meta.module;
var workspace = module && this.get_workspace_for_module(module);
if (workspace && frappe.boot.workspace_sidebar_item[workspace.toLowerCase()]) {
this.setup(workspace);
}
} catch (e) {
console.error("[jey_theme] sidebar fallback failed", e);
}
};
return true;
}
// frappe.ui.Sidebar may not be defined yet when this file runs; retry until
// the desk bundle has loaded. Patching the prototype affects the already
// created frappe.app.sidebar instance too, since lookup happens at call time.
if (!patch()) {
var tries = 0;
var timer = setInterval(function () {
if (patch() || ++tries > 100) clearInterval(timer);
}, 50);
}
})();