#42964·frappe

Sidebar resolver drops third-party app sidebars because it filters candidates by the previous DocType's module (version-16)

Author: paulxtCreated Sep 17, 2026Updated Sep 18, 2026

Frappe version: version-16 (reproduced on v16.32.0 and v16.34.0). Not reproducible on develop: the sidebar resolver was rewritten there between 2026-09-04 and 2026-09-08 (latest 4edd7f9 "fix: change dock based on doctype"); resolve_sidebar_for() reads a report's module from frappe.boot.entity_module and its comment says "Resolution never looks at which app the route belongs to". This report asks for a backport of that behaviour, or the minimal patch below, to version-16.

Affected code

  • frappe/public/js/frappe/ui/sidebar/sidebar.js: set_workspace_sidebar() (L665–L690), resolve_sidebar() (L709–L745), filter_sidebars_from_app() (L746–L756)
  • frappe/public/js/frappe/router.js: set_doctype_route() L206 (this.meta = meta)

Steps to reproduce

  1. Install any app besides erpnext that ships a Script Report and a Workspace Sidebar (or just a Module Def — Frappe auto-generates a sidebar per module via auto_generate_sidebar_from_module()). The report's module is the app's own module. Call the app myapp, the module My Module, the report My Report.
  2. Open any ERPNext page that belongs to a different app, e.g. /app/stock-entry (module Stock, app erpnext). The Stock sidebar is shown.
  3. Navigate in-app to the report — via the awesomebar (Ctrl+K → "My Report"), or by pasting /app/query-report/My Report in the URL bar.

Expected

The sidebar switches to My Module (the only sidebar that links to My Report), and the breadcrumb reads My Module / My Report.

Actual

The Stock sidebar stays. The breadcrumb reads Stock / My Report, because set_workspace_breadcrumb() in breadcrumbs.js derives the first crumb from frappe.app.sidebar.sidebar_title.

ERPNext's own reports are not affected: any stale module still belongs to app erpnext, so the app filter keeps their sidebars.

Cause

set_workspace_sidebar(router) passes router?.meta?.module into resolve_sidebar() as the "active module". But router.meta is only assigned in set_doctype_route() (router.js L206) — i.e. for DocType routes. For query-report/* (and Workspaces/*, pages, dashboards) it is never updated, so it still holds the meta of the previous DocType page.

resolve_sidebar() then does:

javascript
// 3. narrow candidates to the active app
if (module) {
    candidates = this.filter_sidebars_from_app(
        candidates,
        frappe.boot.module_app[module.toLowerCase().replace(/[ -]/g, "_")]
    );
}

With module = "Stock" the app becomes erpnext; filter_sidebars_from_app() keeps only sidebars whose app === "erpnext", which removes the third-party sidebar — the only candidate that actually links to the entity. candidates is now empty, rule 4 falls through to resolve_module_sidebar("Stock"), which returns the Stock sidebar, and the final if (!sidebar_name && candidates.length > 0) fallback never fires because the original candidates were discarded.

Two bugs compound:

  1. The module used for narrowing is stale for non-DocType routes.
  2. The app filter can throw away every candidate; when it does, the resolver prefers "a sidebar from the (wrong) app" over "the sidebar that links to the entity".

Suggested fix

Backporting develop's resolver is the complete fix. If that is too large for version-16, either of these alone fixes the reported case; both together are more robust.

a. In resolve_sidebar(), never let the app filter empty the list — fall back to the unfiltered candidates:

javascript
if (module) {
    const app = frappe.boot.module_app[module.toLowerCase().replace(/[ -]/g, "_")];
    const same_app = this.filter_sidebars_from_app(candidates, app);
    if (same_app.length) candidates = same_app;
}

b. Use the entity's own module for reports instead of the stale router.meta. frappe.boot.workspace_sidebar_item[*].items[*].report already carries ref_doctype for report links, and frappe.boot.all_reports[name].module is available, so set_workspace_sidebar() could derive module for query-report routes from the report, and set_doctype_route() is not the only place router.meta should be refreshed (clearing this.meta on non-DocType routes would also stop the stale value from leaking).

A regression test (cypress spec) that navigates stock-entry → query-report/<third-party report> and asserts frappe.app.sidebar.sidebar_title would cover this on both branches.

Workaround for app authors

Ship a Workspace Sidebar + Desktop Icon for the module and, in an app_include_js, call frappe.app.sidebar.setup("<Sidebar>") on frappe.router.on("change") when the route points at one of the app's DocTypes or reports. Once the correct sidebar is active, rule 1 of the resolver ("current sidebar already links to this entity → keep it") holds on its own.