diff --git a/frappe/boot.py b/frappe/boot.py index bcd4c6c793..29c1666215 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -548,6 +548,8 @@ def get_sidebar_items(): "display_depends_on": si.display_depends_on, "url": si.url, "show_arrow": si.show_arrow, + "filters": si.filters, + "route_options": si.route_options, } if si.link_type == "Report" and si.link_to: report_type, ref_doctype = frappe.db.get_value( diff --git a/frappe/desk/doctype/report_center/__init__.py b/frappe/desk/doctype/report_center/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/report_center/report_center.js b/frappe/desk/doctype/report_center/report_center.js new file mode 100644 index 0000000000..13028656e4 --- /dev/null +++ b/frappe/desk/doctype/report_center/report_center.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Report Center", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/desk/doctype/report_center/report_center.json b/frappe/desk/doctype/report_center/report_center.json new file mode 100644 index 0000000000..8a267eb81d --- /dev/null +++ b/frappe/desk/doctype/report_center/report_center.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:sidebar", + "creation": "2025-11-10 12:49:52.421973", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "sidebar", + "links" + ], + "fields": [ + { + "fieldname": "sidebar", + "fieldtype": "Link", + "label": "Sidebar", + "options": "Workspace Sidebar", + "unique": 1 + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "Report Center Link" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-11-10 12:51:46.093654", + "modified_by": "Administrator", + "module": "Desk", + "name": "Report Center", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe/desk/doctype/report_center/report_center.py b/frappe/desk/doctype/report_center/report_center.py new file mode 100644 index 0000000000..406aee33ff --- /dev/null +++ b/frappe/desk/doctype/report_center/report_center.py @@ -0,0 +1,21 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ReportCenter(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.desk.doctype.report_center_link.report_center_link import ReportCenterLink + from frappe.types import DF + + links: DF.Table[ReportCenterLink] + sidebar: DF.Link | None + # end: auto-generated types + pass diff --git a/frappe/desk/doctype/report_center/test_report_center.py b/frappe/desk/doctype/report_center/test_report_center.py new file mode 100644 index 0000000000..f97f5758d2 --- /dev/null +++ b/frappe/desk/doctype/report_center/test_report_center.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestReportCenter(IntegrationTestCase): + """ + Integration tests for ReportCenter. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/frappe/desk/doctype/report_center_link/__init__.py b/frappe/desk/doctype/report_center_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/report_center_link/report_center_link.json b/frappe/desk/doctype/report_center_link/report_center_link.json new file mode 100644 index 0000000000..b041266f8c --- /dev/null +++ b/frappe/desk/doctype/report_center_link/report_center_link.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-11-10 12:51:15.244944", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "report" + ], + "fields": [ + { + "fieldname": "report", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Report", + "options": "Report" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-11-10 12:52:34.403583", + "modified_by": "Administrator", + "module": "Desk", + "name": "Report Center Link", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe/desk/doctype/report_center_link/report_center_link.py b/frappe/desk/doctype/report_center_link/report_center_link.py new file mode 100644 index 0000000000..369261b9e0 --- /dev/null +++ b/frappe/desk/doctype/report_center_link/report_center_link.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ReportCenterLink(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + report: DF.Link | None + # end: auto-generated types + + pass diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json index 9b331e395e..ab0a89d5bc 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json @@ -21,7 +21,10 @@ "keep_closed", "show_arrow", "column_break_jexf", - "display_depends_on" + "display_depends_on", + "section_break_whjq", + "filters", + "route_options" ], "fields": [ { @@ -60,7 +63,8 @@ "fieldname": "icon", "fieldtype": "Icon", "in_list_view": 1, - "label": "Icon" + "label": "Icon", + "options": "Emojis" }, { "default": "0", @@ -134,13 +138,29 @@ "fieldname": "show_arrow", "fieldtype": "Check", "label": "Show Arrow" + }, + { + "fieldname": "section_break_whjq", + "fieldtype": "Section Break" + }, + { + "fieldname": "filters", + "fieldtype": "Code", + "label": "Filters", + "options": "JSON" + }, + { + "fieldname": "route_options", + "fieldtype": "Code", + "label": "Route Options", + "options": "JSON" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-07 10:47:58.882767", + "modified": "2025-11-11 11:46:36.949606", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Sidebar Item", diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py index 9f82c7271e..db9fa4eb47 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py @@ -17,6 +17,7 @@ class WorkspaceSidebarItem(Document): child: DF.Check collapsible: DF.Check display_depends_on: DF.Code | None + filters: DF.Code | None indent: DF.Check keep_closed: DF.Check label: DF.Data | None @@ -25,6 +26,7 @@ class WorkspaceSidebarItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + route_options: DF.Code | None show_arrow: DF.Check type: DF.Literal["Link", "Section Break", "Spacer"] url: DF.Data | None diff --git a/frappe/desk/page/reports_center/__init__.py b/frappe/desk/page/reports_center/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/page/reports_center/reports_center.css b/frappe/desk/page/reports_center/reports_center.css new file mode 100644 index 0000000000..6033741f3c --- /dev/null +++ b/frappe/desk/page/reports_center/reports_center.css @@ -0,0 +1,14 @@ +.reports-center-wrapper { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin-top: 20px; +} +.reports-list-container{ + width: 70%; +} +.report-details{ + margin-top: 4px; + margin-bottom: 4px; +} \ No newline at end of file diff --git a/frappe/desk/page/reports_center/reports_center.html b/frappe/desk/page/reports_center/reports_center.html new file mode 100644 index 0000000000..6de5341032 --- /dev/null +++ b/frappe/desk/page/reports_center/reports_center.html @@ -0,0 +1,11 @@ + +
+

Reports Center

+
+
+ +
+
+
\ No newline at end of file diff --git a/frappe/desk/page/reports_center/reports_center.js b/frappe/desk/page/reports_center/reports_center.js new file mode 100644 index 0000000000..67c85310b7 --- /dev/null +++ b/frappe/desk/page/reports_center/reports_center.js @@ -0,0 +1,72 @@ +frappe.pages["reports-center"].on_page_load = async function (wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: "Reports Center", + single_column: true, + }); + + page.page_head.hide(); + let module = frappe.route_options?.module; + let reports_data; + $(frappe.render_template("reports_center")).appendTo(page.body); + await frappe.call({ + method: "frappe.desk.page.reports_center.reports_center.get_reports", + args: { + module_name: module, + }, + callback: function (r) { + reports_data = r.message; + render_list(module, reports_data); + }, + }); + + // $('.report-name').on('click', function() { + // const report_name = $(this).data('report'); + // frappe.set_route('query-report', report_name); + // }); +}; + +function render_list(module_name, reports) { + let module_name_wrapper = $(".module-name"); + module_name_wrapper.text(module_name); + const container = $(".report-links"); + container.empty(); + if (!reports || Object.keys(reports).length === 0) { + container.append(`
No reports found.
`); + return; + } + + Object.values(reports).forEach((report_data) => { + const report_html = ` +
+
${report_data.title}
+
+ `; + container.append(report_html); + }); + + // Optional: Add click handler + container.find(".report-name").on("click", function () { + const report_name = $(this).data("report"); + frappe.set_route("query-report", report_name); + }); +} + +frappe.pages["reports-center"].on_page_show = function (wrapper) { + // if (frappe.has_route_options()) { + // let module = frappe.route_options?.module; + // if (module) { + // const filtered = Object.fromEntries( + // Object.entries(frappe.boot.allowed_reports).filter( + // ([, value]) => value.module === module + // ) + // ); + // console.log("Filtered Reports:", filtered); + // render_list(module,filtered); + // } else { + // render_list(module, frappe.boot.allowed_reports); + // } + // } else { + // render_list(frappe.boot.allowed_reports); + // } +}; diff --git a/frappe/desk/page/reports_center/reports_center.json b/frappe/desk/page/reports_center/reports_center.json new file mode 100644 index 0000000000..7d8e0995ff --- /dev/null +++ b/frappe/desk/page/reports_center/reports_center.json @@ -0,0 +1,19 @@ +{ + "content": null, + "creation": "2025-11-09 23:41:57.731008", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2025-11-09 23:41:57.731008", + "modified_by": "Administrator", + "module": "Desk", + "name": "reports-center", + "owner": "Administrator", + "page_name": "reports-center", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 1, + "title": "Reports Center" +} diff --git a/frappe/desk/page/reports_center/reports_center.py b/frappe/desk/page/reports_center/reports_center.py new file mode 100644 index 0000000000..76aae0b908 --- /dev/null +++ b/frappe/desk/page/reports_center/reports_center.py @@ -0,0 +1,15 @@ +from json import dumps + +import frappe +from frappe.boot import get_allowed_pages, get_allowed_reports + + +@frappe.whitelist() +def get_reports(module_name=None): + reports_info = [] + if module_name: + report_center = frappe.get_doc("Report Center", module_name) + for report_links in report_center.links: + if report_links.report in get_allowed_reports().keys(): + reports_info.append(get_allowed_reports()[report_links.report]) + return reports_info diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.html b/frappe/public/js/frappe/ui/sidebar/sidebar.html index 57e2e0f2db..97d85f380f 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.html +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.html @@ -1,4 +1,4 @@ -
+
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 473e190b26..f6b84360ca 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -31,13 +31,20 @@ frappe.ui.Sidebar = class Sidebar { } choose_app_name() { - if (frappe.boot.app_name_style == "Default") return; - frappe.boot.app_data.forEach((a) => { - if (a.workspaces.includes(this.workspace_title)) { - this.app_name = a.app_title; - this.app_logo_url = a.app_logo_url; + if (frappe.boot.app_name_style === "Default") return; + + for (const app of frappe.boot.app_data) { + if (app.workspaces.includes(this.workspace_title)) { + this.app_name = app.app_title; + this.app_logo_url = app.app_logo_url; + return; } - }); + } + + const icon = frappe.boot.desktop_icons.find((i) => i.label === this.workspace_title); + if (icon) { + this.app_name = icon.parent_icon; + } } find_nested_items() { @@ -96,6 +103,7 @@ frappe.ui.Sidebar = class Sidebar { } } make_dom() { + this.load_sidebar_state(); this.wrapper = $( frappe.render_template("sidebar", { expanded: this.sidebar_expanded, @@ -122,7 +130,8 @@ frappe.ui.Sidebar = class Sidebar { let match = false; const that = this; $(".item-anchor").each(function () { - if ($(this).attr("href") == decodeURIComponent(window.location.pathname)) { + let href = $(this).attr("href")?.split("?")[0]; + if (href == decodeURIComponent(window.location.pathname)) { match = true; if (that.active_item) that.active_item.removeClass("active-sidebar"); that.active_item = $(this).parent(); @@ -134,6 +143,15 @@ frappe.ui.Sidebar = class Sidebar { } set_sidebar_state() { + this.load_sidebar_state(); + if (this.workspace_sidebar_items.length === 0) { + this.sidebar_expanded = true; + } + + this.expand_sidebar(); + } + + load_sidebar_state() { this.sidebar_expanded = true; if (localStorage.getItem("sidebar-expanded") !== null) { this.sidebar_expanded = JSON.parse(localStorage.getItem("sidebar-expanded")); @@ -142,12 +160,6 @@ frappe.ui.Sidebar = class Sidebar { if (frappe.is_mobile()) { this.sidebar_expanded = false; } - - if (this.workspace_sidebar_items.length === 0) { - this.sidebar_expanded = true; - } - - this.expand_sidebar(); } empty() { if (this.wrapper.find(".sidebar-items")[0]) { @@ -490,6 +502,14 @@ frappe.ui.Sidebar = class Sidebar { in_list_view: 1, label: "Link To", options: "link_type", + onchange: function () { + if (d.get_value("link_type") == "DocType") { + let doctype = this.get_value(); + if (doctype) { + me.setup_filter(d, doctype); + } + } + }, }, { depends_on: 'eval: doc.link_type == "URL"', @@ -502,9 +522,14 @@ frappe.ui.Sidebar = class Sidebar { 'eval: doc.type == "Link" || (doc.indent == 1 && doc.type == "Section Break")', fieldname: "icon", fieldtype: "Icon", + options: "Emojis", in_list_view: 1, label: "Icon", }, + { + fieldtype: "HTML", + fieldname: "filter_area", + }, { depends_on: 'eval: doc.type == "Section Break"', fieldname: "display_section", @@ -558,6 +583,17 @@ frappe.ui.Sidebar = class Sidebar { options: "JS", max_height: "10px", }, + { + fieldtype: "Section Break", + }, + { + fieldname: "route_options", + fieldtype: "Code", + display_depends_on: "eval: doc.link_type == 'Page'", + label: "Route Options", + options: "JSON", + max_height: "50px", + }, ]; if (opts && opts.item) { dialog_fields.forEach((f) => { @@ -571,12 +607,17 @@ frappe.ui.Sidebar = class Sidebar { }); title = "Edit Sidebar Item"; } - let d = new frappe.ui.Dialog({ + let d; + this.dialog = d = new frappe.ui.Dialog({ title: title, fields: dialog_fields, primary_action_label: "Save", size: "small", primary_action(values) { + if (me.filter_group) { + me.filter_group.get_filters(); + } + if (me.new_sidebar_items.length === 0) { me.new_sidebar_items = Array.from(me.workspace_sidebar_items); } @@ -619,7 +660,36 @@ frappe.ui.Sidebar = class Sidebar { return d; } + setup_filter(d, doctype) { + if (this.filter_group) { + this.filter_group.wrapper.empty(); + delete this.filter_group; + } + // let $loading = this.dialog.get_field("filter_area_loading").$wrapper; + // $(`${__("Loading Filters...")}`).appendTo($loading); + + this.filters = []; + + this.generate_filter_from_json && this.generate_filter_from_json(); + + this.filter_group = new frappe.ui.FilterGroup({ + parent: d.get_field("filter_area").$wrapper, + doctype: doctype, + on_change: () => {}, + }); + + frappe.model.with_doctype(doctype, () => { + this.filter_group.add_filters_to_filter_group(this.filters); + }); + } + hide_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", true); + } + + show_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", false); + } setup_editing_controls() { const me = this; this.save_sidebar_button = this.wrapper.find(".save-sidebar"); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js index b71d464c1b..dc1a928fa2 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -32,6 +32,12 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem { } } else if (this.item.link_type === "URL") { path = this.item.url; + } else if (this.item.link_type == "Page" && this.item.route_options) { + path = frappe.utils.generate_route({ + type: this.item.link_type, + name: this.item.link_to, + route_options: JSON.parse(this.item.route_options), + }); } else { path = frappe.utils.generate_route({ type: this.item.link_type, @@ -176,7 +182,6 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends .appendTo(sidebar_control); this.$drop_icon.removeClass("hidden"); - this.setup_event_listner($item_container); } if (item.keep_closed) { @@ -190,6 +195,8 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends } if (item.show_arrow) { this.$drop_icon = this.wrapper.find('[item-icon="chevron-right"]'); + } + if (item.collapsible || item.show_arrow) { this.setup_event_listner(); } } @@ -214,6 +221,15 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends me.save_section_break_state(); } }); + + $(this.wrapper.find(".standard-sidebar-item")[0]).on("click", (e) => { + me.collapsed = me.$drop_icon.find("use").attr("href") === "#icon-chevron-down"; + me.toggle(); + + if (e.originalEvent.isTrusted) { + me.save_section_break_state(); + } + }); } save_section_break_state() { if (!this.section_breaks_state[this.workspace_title]) {