feat: create sidebar from workspace sidebar items

This commit is contained in:
sokumon 2025-08-18 16:16:01 +05:30
parent e1ea6c58f2
commit a51218f511
12 changed files with 198 additions and 154 deletions

View file

@ -14,6 +14,7 @@ from frappe.core.doctype.installed_applications.installed_applications import (
)
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
@ -148,8 +149,11 @@ def load_conf_settings(bootinfo):
def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.sidebar_pages = get_workspace_sidebar_items()
allowed_pages = [d.name for d in bootinfo.sidebar_pages.get("pages")]
bootinfo.desktop_icons = get_desktop_icons()
bootinfo.workspaces = get_workspace_sidebar_items()
bootinfo.workspace_sidebar_item = get_sidebar_items()
print(bootinfo.workspace_sidebar_item)
allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")]
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
bootinfo.dashboards = frappe.get_all("Dashboard")
bootinfo.app_data = []
@ -518,3 +522,26 @@ def get_sentry_dsn():
return
return os.getenv("FRAPPE_SENTRY_DSN")
def get_sidebar_items():
sidebars = frappe.get_all("Workspace Sidebar", pluck="name")
sidebar_items = {}
for s in sidebars:
w = frappe.get_doc("Workspace Sidebar", s)
desktop_icon = frappe.db.get_value("Desktop Icon", w.desktop_icon, "label").lower()
sidebar_items[desktop_icon] = []
for si in w.items:
workspace_sidebar = {
"label": si.label,
"link_to": si.link_to,
"link_type": si.link_type,
"type": si.type,
"icon": si.icon,
"child": si.child,
}
sidebar_items[desktop_icon].append(workspace_sidebar)
return sidebar_items

View file

@ -9,7 +9,9 @@
"label",
"type",
"link_type",
"link_to"
"link_to",
"icon",
"child"
],
"fields": [
{
@ -39,13 +41,26 @@
"in_list_view": 1,
"label": "Link To",
"options": "link_type"
},
{
"fieldname": "icon",
"fieldtype": "Icon",
"in_list_view": 1,
"label": "Icon"
},
{
"default": "0",
"fieldname": "child",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Child Item"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-12 12:55:03.654000",
"modified": "2025-08-18 03:41:56.405534",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Sidebar Item",

View file

@ -14,6 +14,7 @@ class WorkspaceSidebarItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
child: DF.Check
label: DF.Data | None
link_to: DF.DynamicLink | None
link_type: DF.Literal["Page", "DocType", "Report"]

View file

@ -10,6 +10,7 @@ import "./frappe/ui/messages.js";
import "./frappe/ui/keyboard.js";
import "./frappe/ui/colors.js";
import "./frappe/ui/sidebar.html";
import "./frappe/ui/sidebar_item.html";
import "./frappe/ui/sidebar.js";
import "./frappe/ui/sidebar_header.js";
import "./frappe/ui/sidebar_header.html";

View file

@ -292,7 +292,7 @@ frappe.Application = class Application {
setup_workspaces() {
frappe.modules = {};
frappe.workspaces = {};
frappe.boot.allowed_workspaces = frappe.boot.sidebar_pages.pages;
frappe.boot.allowed_workspaces = frappe.boot.workspaces.pages;
for (let page of frappe.boot.allowed_workspaces || []) {
frappe.modules[page.module] = page;

View file

@ -3,14 +3,14 @@ frappe.ui.Sidebar = class Sidebar {
this.items = {};
this.parent_items = [];
this.sidebar_expanded = false;
this.workspace_sidebar_items = [];
if (!frappe.boot.setup_complete) {
// no sidebar if setup is not complete
return;
}
this.set_all_pages();
this.make_dom();
// this.make_dom();
this.sidebar_items = {
public: {},
private: {},
@ -31,12 +31,14 @@ frappe.ui.Sidebar = class Sidebar {
];
this.setup_pages();
this.apps_switcher.populate_apps_menu();
this.handle_outside_click();
}
setup(workspace_title) {
this.make_dom();
this.apps_switcher = new frappe.ui.SidebarHeader(this, workspace_title);
this.make_sidebar(workspace_title.toLowerCase());
}
make_dom() {
this.set_default_app();
this.wrapper = $(frappe.render_template("sidebar")).prependTo("body");
this.$sidebar = this.wrapper.find(".sidebar-items");
@ -64,10 +66,7 @@ frappe.ui.Sidebar = class Sidebar {
}
set_all_pages() {
this.sidebar_pages = frappe.boot.sidebar_pages;
this.all_pages = this.sidebar_pages.pages;
this.has_access = this.sidebar_pages.has_access;
this.has_create_access = this.sidebar_pages.has_create_access;
this.sidebar_items = frappe.boot.workspace_sidebar_item;
}
set_default_app() {
@ -78,21 +77,7 @@ frappe.ui.Sidebar = class Sidebar {
}
set_active_workspace_item() {
if (!frappe.get_route()) return;
let current_route = frappe.get_route();
let current_route_str = frappe.get_route_str();
let current_item;
if (current_route[0] == "Workspaces") {
current_item = current_route[1];
} else if (frappe.breadcrumbs) {
if (Object.keys(frappe.breadcrumbs.all).length == 0) return;
if (frappe.breadcrumbs.all[current_route_str]) {
current_item =
frappe.breadcrumbs.all[current_route_str].workspace ||
frappe.breadcrumbs.all[current_route_str].module;
}
}
if (this.is_route_in_sidebar(current_item)) {
if (this.is_route_in_sidebar()) {
this.active_item.addClass("active-sidebar");
}
if (this.active_item) {
@ -135,11 +120,12 @@ frappe.ui.Sidebar = class Sidebar {
});
return sidebar_item;
}
is_route_in_sidebar(active_module) {
let match = false;
const that = this;
$(".item-anchor").each(function () {
if ($(this).attr("title") == active_module) {
if ($(this).attr("href") == window.location.pathname) {
match = true;
if (that.active_item) that.active_item.removeClass("active-sidebar");
that.active_item = $(this).parent();
@ -152,13 +138,6 @@ frappe.ui.Sidebar = class Sidebar {
setup_pages() {
this.set_all_pages();
this.all_pages.forEach((page) => {
page.is_editable = !page.public || this.has_access;
if (typeof page.content == "string") {
page.content = JSON.parse(page.content);
}
});
if (this.all_pages) {
frappe.workspaces = {};
frappe.workspace_list = [];
@ -176,8 +155,8 @@ frappe.ui.Sidebar = class Sidebar {
}
this.make_sidebar();
}
this.set_hover();
this.set_sidebar_state();
// this.set_hover();
// this.set_sidebar_state();
}
set_sidebar_state() {
this.sidebar_expanded = true;
@ -189,21 +168,31 @@ frappe.ui.Sidebar = class Sidebar {
}
this.expand_sidebar();
}
make_sidebar() {
make_sidebar(workspace_title) {
if (this.wrapper.find(".standard-sidebar-section")[0]) {
this.wrapper.find(".standard-sidebar-section").remove();
}
let app_workspaces = frappe.boot.app_data_map[frappe.current_app || "frappe"].workspaces;
let parent_pages = this.all_pages.filter((p) => !p.parent_page).uniqBy((p) => p.name);
if (frappe.current_app === "private") {
parent_pages = parent_pages.filter((p) => !p.public);
this.workspace_sidebar_items = frappe.boot.workspace_sidebar_item[workspace_title];
let parent_pages = this.workspace_sidebar_items;
if (this.workspace_sidebar_items && this.workspace_sidebar_items.length > 0) {
this.workspace_sidebar_items.unshift({
label: "Home",
icon: "home",
type: "Workspace",
route: `/app/${workspace_title}`,
});
} else {
parent_pages = parent_pages.filter((p) => p.public && app_workspaces.includes(p.name));
this.workspace_sidebar_items = [];
this.workspace_sidebar_items[0] = {
label: "Home",
icon: "home",
type: "Workspace",
route: `/app/${workspace_title}`,
};
}
this.build_sidebar_section("All", parent_pages);
// this.build_sidebar_section("All", parent_pages);
this.create_sidebar();
// Scroll sidebar to selected page if it is not in viewport.
this.wrapper.find(".selected").length &&
@ -213,8 +202,21 @@ frappe.ui.Sidebar = class Sidebar {
this.setup_sorting();
this.set_active_workspace_item();
this.set_hover();
this.set_sidebar_state();
}
create_sidebar() {
if (this.workspace_sidebar_items && this.workspace_sidebar_items.length > 0) {
let parent_links = this.workspace_sidebar_items.filter((f) => f.child !== 1);
parent_links.forEach((w) => {
this.append_item(w, this.wrapper.find(".sidebar-items"));
});
} else {
let no_items_message = $(
"<div class='flex' style='padding: 30px'> No Sidebar Items </div>"
);
this.wrapper.find(".sidebar-items").append(no_items_message);
}
}
build_sidebar_section(title, root_pages) {
let sidebar_section = $(
`<div class="standard-sidebar-section nested-container" data-title="${title}"></div>`
@ -251,13 +253,14 @@ frappe.ui.Sidebar = class Sidebar {
}
// visibility not explicitly set to 0
if (item.visibility !== 0) {
if (item.child !== 0) {
this.append_item(item, child_container);
}
last_item = item;
}
child_container.appendTo(item_container);
}
toggle_sidebar() {
if (!this.sidebar_expanded) {
this.open_sidebar();
@ -265,6 +268,7 @@ frappe.ui.Sidebar = class Sidebar {
this.close_sidebar();
}
}
expand_sidebar() {
let direction;
if (this.sidebar_expanded) {
@ -295,28 +299,28 @@ frappe.ui.Sidebar = class Sidebar {
let $item_container = this.sidebar_item_container(item);
let sidebar_control = $item_container.find(".sidebar-item-control");
let child_items = this.all_pages.filter(
(page) => page.parent_page == item.name || page.parent_page == item.title
);
if (child_items.length > 0) {
let child_container = $item_container.find(".sidebar-child-item");
child_container.addClass("hidden");
this.prepare_sidebar(child_items, child_container, $item_container);
this.parent_items.push($item_container);
if (item.type == "Section Break") {
let current_index = this.workspace_sidebar_items.indexOf(item);
let child_items = this.workspace_sidebar_items
.slice(current_index)
.filter((page) => page.child == 1);
if (child_items.length > 0) {
let child_container = $item_container.find(".sidebar-child-item");
child_container.addClass("hidden");
this.prepare_sidebar(child_items, child_container, $item_container);
this.parent_items.push($item_container);
$item_container.find(".drop-icon").first().addClass("show-in-edit-mode");
}
}
$item_container.appendTo(container);
this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container;
// this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container;
if ($item_container.parent().hasClass("hidden") && is_current_page) {
if ($item_container.parent().hasClass("hidden")) {
$item_container.parent().toggleClass("hidden");
}
this.add_toggle_children(item, sidebar_control, $item_container);
if (child_items.length > 0) {
$item_container.find(".drop-icon").first().addClass("show-in-edit-mode");
}
}
sidebar_item_container(item) {
@ -336,66 +340,32 @@ frappe.ui.Sidebar = class Sidebar {
}
} else if (item.type === "URL") {
path = item.external_link;
} else {
if (item.public) {
path = "/app/" + frappe.router.slug(item.name);
} else {
path = "/app/private/" + frappe.router.slug(item.name.split("-")[0]);
} else if (item.type == "Workspace") {
path = "/app/" + frappe.router.slug(item.label);
if (item.route) {
path = item.route;
}
}
return $(`
<div
class="sidebar-item-container ${item.is_editable ? "is-draggable" : ""}"
item-parent="${item.parent_page}"
item-name="${item.name}"
item-title="${item.title}"
item-public="${item.public || 0}"
item-is-hidden="${item.is_hidden || 0}"
>
<div class="standard-sidebar-item ${item.selected ? "selected" : ""}">
<a
href="${path}"
target="${item.type === "URL" ? "_blank" : ""}"
class="item-anchor ${item.is_editable ? "" : "block-click"}" title="${__(item.title)}"
>
<span class="sidebar-item-icon" item-icon=${item.icon || "folder-normal"}>
${
item.public || item.icon
? frappe.utils.icon(item.icon || "folder-normal", "md")
: `<span class="indicator ${item.indicator_color}"></span>`
}
</span>
<span class="sidebar-item-label">${__(item.title)}<span>
</a>
<div class="sidebar-item-control"></div>
</div>
<div class="sidebar-child-item nested-container"></div>
</div>
`);
console.log(item);
return $(
frappe.render_template("sidebar_item", {
item: item,
path: path,
})
);
}
add_toggle_children(item, sidebar_control, item_container) {
let drop_icon = "es-line-down";
if (
this.current_page &&
item_container.find(`[item-name="${this.current_page.name}"]`).length
) {
let $child_item_section = item_container.find(".sidebar-child-item");
let drop_icon = "es-line-up";
if ($child_item_section.children() > 0) {
drop_icon = "small-up";
}
let $child_item_section = item_container.find(".sidebar-child-item");
let $drop_icon = $(`<button class="btn-reset drop-icon hidden">`)
.html(frappe.utils.icon(drop_icon, "sm"))
.appendTo(sidebar_control);
if (
this.all_pages.some(
(e) =>
(e.parent_page == item.title || e.parent_page == item.name) &&
(e.is_hidden == 0 || !this.is_read_only)
)
) {
if (item.type == "Section Break") {
$drop_icon.removeClass("hidden");
}
$drop_icon.on("click", () => {
@ -406,7 +376,6 @@ frappe.ui.Sidebar = class Sidebar {
} else {
$drop_icon.attr("data-state", "opened").find("use").attr("href", "#es-line-up");
}
``;
$child_item_section.toggleClass("hidden");
});
}

View file

@ -1,12 +1,11 @@
<a class="app-switcher-dropdown" style="text-decoration: none;">
<div class="standard-sidebar-item">
<div class="d-flex">
<div class="d-flex" style="align-items: center;">
<div class="sidebar-item-icon app-logo-container">
<img class="app-logo"
src="{%= app_logo_url %}" alt="{%= ("App Logo") %}">
{%= icon %}
</div>
<div class="sidebar-item-label app-title" style="margin-left: 10px; margin-top: 1px">
{%= app_title %}
{%= workspace_title %}
</div>
</div>
<div class="sidebar-item-control">

View file

@ -1,22 +1,29 @@
frappe.ui.SidebarHeader = class SidebarHeader {
constructor(sidebar) {
constructor(sidebar, workspace_title) {
this.sidebar = sidebar;
this.sidebar_wrapper = $(this.sidebar.wrapper.find(".body-sidebar"));
this.drop_down_expanded = false;
this.workspace_title = workspace_title;
this.make();
this.setup_app_switcher();
this.set_hover();
this.populate_apps_menu();
}
make() {
this.desktop_icon = this.get_desktop_icon();
this.wrapper = $(
frappe.render_template("sidebar_header", {
app_logo_url: frappe.boot.app_data[0].app_logo_url,
app_title: __(frappe.boot.app_data[0].app_title),
workspace_title: this.workspace_title,
icon: frappe.utils.icon(this.desktop_icon.icon, "lg"),
})
).prependTo(this.sidebar_wrapper);
this.app_switcher_dropdown = $(".app-switcher-dropdown");
}
get_desktop_icon() {
return frappe.boot.desktop_icons.filter((f) => f.label == this.workspace_title)[0];
}
setup_app_switcher() {
this.app_switcher_menu = $(".app-switcher-menu");
$(".app-switcher-dropdown").on("click", (e) => {
@ -38,11 +45,8 @@ frappe.ui.SidebarHeader = class SidebarHeader {
}
}
populate_apps_menu() {
this.add_private_app();
this.add_website_select();
this.add_settings_select();
this.setup_select_app();
}
add_app_item(app) {
@ -117,7 +121,6 @@ frappe.ui.SidebarHeader = class SidebarHeader {
}
// refactor them into one single function
add_website_select() {
$(`<div class="divider"></div>`).appendTo(this.app_switcher_menu);
this.add_app_item(
{
app_name: "website",

View file

@ -0,0 +1,26 @@
<div
class="sidebar-item-container {%= item.is_editable ? 'is-draggable' : '' %}"
item-name="{{ item.label }}"
item-title="{{ item.label }}"
>
<div class="standard-sidebar-item" >
<a
href="{{ path }}"
target="{%= item.type === "URL" ? "_blank" : "" %}"
class="item-anchor {% item.is_editable ? "" : "block-click" %}"
title="{{ item.label }}"
>
{% if (item.icon) { %}
<span class="sidebar-item-icon" item-icon="{{ item.icon }}">
{%= frappe.utils.icon(item.icon, "md")%}
</span>
{% } else { %}
<span class="sidebar-item-icon" item-icon="{{ item.icon }}">
</span>
{% } %}
<span class="sidebar-item-label">{{ item.label }}<span>
</a>
<div class="sidebar-item-control"></div>
</div>
<div class="sidebar-child-item nested-container"></div>
</div>

View file

@ -42,7 +42,6 @@ frappe.breadcrumbs = {
}
this.all[frappe.breadcrumbs.current_page()] = obj;
this.update();
frappe.app.sidebar.set_active_workspace_item();
},
current_page() {
@ -74,15 +73,6 @@ frappe.breadcrumbs = {
}
}
if (
breadcrumbs.workspace &&
frappe.workspace_map[breadcrumbs.workspace]?.app &&
frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app
) {
let app = frappe.workspace_map[breadcrumbs.workspace].app;
frappe.app.sidebar.sidebar_header.set_current_app(app);
}
this.toggle(true);
},

View file

@ -21,6 +21,8 @@ frappe.views.Workspace = class Workspace {
constructor(wrapper) {
this.wrapper = $(wrapper);
this.page = wrapper.page;
this.workspaces = frappe.boot.workspaces.pages;
this.blocks = frappe.workspace_block.blocks;
this.is_read_only = true;
this.pages = {};
@ -46,15 +48,29 @@ frappe.views.Workspace = class Workspace {
this.prepare_container();
this.sidebar = frappe.app.sidebar;
this.app_switcher_menu = frappe.app.app_switcher_menu;
this.sidebar.setup_pages();
this.cached_pages = $.extend(true, {}, frappe.boot.sidebar_pages);
this.has_access = frappe.boot.sidebar_pages.has_access;
this.has_create_access = frappe.boot.sidebar_pages.has_create_access;
this.has_access = frappe.boot.workspaces.has_access;
this.has_create_access = frappe.boot.workspaces.has_create_access;
this.setup();
this.show();
this.register_awesomebar_shortcut();
this.setup_sidebar();
}
setup() {
const me = this;
this.workspaces.map((workspace) => {
workspace.is_editable = !workspace.public || me.has_access;
if (typeof workspace.content == "string") {
workspace.content = JSON.parse(workspace.content);
}
});
}
setup_sidebar() {
if (this._page) {
this.sidebar.setup(this._page.name);
}
}
prepare_container() {
this.body = this.wrapper.find(".layout-main-section");
this.prepare_new_and_edit();
@ -104,7 +120,7 @@ frappe.views.Workspace = class Workspace {
}
show() {
if (!this.sidebar.all_pages) {
if (!this.workspaces) {
// pages not yet loaded, call again after a bit
setTimeout(() => this.show(), 100);
return;
@ -167,17 +183,16 @@ frappe.views.Workspace = class Workspace {
};
} else if (
localStorage.current_page &&
this.sidebar.all_pages.filter((page) => page.name == localStorage.current_page)
.length != 0
this.workspaces.filter((page) => page.name == localStorage.current_page).length != 0
) {
default_page = {
name: localStorage.current_page,
public: localStorage.is_current_page_public != "false",
};
} else if (Object.keys(this.sidebar.all_pages).length !== 0) {
} else if (Object.keys(this.workspaces).length !== 0) {
default_page = {
name: this.sidebar.all_pages[0].name,
public: this.sidebar.all_pages[0].public,
name: this.workspaces[0].name,
public: this.workspaces[0].public,
};
} else {
default_page = { name: "Build", public: true };
@ -197,10 +212,10 @@ frappe.views.Workspace = class Workspace {
`).appendTo(this.body.find(".editor-js-container"));
}
if (this.sidebar.all_pages.length) {
if (this.workspaces.length) {
this.create_page_skeleton();
let current_page = this.sidebar.all_pages.find((p) => p.name == page.name);
let current_page = this.workspaces.find((p) => p.name == page.name);
this._page = current_page;
// set app
@ -236,7 +251,6 @@ frappe.views.Workspace = class Workspace {
$(".item-anchor").removeClass("disable-click");
this.remove_page_skeleton();
frappe.app.sidebar.apps_switcher.set_current_app(app);
this.wrapper.find(".workspace-title").html(__(this._page.title));
this.wrapper
.find(".workspace-icon")
@ -282,7 +296,7 @@ frappe.views.Workspace = class Workspace {
}
setup_actions(page) {
let current_page = this.sidebar.all_pages.filter((p) => p.name == page.name)[0];
let current_page = this.workspaces.filter((p) => p.name == page.name)[0];
if (!this.is_read_only) {
this.setup_customization_buttons(current_page);
@ -543,7 +557,6 @@ frappe.views.Workspace = class Workspace {
});
}
frappe.boot.sidebar_pages = r.message;
this.sidebar.setup_pages();
if (!frappe.boot.app_data_map["private"] && new_page.public === 0) {
this.sidebar.apps_switcher.add_private_app();
@ -709,7 +722,7 @@ frappe.views.Workspace = class Workspace {
get_parent_pages(page) {
this.public_parent_pages = [
"",
...this.sidebar.all_pages
...this.workspaces
.filter((p) => p.public && !p.parent_page)
.map((p) => {
return { label: p.title, value: p.name };
@ -717,7 +730,7 @@ frappe.views.Workspace = class Workspace {
];
this.private_parent_pages = [
"",
...this.sidebar.all_pages
...this.workspaces
.filter((p) => !p.public && !p.parent_page)
.map((p) => {
return { label: p.title, value: p.name };

View file

@ -185,7 +185,7 @@ body {
align-items: center;
// padding: 3px 0px 3px 11px;
flex: 1;
height: 30px;
&:hover {
text-decoration: none !important;
}
@ -194,7 +194,6 @@ body {
padding: 7px;
width: 30px;
height: 30px;
svg {
width: 16px;
@ -329,6 +328,7 @@ body {
}
}
// Sidebar Header
.app-switcher-dropdown {
position: relative;
text-decoration: none;