diff --git a/.eslintrc b/.eslintrc
index c5e7d6831a..36c82c3048 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -96,6 +96,7 @@
"hljs": true,
"Awesomplete": true,
"Sortable": true,
+ "gemoji": true,
"Showdown": true,
"Taggle": true,
"Gantt": true,
diff --git a/cypress.config.js b/cypress.config.js
index cfb529b65c..b2b49bc63d 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -36,5 +36,9 @@ module.exports = defineConfig({
testIsolation: false,
baseUrl: "http://test_site_ui:8000",
specPattern: ["./cypress/integration/*.js", "**/ui_test_*.js"],
+ excludeSpecPattern: [
+ "./cypress/integration/workspace.js",
+ "./cypress/integration/workspace_blocks.js",
+ ],
},
});
diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js
index 9b27516c2b..3139db8957 100644
--- a/cypress/integration/awesome_bar.js
+++ b/cypress/integration/awesome_bar.js
@@ -2,11 +2,11 @@ context("Awesome Bar", () => {
before(() => {
cy.visit("/login");
cy.login();
- cy.visit("/app/todo"); // Make sure ToDo filters are cleared.
+ cy.visit("/desk/todo"); // Make sure ToDo filters are cleared.
cy.clear_filters();
- cy.visit("/app/web-page"); // Make sure Blog Post filters are cleared.
+ cy.visit("/desk/web-page"); // Make sure Blog Post filters are cleared.
cy.clear_filters();
- cy.visit("/app/build"); // Go to some other page.
+ cy.visit("/desk/build"); // Go to some other page.
});
beforeEach(() => {
@@ -18,7 +18,7 @@ context("Awesome Bar", () => {
});
after(() => {
- cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec.
+ cy.visit("/desk/todo"); // Make sure we're not bleeding any filters to the next spec.
cy.clear_filters();
});
@@ -28,7 +28,7 @@ context("Awesome Bar", () => {
cy.get(".awesomplete").findByRole("listbox").should("be.visible");
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
- cy.location("pathname").should("eq", "/app/todo");
+ cy.location("pathname").should("eq", "/desk/todo");
});
it("finds text in doctype list", () => {
@@ -40,7 +40,7 @@ context("Awesome Bar", () => {
cy.get('[data-original-title="ID"]:visible > input').should("have.value", "%test%");
// filter preserved, now finds something else
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.get(".title-text").should("contain", "To Do");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"]:visible > input').as("filter");
diff --git a/cypress/integration/control_attach.js b/cypress/integration/control_attach.js
index 70d79855a9..7a079edc6d 100644
--- a/cypress/integration/control_attach.js
+++ b/cypress/integration/control_attach.js
@@ -1,7 +1,7 @@
context("Attach Control", () => {
before(() => {
cy.login();
- cy.visit("/app/doctype");
+ cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@@ -166,7 +166,7 @@ context("Attach Control", () => {
context("Attach Control with Failed Document Save", () => {
before(() => {
cy.login();
- cy.visit("/app/doctype");
+ cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
diff --git a/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js
index 4fc825f80d..d66e1a0a60 100644
--- a/cypress/integration/control_autocomplete.js
+++ b/cypress/integration/control_autocomplete.js
@@ -1,7 +1,7 @@
context("Control Autocomplete", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
cy.wait(4000);
});
diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js
index 96a1bb43d4..067e9b11fe 100644
--- a/cypress/integration/control_barcode.js
+++ b/cypress/integration/control_barcode.js
@@ -1,7 +1,7 @@
context("Control Barcode", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_barcode() {
diff --git a/cypress/integration/control_color.js b/cypress/integration/control_color.js
index aa3a45eed8..773d0faf47 100644
--- a/cypress/integration/control_color.js
+++ b/cypress/integration/control_color.js
@@ -1,7 +1,7 @@
context("Control Color", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_color() {
diff --git a/cypress/integration/control_currency.js b/cypress/integration/control_currency.js
index 9db5dee2a3..5c07e0a380 100644
--- a/cypress/integration/control_currency.js
+++ b/cypress/integration/control_currency.js
@@ -3,7 +3,7 @@ context("Control Currency", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_currency(df_options = {}) {
diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js
index 019ce68214..b4f737a8c9 100644
--- a/cypress/integration/control_data.js
+++ b/cypress/integration/control_data.js
@@ -1,7 +1,7 @@
context("Data Control", () => {
before(() => {
cy.login();
- cy.visit("/app/doctype");
+ cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@@ -39,7 +39,7 @@ context("Data Control", () => {
});
it("check custom formatters", () => {
- cy.visit(`/app/doctype/User`);
+ cy.visit(`/desk/doctype/User`);
cy.get(
'[data-fieldname="fields"] .grid-row[data-idx="3"] [data-fieldname="fieldtype"] .static-area'
).should("have.text", "Section Break");
@@ -49,7 +49,10 @@ context("Data Control", () => {
cy.new_form("Test Data Control");
//Checking the URL for the new form of the doctype
- cy.location("pathname").should("contains", "/app/test-data-control/new-test-data-control");
+ cy.location("pathname").should(
+ "contains",
+ "/desk/test-data-control/new-test-data-control"
+ );
cy.get(".title-text").should("have.text", "New Test Data Control");
cy.get('.frappe-control[data-fieldname="name1"]')
.find("label")
@@ -130,7 +133,7 @@ context("Data Control", () => {
//Checking if the fields contains the data which has been filled in
cy.location("pathname").should(
"not.contains",
- "/app/test-data-control/new-test-data-control"
+ "/desk/test-data-control/new-test-data-control"
);
cy.get_field("name1").should("have.value", "Komal");
cy.get_field("email").should("have.value", "komal@test.com");
diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js
index 0744961147..537aeea035 100644
--- a/cypress/integration/control_date.js
+++ b/cypress/integration/control_date.js
@@ -1,7 +1,7 @@
context("Date Control", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
function get_dialog(date_field_options) {
diff --git a/cypress/integration/control_date_range.js b/cypress/integration/control_date_range.js
index f95a3825cc..608e7dc87a 100644
--- a/cypress/integration/control_date_range.js
+++ b/cypress/integration/control_date_range.js
@@ -1,7 +1,7 @@
context("Date Range Control", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
function get_dialog() {
diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js
index 889e68d12e..18597789ed 100644
--- a/cypress/integration/control_duration.js
+++ b/cypress/integration/control_duration.js
@@ -1,7 +1,7 @@
context("Control Duration", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) {
diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js
index f98e23c57a..d3343a56a3 100644
--- a/cypress/integration/control_dynamic_link.js
+++ b/cypress/integration/control_dynamic_link.js
@@ -1,7 +1,7 @@
context("Dynamic Link", () => {
before(() => {
cy.login();
- cy.visit("/app/doctype");
+ cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@@ -107,7 +107,7 @@ context("Dynamic Link", () => {
});
it("Creating a dynamic link and verifying it", () => {
- cy.visit("/app/test-dynamic-link");
+ cy.visit("/desk/test-dynamic-link");
//Clicking on the Document ID field
cy.get_field("doc_type").clear();
diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js
index 25936066cd..b5575edc7b 100644
--- a/cypress/integration/control_float.js
+++ b/cypress/integration/control_float.js
@@ -1,7 +1,7 @@
context("Control Float", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_float() {
diff --git a/cypress/integration/control_icon.js b/cypress/integration/control_icon.js
index 406e9f1162..edb7c399f8 100644
--- a/cypress/integration/control_icon.js
+++ b/cypress/integration/control_icon.js
@@ -1,7 +1,7 @@
context("Control Icon", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_icon() {
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index e263017bb3..b77fce3399 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -1,11 +1,11 @@
context("Control Link", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
beforeEach(() => {
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.create_records({
doctype: "ToDo",
description: "this is a test todo for link",
@@ -120,7 +120,7 @@ context("Control Link", () => {
cy.get("@input").trigger("mouseover");
cy.get(".frappe-control[data-fieldname=link] .btn-open")
.should("be.visible")
- .should("have.attr", "href", `/app/todo/${todos[0]}`);
+ .should("have.attr", "href", `/desk/todo/${todos[0]}`);
});
});
@@ -176,7 +176,7 @@ context("Control Link", () => {
it("should update dependant fields (via fetch_from)", () => {
cy.get("@todos").then((todos) => {
- cy.visit(`/app/todo/${todos[0]}`);
+ cy.visit(`/desk/todo/${todos[0]}`);
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link");
diff --git a/cypress/integration/control_markdown_editor.js b/cypress/integration/control_markdown_editor.js
index 24ab32f48a..520dd22024 100644
--- a/cypress/integration/control_markdown_editor.js
+++ b/cypress/integration/control_markdown_editor.js
@@ -1,11 +1,11 @@
context("Control Markdown Editor", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
it("should allow inserting images by drag and drop", () => {
- cy.visit("/app/web-page/new");
+ cy.visit("/desk/web-page/new");
cy.fill_field("content_type", "Markdown", "Select");
cy.get_field("main_section_md", "Markdown Editor").selectFile(
"cypress/fixtures/sample_image.jpg",
diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js
index 103b813013..adfd82b0c8 100644
--- a/cypress/integration/control_phone.js
+++ b/cypress/integration/control_phone.js
@@ -3,7 +3,7 @@ import doctype_with_phone from "../fixtures/doctype_with_phone";
context("Control Phone", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
afterEach(() => {
@@ -68,13 +68,13 @@ context("Control Phone", () => {
});
it("existing document should render phone field with data", () => {
- cy.visit("/app/doctype");
+ cy.visit("/desk/doctype");
cy.insert_doc("DocType", doctype_with_phone, true);
cy.clear_cache();
// Creating custom doctype
cy.insert_doc("DocType", doctype_with_phone, true);
- cy.visit("/app/doctype-with-phone");
+ cy.visit("/desk/doctype-with-phone");
cy.click_listview_primary_button("Add Doctype With Phone");
// create a record
diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js
index 613a6e9f92..1efee0857b 100644
--- a/cypress/integration/control_rating.js
+++ b/cypress/integration/control_rating.js
@@ -1,7 +1,7 @@
context("Control Rating", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_rating() {
diff --git a/cypress/integration/control_select.js b/cypress/integration/control_select.js
index 5f7a07e0c4..8aec4121bf 100644
--- a/cypress/integration/control_select.js
+++ b/cypress/integration/control_select.js
@@ -1,7 +1,7 @@
context("Control Select", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
function get_dialog_with_select() {
diff --git a/cypress/integration/custom_buttons.js b/cypress/integration/custom_buttons.js
index fd93613900..ac3027bfba 100644
--- a/cypress/integration/custom_buttons.js
+++ b/cypress/integration/custom_buttons.js
@@ -40,7 +40,7 @@ describe(
() => {
before(() => {
cy.login();
- cy.visit(`/app/note/new`);
+ cy.visit(`/desk/note/new`);
// close the sidebar cause default is expanded
cy.get(".body-sidebar .collapse-sidebar-link").click();
});
diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js
index bd8ca1d73b..a8cd4f767f 100644
--- a/cypress/integration/customize_form.js
+++ b/cypress/integration/customize_form.js
@@ -1,7 +1,7 @@
context("Customize Form", () => {
before(() => {
cy.login();
- cy.visit("/app/customize-form");
+ cy.visit("/desk/customize-form");
});
it("Changing to naming rule should update autoname", () => {
cy.fill_field("doc_type", "ToDo", "Link").blur();
diff --git a/cypress/integration/dashboard.js b/cypress/integration/dashboard.js
index 6eb28567bc..21e3f3f789 100644
--- a/cypress/integration/dashboard.js
+++ b/cypress/integration/dashboard.js
@@ -1,7 +1,7 @@
describe("Dashboard view", { scrollBehavior: false }, () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
it("should load", () => {
diff --git a/cypress/integration/dashboard_chart.js b/cypress/integration/dashboard_chart.js
index f2a837e4b3..487a0ea42e 100644
--- a/cypress/integration/dashboard_chart.js
+++ b/cypress/integration/dashboard_chart.js
@@ -1,7 +1,7 @@
context("Dashboard Chart", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("Check filter populate for child table doctype", () => {
diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js
index ebcdfa0048..627273ea35 100644
--- a/cypress/integration/dashboard_links.js
+++ b/cypress/integration/dashboard_links.js
@@ -24,7 +24,7 @@ context("Dashboard links", () => {
});
it("Adding a new contact, checking for the counter on the dashboard and deleting the created contact", () => {
- cy.visit("/app/contact");
+ cy.visit("/desk/contact");
cy.clear_filters();
cy.visit(`/app/user/${cy.config("testUser")}`);
@@ -48,7 +48,7 @@ context("Dashboard links", () => {
cy.get('[data-doctype="Contact"]').contains("Contact").click();
//Deleting the newly created contact
- cy.visit("/app/contact");
+ cy.visit("/desk/contact");
cy.get(".list-subject > .select-like > .list-row-checkbox").eq(0).click({ force: true });
cy.findByRole("button", { name: "Actions" }).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
@@ -56,7 +56,7 @@ context("Dashboard links", () => {
//To check if the counter from the "Contact" doc link is removed
cy.wait(700);
- cy.visit("/app/user");
+ cy.visit("/desk/user");
cy.get(".list-row-col > .level-item > .ellipsis").eq(0).click({ force: true });
cy.get('[data-doctype="Contact"]').should("contain", "Contact");
});
diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js
index 49513e72fb..1bb839f573 100644
--- a/cypress/integration/data_field_form_validation.js
+++ b/cypress/integration/data_field_form_validation.js
@@ -4,7 +4,7 @@ const doctype_name = data_field_validation_doctype.name;
context("Data Field Input Validation in New Form", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy.insert_doc("DocType", data_field_validation_doctype, true);
});
diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js
index 7a8a68c1d9..93ebfed969 100644
--- a/cypress/integration/datetime.js
+++ b/cypress/integration/datetime.js
@@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name;
context("Control Date, Time and DateTime", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy.insert_doc("DocType", datetime_doctype, true);
});
diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js
index 6419809466..74be1fc493 100644
--- a/cypress/integration/depends_on.js
+++ b/cypress/integration/depends_on.js
@@ -1,7 +1,7 @@
context("Depends On", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js
index 9905bc7461..5e56d0e596 100644
--- a/cypress/integration/file_uploader.js
+++ b/cypress/integration/file_uploader.js
@@ -4,7 +4,7 @@ context("FileUploader", () => {
});
beforeEach(() => {
- cy.visit("/app");
+ cy.visit("/desk");
cy.wait(2000); // workspace can load async and clear active dialog
});
diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index d40b0d5c89..c533d14f03 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -17,7 +17,7 @@ const type_value = (value) => {
context("Form", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@@ -28,11 +28,11 @@ context("Form", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("create a new form", () => {
- cy.visit("/app/todo/new");
+ cy.visit("/desk/todo/new");
cy.get_field("description", "Text Editor")
.type("this is a test todo", { force: true })
.wait(1000);
@@ -51,7 +51,7 @@ context("Form", () => {
});
it("navigates between documents with child table list filters applied", () => {
- cy.visit("/app/contact");
+ cy.visit("/desk/contact");
cy.clear_filters();
cy.get('.standard-filter-section [data-fieldname="name"] input')
@@ -60,7 +60,7 @@ context("Form", () => {
cy.click_listview_row_item_with_text("Test Form Contact 3");
// clear filters
- cy.visit("/app/contact");
+ cy.visit("/desk/contact");
cy.clear_filters();
});
@@ -70,7 +70,7 @@ context("Form", () => {
let valid_email = "user@email.com";
let expectBackgroundColor = "rgb(255, 245, 245)";
- cy.visit("/app/contact/new");
+ cy.visit("/desk/contact/new");
cy.fill_field("company_name", "Test Company");
cy.get('.frappe-control[data-fieldname="email_ids"]').as("table");
@@ -105,7 +105,7 @@ context("Form", () => {
});
it("update docfield property using set_df_property in child table", () => {
- cy.visit("/app/contact/Test Form Contact 1");
+ cy.visit("/desk/contact/Test Form Contact 1");
cy.window()
.its("cur_frm")
.then((frm) => {
diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js
index 04f698a46a..da4b9e75fb 100644
--- a/cypress/integration/form_builder.js
+++ b/cypress/integration/form_builder.js
@@ -3,18 +3,18 @@ const doctype_name = form_builder_doctype.name;
context("Form Builder", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
return cy.insert_doc("DocType", form_builder_doctype, true);
});
it("Open Form Builder for Web Form Doctype/Customize Form", () => {
// doctype
- cy.visit("/app/doctype/Web Form");
+ cy.visit("/desk/doctype/Web Form");
cy.findByRole("tab", { name: "Form" }).click();
cy.get(".form-builder-container").should("exist");
// customize form
- cy.visit("/app/customize-form?doc_type=Web%20Form");
+ cy.visit("/desk/customize-form?doc_type=Web%20Form");
cy.findByRole("tab", { name: "Form" }).click();
cy.get(".form-builder-container").should("exist");
});
@@ -264,7 +264,7 @@ context("Form Builder", () => {
cy.findByRole("button", { name: "Save" }).click({ force: true });
- cy.visit("/app/form-builder-doctype/new");
+ cy.visit("/desk/form-builder-doctype/new");
cy.get("[data-fieldname='data3'] .clearfix label").should("have.text", "New Title");
});
diff --git a/cypress/integration/form_tab_break.js b/cypress/integration/form_tab_break.js
index c4f7626e3d..feb3ffd208 100644
--- a/cypress/integration/form_tab_break.js
+++ b/cypress/integration/form_tab_break.js
@@ -3,7 +3,7 @@ const doctype_name = doctype_with_tab_break.name;
context("Form Tab Break", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy.insert_doc("DocType", doctype_with_tab_break, true);
});
it("Should switch tab and open correct tabs on validation error", () => {
diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js
index f4ae0dbb6d..4a43626c55 100644
--- a/cypress/integration/form_tour.js
+++ b/cypress/integration/form_tour.js
@@ -1,7 +1,7 @@
context.skip("Form Tour", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
return cy
.window()
.its("frappe")
@@ -11,11 +11,11 @@ context.skip("Form Tour", () => {
});
const open_test_form_tour = () => {
- cy.visit("/app/form-tour/Test Form Tour");
+ cy.visit("/desk/form-tour/Test Form Tour");
cy.findByRole("button", { name: "Show Tour" }).should("be.visible").as("show_tour");
cy.get("@show_tour").click();
cy.wait(500);
- cy.url().should("include", "/app/contact");
+ cy.url().should("include", "/desk/contact");
};
it("jump to a form tour", open_test_form_tour);
diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js
index ae9b59f868..607db6746e 100644
--- a/cypress/integration/grid.js
+++ b/cypress/integration/grid.js
@@ -1,11 +1,11 @@
context("Grid", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@@ -16,7 +16,7 @@ context("Grid", () => {
});
});
it("update docfield property using update_docfield_property", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@@ -40,7 +40,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_display", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@@ -64,7 +64,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_enable", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@@ -88,7 +88,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_reqd", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js
index a367414489..f8fdd89b6c 100644
--- a/cypress/integration/grid_configuration.js
+++ b/cypress/integration/grid_configuration.js
@@ -1,7 +1,7 @@
context("Grid Configuration", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website-settings");
+ cy.visit("/desk/website-settings");
});
it("Set user wise grid settings", () => {
cy.findByRole("tab", { name: "Navbar" }).click();
diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js
index 097f2a5cdc..6a6a3d2d2b 100644
--- a/cypress/integration/grid_pagination.js
+++ b/cypress/integration/grid_pagination.js
@@ -1,11 +1,11 @@
context("Grid Pagination", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@@ -16,14 +16,14 @@ context("Grid Pagination", () => {
});
});
it("creates pages for child table", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".current-page-number").should("have.value", "1");
cy.get("@table").find(".total-page-number").should("contain", "20");
cy.get("@table").find(".grid-body .grid-row").should("have.length", 50);
});
it("goes to the next and previous page", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".next-page").click();
cy.get("@table").find(".current-page-number").should("have.value", "2");
@@ -36,7 +36,7 @@ context("Grid Pagination", () => {
cy.get("@table").find(".grid-body .grid-row").first().should("have.attr", "data-idx", "1");
});
it("adds and deletes rows and changes page", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").findByRole("button", { name: "Add Row" }).click();
cy.get("@table").find(".grid-body .row-index").should("contain", 1001);
@@ -49,7 +49,7 @@ context("Grid Pagination", () => {
cy.get("@table").find(".total-page-number").should("contain", "20");
});
it("go to specific page, use up and down arrow, type characters, 0 page and more than existing page", () => {
- cy.visit("/app/contact/Test Contact");
+ cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".current-page-number").focus().clear().type("17").blur();
cy.get("@table").find(".grid-body .row-index").should("contain", 801);
diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js
index 3d43412313..66364501a6 100644
--- a/cypress/integration/grid_search.js
+++ b/cypress/integration/grid_search.js
@@ -7,7 +7,7 @@ context("Grid Search", () => {
before(() => {
cy.visit("/login");
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.insert_doc("DocType", child_table_doctype, true);
cy.insert_doc("DocType", child_table_doctype_1, true);
cy.insert_doc("DocType", doctype_with_child_table, true);
@@ -40,7 +40,7 @@ context("Grid Search", () => {
});
});
- cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
+ cy.visit(`/desk/doctype-with-child-table/Test Grid Search`);
cy.get('.frappe-control[data-fieldname="child_table_1"]').as("table");
cy.get("@table").find(".grid-row-check:last").click();
@@ -49,7 +49,7 @@ context("Grid Search", () => {
});
it("test search field for different fieldtypes", () => {
- cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
+ cy.visit(`/desk/doctype-with-child-table/Test Grid Search`);
cy.get('.frappe-control[data-fieldname="child_table_1"]').as("table");
diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js
index c529b4e4ff..f4834450e2 100644
--- a/cypress/integration/kanban.js
+++ b/cypress/integration/kanban.js
@@ -1,11 +1,11 @@
context("Kanban Board", () => {
before(() => {
cy.login("frappe@example.com");
- cy.visit("/app");
+ cy.visit("/desk");
});
it("Create ToDo Kanban", () => {
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.get(".page-actions .custom-btn-group button").click();
cy.get(".page-actions .custom-btn-group ul.dropdown-menu li").contains("Kanban").click();
@@ -33,7 +33,7 @@ context("Kanban Board", () => {
});
it("Add and Remove fields", () => {
- cy.visit("/app/todo/view/kanban/ToDo Kanban");
+ cy.visit("/desk/todo/view/kanban/ToDo Kanban");
cy.intercept(
"POST",
@@ -110,7 +110,7 @@ context("Kanban Board", () => {
cy.switch_to_user(not_system_manager);
- cy.visit("/app/todo/view/kanban/Admin Kanban");
+ cy.visit("/desk/todo/view/kanban/Admin Kanban");
// Menu button should be hidden (dropdown for 'Save Filters' and 'Delete Kanban Board')
cy.get(".no-list-sidebar .menu-btn-group .btn-default[data-original-title='Menu']").should(
diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js
index 2ce347828a..21d39081d1 100644
--- a/cypress/integration/list_paging.js
+++ b/cypress/integration/list_paging.js
@@ -1,7 +1,7 @@
context("List Paging", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@@ -11,7 +11,7 @@ context("List Paging", () => {
});
it("test load more with count selection buttons", () => {
- cy.visit("/app/todo/view/report");
+ cy.visit("/desk/todo/view/report");
cy.clear_filters();
cy.get(".list-paging-area .list-count").should("contain.text", "20 of");
diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js
index 397c91215b..b88ea04f47 100644
--- a/cypress/integration/list_view.js
+++ b/cypress/integration/list_view.js
@@ -1,7 +1,7 @@
context("List View", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy
.window()
.its("frappe")
diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js
index 027dfaa768..57c7f3023d 100644
--- a/cypress/integration/list_view_settings.js
+++ b/cypress/integration/list_view_settings.js
@@ -1,17 +1,17 @@
context("List View Settings", () => {
beforeEach(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("Default settings", () => {
- cy.visit("/app/List/DocType/List");
+ cy.visit("/desk/List/DocType/List");
cy.clear_filters();
cy.get(".list-count").should("contain", "20 of");
cy.get(".list-stats").should("contain", "Tags");
});
it("disable count and sidebar stats then verify", () => {
cy.wait(300);
- cy.visit("/app/List/DocType/List");
+ cy.visit("/desk/List/DocType/List");
cy.clear_filters();
cy.wait(300);
cy.get(".list-count").should("contain", "20 of");
diff --git a/cypress/integration/login.js b/cypress/integration/login.js
index 7eb62fefa5..aec28ac06a 100644
--- a/cypress/integration/login.js
+++ b/cypress/integration/login.js
@@ -36,7 +36,7 @@ context("Login", () => {
cy.get("#login_password").type(Cypress.env("adminPassword"));
cy.findByRole("button", { name: "Login" }).click();
- cy.location("pathname").should("match", /^\/app/);
+ cy.location("pathname").should("match", /^\/desk/);
cy.window().its("frappe.session.user").should("eq", "Administrator");
});
diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js
index abf6604bec..463f39bdb4 100644
--- a/cypress/integration/multi_select_dialog.js
+++ b/cypress/integration/multi_select_dialog.js
@@ -1,7 +1,7 @@
context("MultiSelectDialog", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
const contact_template = {
doctype: "Contact",
first_name: "Test",
diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js
index a5e579d12a..5194498985 100644
--- a/cypress/integration/navigation.js
+++ b/cypress/integration/navigation.js
@@ -2,7 +2,7 @@ context("Navigation", () => {
before(() => {
cy.visit("/login");
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("Navigate to route with hash in document name", () => {
cy.insert_doc(
@@ -15,14 +15,14 @@ context("Navigation", () => {
},
true
);
- cy.visit(`/app/client-script/${encodeURIComponent("ABC#123")}`);
+ cy.visit(`/desk/client-script/${encodeURIComponent("ABC#123")}`);
cy.title().should("eq", "ABC#123");
cy.go("back");
cy.title().should("eq", "Website");
});
it("Navigate to previous page after login", () => {
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.get(".page-head").findByTitle("To Do").should("be.visible");
cy.clear_filters();
cy.call("logout");
@@ -31,6 +31,6 @@ context("Navigation", () => {
cy.location("pathname").should("eq", "/login");
cy.login();
cy.reload().as("reload");
- cy.location("pathname").should("eq", "/app/todo");
+ cy.location("pathname").should("eq", "/desk/todo");
});
});
diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js
index 24227fe27b..c3e3edcf5e 100644
--- a/cypress/integration/number_card.js
+++ b/cypress/integration/number_card.js
@@ -1,7 +1,7 @@
context("Number Card", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("Check filter populate for child table doctype", () => {
diff --git a/cypress/integration/permissions.js b/cypress/integration/permissions.js
index 9c21a7914d..8702302264 100644
--- a/cypress/integration/permissions.js
+++ b/cypress/integration/permissions.js
@@ -2,11 +2,11 @@ context.skip("Permissions API", () => {
before(() => {
cy.visit("/login");
cy.remove_role("frappe@example.com", "System Manager");
- cy.visit("/app");
+ cy.visit("/desk");
});
it("Checks permissions via `has_perm` for Kanban Board DocType", () => {
- cy.visit("/app/kanban-board/view/list");
+ cy.visit("/desk/kanban-board/view/list");
cy.window()
.its("frappe")
.then((frappe) => {
@@ -20,7 +20,7 @@ context.skip("Permissions API", () => {
});
it("Checks permissions via `get_perm` for Kanban Board DocType", () => {
- cy.visit("/app/kanban-board/view/list");
+ cy.visit("/desk/kanban-board/view/list");
cy.window()
.its("frappe")
.then((frappe) => {
diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js
index 4a2d753ff0..8df908683c 100644
--- a/cypress/integration/query_report.js
+++ b/cypress/integration/query_report.js
@@ -1,7 +1,7 @@
context("Query Report", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.insert_doc(
"Report",
{
@@ -19,7 +19,7 @@ context("Query Report", () => {
});
it("add custom column in report", () => {
- cy.visit("/app/query-report/Permitted Documents For User");
+ cy.visit("/desk/query-report/Permitted Documents For User");
cy.get(".page-form.flex", { timeout: 60000 })
.should("have.length", 1)
@@ -77,12 +77,12 @@ context("Query Report", () => {
.findByRole("button", { name: "Submit" })
.click({ timeout: 1000, force: true });
- cy.visit("/app/query-report/" + report);
+ cy.visit("/desk/query-report/" + report);
cy.get(".datatable").should("exist");
};
it("test multi level query report", () => {
- cy.visit("/app/query-report/Test ToDo Report");
+ cy.visit("/desk/query-report/Test ToDo Report");
cy.get(".datatable").should("exist");
save_report_and_open("Test ToDo Report 1", " 1");
diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js
index 27fe840450..8311511e87 100644
--- a/cypress/integration/report_view.js
+++ b/cypress/integration/report_view.js
@@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
context("Report View", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.insert_doc("DocType", custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(
diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js
index f778e009bb..89e305b1f6 100644
--- a/cypress/integration/rounding.js
+++ b/cypress/integration/rounding.js
@@ -1,7 +1,7 @@
context("Rounding behaviour", () => {
before(() => {
cy.login();
- cy.visit("/app/");
+ cy.visit("/desk/");
});
it("Commercial Rounding", () => {
diff --git a/cypress/integration/routing.js b/cypress/integration/routing.js
index 79c0cea9dc..8b5c3a8880 100644
--- a/cypress/integration/routing.js
+++ b/cypress/integration/routing.js
@@ -1,4 +1,4 @@
-const list_view = "/app/todo";
+const list_view = "/desk/todo";
// test round trip with filter types
diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js
index 5d170ca9ca..fc12929ba7 100644
--- a/cypress/integration/sidebar.js
+++ b/cypress/integration/sidebar.js
@@ -38,7 +38,7 @@ context("Sidebar", () => {
before(() => {
cy.visit("/");
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
return cy
.window()
.its("frappe")
@@ -87,7 +87,7 @@ context("Sidebar", () => {
description: "Sidebar Attachment ToDo",
}).then((todo) => {
let todo_name = todo.message.name;
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
//To check if no filter is available in "Assigned To" dropdown
@@ -99,7 +99,7 @@ context("Sidebar", () => {
cy.get_field("assign_to_me", "Check").click();
cy.wait(1000);
cy.get(".modal-footer > .standard-actions > .btn-primary").click();
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
//To check if filter is added in "Assigned To" dropdown after assignment
@@ -136,7 +136,7 @@ context("Sidebar", () => {
cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click();
cy.get(".remove-btn").click({ force: true });
cy.hide_dialog();
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
cy.get(".empty-state").should("contain", "No filters found");
});
diff --git a/cypress/integration/socket_updates.js b/cypress/integration/socket_updates.js
index 4253618e18..4b892d95c7 100644
--- a/cypress/integration/socket_updates.js
+++ b/cypress/integration/socket_updates.js
@@ -4,7 +4,7 @@ context("Realtime updates", () => {
});
beforeEach(() => {
- cy.visit("/app/todo");
+ cy.visit("/desk/todo");
// required because immediately after load socket is still connecting.
// Not a huge deal breaker in prod.
cy.wait(500);
diff --git a/cypress/integration/url_data_field.js b/cypress/integration/url_data_field.js
index c74bc8b1a2..c6f45b5488 100644
--- a/cypress/integration/url_data_field.js
+++ b/cypress/integration/url_data_field.js
@@ -5,7 +5,7 @@ const doctype_name = data_field_validation_doctype.name;
context("URL Data Field Input", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
return cy.insert_doc("DocType", data_field_validation_doctype, true);
});
diff --git a/cypress/integration/utils.js b/cypress/integration/utils.js
index 083a03294a..d3fd3d4016 100644
--- a/cypress/integration/utils.js
+++ b/cypress/integration/utils.js
@@ -1,7 +1,7 @@
context("Utils", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
function run_util(name, ...args) {
diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js
index 5942d4f005..5bb3c2d8db 100644
--- a/cypress/integration/view_routing.js
+++ b/cypress/integration/view_routing.js
@@ -1,11 +1,11 @@
context("View", () => {
before(() => {
cy.login();
- cy.visit("/app/website");
+ cy.visit("/desk/website");
});
it("Route to ToDo List View", () => {
- cy.visit("/app/todo/view/list");
+ cy.visit("/desk/todo/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -15,7 +15,7 @@ context("View", () => {
});
it("Route to ToDo Report View", () => {
- cy.visit("/app/todo/view/report");
+ cy.visit("/desk/todo/view/report");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -25,7 +25,7 @@ context("View", () => {
});
it("Route to ToDo Dashboard View", () => {
- cy.visit("/app/todo/view/dashboard");
+ cy.visit("/desk/todo/view/dashboard");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -35,7 +35,7 @@ context("View", () => {
});
it("Route to ToDo Gantt View", () => {
- cy.visit("/app/todo/view/gantt");
+ cy.visit("/desk/todo/view/gantt");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -46,7 +46,7 @@ context("View", () => {
it("Route to ToDo Kanban View", () => {
cy.call("frappe.tests.ui_test_helpers.create_kanban").then(() => {
- cy.visit("/app/note/view/kanban/_Note _Kanban");
+ cy.visit("/desk/note/view/kanban/_Note _Kanban");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -57,7 +57,7 @@ context("View", () => {
});
it("Route to ToDo Calendar View", () => {
- cy.visit("/app/todo/view/calendar");
+ cy.visit("/desk/todo/view/calendar");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -68,7 +68,7 @@ context("View", () => {
it("Route to Custom Tree View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_tree_doctype").then(() => {
- cy.visit("/app/custom-tree/view/tree");
+ cy.visit("/desk/custom-tree/view/tree");
cy.wait(500);
cy.window()
.its("cur_tree")
@@ -137,7 +137,7 @@ context("View", () => {
it("Route to default view from app/{doctype}", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
- cy.visit("/app/event");
+ cy.visit("/desk/event");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -149,7 +149,7 @@ context("View", () => {
it("Route to default view from app/{doctype}/view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
- cy.visit("/app/event/view");
+ cy.visit("/desk/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -164,7 +164,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
- cy.visit("/app/event");
+ cy.visit("/desk/event");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -179,7 +179,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
- cy.visit("/app/event/view");
+ cy.visit("/desk/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -194,7 +194,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
- cy.visit("/app/event/view/list");
+ cy.visit("/desk/event/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
@@ -206,17 +206,17 @@ context("View", () => {
it("Validate Route History for Default View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
- cy.visit("/app/event");
- cy.visit("/app/event/view/list");
- cy.location("pathname").should("eq", "/app/event/view/list");
+ cy.visit("/desk/event");
+ cy.visit("/desk/event/view/list");
+ cy.location("pathname").should("eq", "/desk/event/view/list");
cy.go("back");
- cy.location("pathname").should("eq", "/app/event");
+ cy.location("pathname").should("eq", "/desk/event");
});
});
it("Route to Form", () => {
const test_user = cy.config("testUser");
- cy.visit(`/app/user/${test_user}`);
+ cy.visit(`/desk/user/${test_user}`);
cy.window()
.its("cur_frm")
.then((frm) => {
@@ -225,7 +225,7 @@ context("View", () => {
});
it("Route to Website Workspace", () => {
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.get(".title-text").should("contain", "Website");
});
});
diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js
index 882181349f..03bd244c94 100644
--- a/cypress/integration/web_form.js
+++ b/cypress/integration/web_form.js
@@ -1,7 +1,7 @@
context("Web Form", () => {
before(() => {
cy.login("Administrator");
- cy.visit("/app/");
+ cy.visit("/desk/");
return cy
.window()
.its("frappe")
@@ -11,7 +11,7 @@ context("Web Form", () => {
});
it("Create Web Form", () => {
- cy.visit("/app/web-form/new");
+ cy.visit("/desk/web-form/new");
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
@@ -55,7 +55,7 @@ context("Web Form", () => {
it("Login Required", () => {
cy.call("logout");
cy.login("Administrator");
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="login_required"]').check({ force: true });
@@ -74,7 +74,7 @@ context("Web Form", () => {
it("Show List", () => {
cy.login("Administrator");
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get(".section-head").contains("List Settings").click();
@@ -88,7 +88,7 @@ context("Web Form", () => {
});
it("Show Custom List Title", () => {
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
@@ -111,7 +111,7 @@ context("Web Form", () => {
cy.get(".web-list-table thead th").contains("Sr.");
cy.get(".web-list-table thead th").contains("Title");
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
@@ -160,7 +160,7 @@ context("Web Form", () => {
});
it("Custom Breadcrumbs", () => {
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Customization" }).click();
cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code");
@@ -192,7 +192,7 @@ context("Web Form", () => {
});
it("Edit Mode", () => {
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_edit"]').check();
@@ -216,7 +216,7 @@ context("Web Form", () => {
});
it("Allow Multiple Response", () => {
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_multiple"]').check();
@@ -234,7 +234,7 @@ context("Web Form", () => {
});
it("Allow Delete", () => {
- cy.visit("/app/web-form/note");
+ cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_delete"]').check();
diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js
index 97db14d683..96b99f2e25 100644
--- a/cypress/integration/workspace.js
+++ b/cypress/integration/workspace.js
@@ -2,16 +2,23 @@ context("Workspace 2.0", () => {
before(() => {
cy.visit("/login");
cy.login();
+ return cy
+ .window()
+ .its("frappe")
+ .then((frappe) => {
+ return frappe.xcall("frappe.tests.ui_test_helpers.empty_my_workspaces");
+ });
});
it("Navigate to page from sidebar", () => {
- cy.visit("/app/build");
+ cy.visit("/desk/build");
cy.get(".codex-editor__redactor .ce-block");
- cy.get('.sidebar-item-container[item-title="Website"]').first().click();
- cy.location("pathname").should("eq", "/app/website");
+ cy.get('.sidebar-item-container[item-name="Page"]').first().click();
+ cy.location("pathname").should("eq", "/desk/page");
});
it("Create Private Page", () => {
+ cy.visit("/desk/website");
cy.intercept({
method: "POST",
url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
@@ -27,61 +34,16 @@ context("Workspace 2.0", () => {
cy.get_open_dialog().find(".btn-primary").click();
// check if sidebar item is added in pubic section
- cy.get('.sidebar-item-container[item-title="Test Private Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
+ cy.get('.sidebar-item-container[item-name="Test Private Page"]');
cy.wait(300);
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
- cy.get('.sidebar-item-container[item-title="Test Private Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
-
- cy.wait("@new_page");
- });
-
- it("Create Child Page", () => {
- cy.intercept({
- method: "POST",
- url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
- }).as("new_page");
-
- cy.get(".codex-editor__redactor .ce-block");
- cy.get(".btn-new-workspace").click();
- cy.fill_field("title", "Test Child Page", "Data");
- cy.fill_field("parent", "Test Private Page", "Select");
- cy.fill_field("type", "Workspace", "Select");
- cy.get_open_dialog().find(".modal-header").click();
- cy.wait(300);
- cy.get_open_dialog().find(".btn-primary").click();
-
- // check if sidebar item is added in pubic section
- cy.get('.sidebar-item-container[item-title="Test Child Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
- cy.wait(300);
- cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
- cy.wait(300);
- cy.get('.sidebar-item-container[item-title="Test Child Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
+ cy.get('.sidebar-item-container[item-name="Test Private Page"]');
cy.wait("@new_page");
});
it("Add New Block", () => {
- cy.get('.sidebar-item-container[item-title="Test Private Page"]').as("sidebar-item");
-
- cy.get("@sidebar-item").find(".standard-sidebar-item").first().click({ force: true });
-
cy.get(".btn-edit-workspace").click({ force: true });
cy.get(".ce-block").click().type("{enter}");
diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js
index 396e80d16b..05e03903ae 100644
--- a/cypress/integration/workspace_blocks.js
+++ b/cypress/integration/workspace_blocks.js
@@ -1,7 +1,7 @@
context("Workspace Blocks", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
return cy
.window()
.its("frappe")
@@ -17,7 +17,7 @@ context("Workspace Blocks", () => {
url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
}).as("new_page");
- cy.visit("/app/website");
+ cy.visit("/desk/website");
cy.get(".codex-editor__redactor .ce-block");
cy.get(".btn-new-workspace").click();
cy.fill_field("title", "Test Block Page", "Data");
@@ -26,19 +26,11 @@ context("Workspace Blocks", () => {
cy.get_open_dialog().find(".btn-primary").click();
// check if sidebar item is added in private section
- cy.get('.sidebar-item-container[item-title="Test Block Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
+ cy.get('.sidebar-item-container[item-name="Test Block Page"]');
cy.wait(300);
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
- cy.get('.sidebar-item-container[item-title="Test Block Page"]').should(
- "have.attr",
- "item-public",
- "0"
- );
+ cy.get('.sidebar-item-container[item-name="Test Block Page"]');
cy.wait("@new_page");
});
diff --git a/frappe/apps.py b/frappe/apps.py
index 08a68ba3de..69da6b3783 100644
--- a/frappe/apps.py
+++ b/frappe/apps.py
@@ -81,7 +81,7 @@ def get_default_path():
if len(_apps) == 1:
return _apps[0].get("route") or "/apps"
elif is_desk_apps(_apps):
- return "/app"
+ return "/desk"
return "/apps"
diff --git a/frappe/auth.py b/frappe/auth.py
index e631fa0c27..b88f2ff88f 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -201,7 +201,7 @@ class LoginManager:
frappe.local.cookie_manager.set_cookie("system_user", "yes", deduplicate=True)
if not resume:
frappe.local.response["message"] = "Logged In"
- frappe.local.response["home_page"] = get_default_path() or "/app"
+ frappe.local.response["home_page"] = get_default_path() or "/desk"
if not resume:
frappe.response["full_name"] = self.full_name
diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json
deleted file mode 100644
index 503740b5ce..0000000000
--- a/frappe/automation/workspace/tools/tools.json
+++ /dev/null
@@ -1,345 +0,0 @@
-{
- "charts": [],
- "content": "[{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"EgtURZsoiF\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"wE9n7TIrAc\",\"type\":\"card\",\"data\":{\"card_name\":\"Alerts and Notifications\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"3imoh2oqsJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
- "creation": "2020-03-02 14:53:24.980279",
- "custom_blocks": [],
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "tool",
- "idx": 0,
- "is_hidden": 0,
- "label": "Tools",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Automation",
- "link_count": 3,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Assignment Rule",
- "link_count": 0,
- "link_to": "Assignment Rule",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Milestone",
- "link_count": 0,
- "link_to": "Milestone",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Auto Repeat",
- "link_count": 0,
- "link_to": "Auto Repeat",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Tools",
- "link_count": 4,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "To Do",
- "link_count": 0,
- "link_to": "ToDo",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Calendar",
- "link_count": 0,
- "link_to": "Event",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Note",
- "link_count": 0,
- "link_to": "Note",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Files",
- "link_count": 0,
- "link_to": "File",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Printing",
- "link_count": 4,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format Builder",
- "link_count": 0,
- "link_to": "print-format-builder",
- "link_type": "Page",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format Builder (New)",
- "link_count": 0,
- "link_to": "print-format-builder-beta",
- "link_type": "Page",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Settings",
- "link_count": 0,
- "link_to": "Print Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Heading",
- "link_count": 0,
- "link_to": "Print Heading",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Alerts and Notifications",
- "link_count": 3,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Notification",
- "link_count": 0,
- "link_to": "Notification",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Auto Email Report",
- "link_count": 0,
- "link_to": "Auto Email Report",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Notification Settings",
- "link_count": 0,
- "link_to": "Notification Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "description": "Manage your data",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Data",
- "link_count": 5,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Import Data",
- "link_count": 0,
- "link_to": "Data Import",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Export Data",
- "link_count": 0,
- "link_to": "Data Export",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Bulk Update",
- "link_count": 0,
- "link_to": "Bulk Update",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Download Backups",
- "link_count": 0,
- "link_to": "backups",
- "link_type": "Page",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Deleted Documents",
- "link_count": 0,
- "link_to": "Deleted Document",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email",
- "link_count": 4,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Account",
- "link_count": 0,
- "link_to": "Email Account",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Domain",
- "link_count": 0,
- "link_to": "Email Domain",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Template",
- "link_count": 0,
- "link_to": "Email Template",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Group",
- "link_count": 0,
- "link_to": "Email Group",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2025-06-27 11:39:44.392114",
- "modified_by": "Administrator",
- "module": "Automation",
- "name": "Tools",
- "number_cards": [],
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "quick_lists": [],
- "restrict_to_domain": "",
- "roles": [],
- "sequence_id": 1.0,
- "shortcuts": [
- {
- "color": "Grey",
- "doc_view": "List",
- "label": "Import Data",
- "link_to": "Data Import",
- "type": "DocType"
- },
- {
- "doc_view": "",
- "label": "ToDo",
- "link_to": "ToDo",
- "stats_filter": "[[\"ToDo\",\"status\",\"=\",\"Open\",false]]",
- "type": "DocType"
- },
- {
- "label": "File",
- "link_to": "File",
- "type": "DocType"
- },
- {
- "label": "Assignment Rule",
- "link_to": "Assignment Rule",
- "type": "DocType"
- }
- ],
- "title": "Tools"
-}
\ No newline at end of file
diff --git a/frappe/boot.py b/frappe/boot.py
index 6d8781c550..d3c4716fef 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -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,13 @@ 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.show_app_icons_as_folder = frappe.db.get_single_value(
+ "Desktop Settings", "show_app_icons_as_folder"
+ )
+ bootinfo.workspace_sidebar_item = get_sidebar_items()
+ 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 = []
@@ -196,7 +202,7 @@ def load_desktop_data(bootinfo):
frappe.get_hooks("app_home", app_name=app_name)
and frappe.get_hooks("app_home", app_name=app_name)[0]
)
- or (workspaces and "/app/" + frappe.utils.slug(workspaces[0]))
+ or (workspaces and "/desk/" + frappe.utils.slug(workspaces[0]))
or "",
app_logo_url=app_info.get("logo")
or frappe.get_hooks("app_logo_url", app_name=app_name)
@@ -360,7 +366,7 @@ def add_home_page(bootinfo, docs):
bootinfo["home_page"] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError):
frappe.clear_last_message()
- bootinfo["home_page"] = "Workspaces"
+ bootinfo["home_page"] = "desktop"
def add_timezone_info(bootinfo):
@@ -518,3 +524,68 @@ def get_sentry_dsn():
return
return os.getenv("FRAPPE_SENTRY_DSN")
+
+
+def get_sidebar_items():
+ sidebars = frappe.get_all(
+ "Workspace Sidebar", fields=["name", "header_icon"], filters={"name": ["not like", "%My Workspaces%"]}
+ )
+ add_user_specific_sidebar(sidebars)
+ sidebar_items = {}
+
+ for s in sidebars:
+ w = frappe.get_doc("Workspace Sidebar", s["name"])
+ sidebar_items[s["name"].lower()] = {
+ "label": s["name"],
+ "items": [],
+ "header_icon": s["header_icon"],
+ "module": w.module,
+ }
+ 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,
+ "collapsible": si.collapsible,
+ "indent": si.indent,
+ "keep_closed": si.keep_closed,
+ "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(
+ "Report", si.link_to, ["report_type", "ref_doctype"]
+ )
+ workspace_sidebar["report"] = {
+ "report_type": report_type,
+ "ref_doctype": ref_doctype,
+ }
+
+ if (
+ "My Workspaces" in s["name"]
+ or si.type == "Section Break"
+ or w.is_item_allowed(si.link_to, si.link_type)
+ ):
+ sidebar_items[s["name"].lower()]["items"].append(workspace_sidebar)
+
+ old_name = f"my workspaces-{frappe.session.user}"
+ if old_name in sidebar_items:
+ sidebar_items["my workspaces"] = sidebar_items.pop(old_name)
+ return sidebar_items
+
+
+def add_user_specific_sidebar(sidebars):
+ try:
+ my_workspace_for_user = frappe.get_doc("Workspace Sidebar", f"My Workspaces-{frappe.session.user}")
+ sidebars.append(
+ {"name": my_workspace_for_user.name, "header_icon": my_workspace_for_user.header_icon}
+ )
+ except frappe.DoesNotExistError:
+ my_workspace = frappe.get_doc("Workspace Sidebar", "My Workspaces")
+ sidebars.append({"name": my_workspace.name, "header_icon": my_workspace.header_icon})
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index ab7b337426..edf33e1b82 100644
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -1584,6 +1584,33 @@ def bypass_patch(context: CliCtxObj, patch_name: str, yes: bool):
frappe.destroy()
+@click.command("create-desktop-icons-and-sidebar")
+@pass_context
+def create_icons_and_sidebar(context: CliCtxObj):
+ """Create desktop icons and workspace sidebars."""
+ from frappe.desk.doctype.desktop_icon.desktop_icon import create_desktop_icons
+ from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import (
+ create_workspace_sidebar_for_workspaces,
+ )
+
+ if not context.sites:
+ raise SiteNotSpecifiedError
+ for site in context.sites:
+ frappe.init(site)
+ frappe.connect()
+ try:
+ print("Creating Desktop Icons")
+ create_desktop_icons()
+ print("Creating Workspace Sidebars")
+ create_workspace_sidebar_for_workspaces()
+ # Saving it in a command need it
+ frappe.db.commit() # nosemgrep
+ except Exception as e:
+ print(f"Error creating icons {site}: {e}")
+ finally:
+ frappe.destroy()
+
+
commands = [
add_system_manager,
add_user_for_sites,
@@ -1620,4 +1647,5 @@ commands = [
trim_database,
clear_log_table,
bypass_patch,
+ create_icons_and_sidebar,
]
diff --git a/frappe/commands/testing.py b/frappe/commands/testing.py
index 2df9a07761..973ccee722 100644
--- a/frappe/commands/testing.py
+++ b/frappe/commands/testing.py
@@ -437,6 +437,7 @@ def run_parallel_tests(
@click.option("--parallel", is_flag=True, help="Run UI Test in parallel mode")
@click.option("--with-coverage", is_flag=True, help="Generate coverage report")
@click.option("--browser", default="chrome", help="Browser to run tests in")
+@click.option("--spec", help="Spec file to run")
@click.option("--ci-build-id")
@pass_context
def run_ui_tests(
@@ -448,6 +449,7 @@ def run_ui_tests(
browser="chrome",
ci_build_id=None,
cypressargs=None,
+ spec=None,
):
"Run UI tests"
site = get_site(context)
@@ -494,6 +496,8 @@ def run_ui_tests(
# run for headless mode
run_or_open = f"run --browser {browser}" if headless else "open"
+ if headless and spec:
+ run_or_open += f" --spec {spec}"
formatted_command = f"{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}"
if os.environ.get("CYPRESS_RECORD_KEY"):
diff --git a/frappe/core/dashboard_chart/background_job_activity/background_job_activity.json b/frappe/core/dashboard_chart/background_job_activity/background_job_activity.json
new file mode 100644
index 0000000000..72d45e614d
--- /dev/null
+++ b/frappe/core/dashboard_chart/background_job_activity/background_job_activity.json
@@ -0,0 +1,34 @@
+{
+ "based_on": "creation",
+ "chart_name": "Background Job Activity",
+ "chart_type": "Count",
+ "creation": "2025-09-08 11:56:13.469137",
+ "currency": "",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Scheduled Job Type",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2025-10-30 21:36:33.646973",
+ "modified": "2025-10-30 21:37:11.340673",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Background Job Activity",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "roles": [],
+ "show_values_over_chart": 0,
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 0c4704a7d1..77c31d9b09 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -375,7 +375,7 @@ class TestUser(IntegrationTestCase):
frappe.set_user("testpassword@example.com")
test_user = frappe.get_doc("User", "testpassword@example.com")
key = self.reset_password(test_user)
- self.assertEqual(update_password(new_password, key=key), "/app")
+ self.assertEqual(update_password(new_password, key=key), "/desk")
self.assertEqual(
update_password(new_password, key="wrong_key"),
"The reset password link has either been used before or is invalid",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 63e0eb96e2..9396ddbec3 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -951,7 +951,7 @@ def update_password(
frappe.db.set_value("User", user, "reset_password_key", "")
if user_doc.user_type == "System User":
- return get_default_path() or "/app"
+ return get_default_path() or "/desk"
else:
return redirect_url or get_default_path() or get_home_page()
diff --git a/frappe/core/workspace/system/system.json b/frappe/core/workspace/system/system.json
new file mode 100644
index 0000000000..3358064534
--- /dev/null
+++ b/frappe/core/workspace/system/system.json
@@ -0,0 +1,41 @@
+{
+ "app": "frappe",
+ "charts": [
+ {
+ "chart_name": "Background Job Activity",
+ "label": "Background Job Activity"
+ },
+ {
+ "chart_name": "Notifications By Type",
+ "label": "Notification Summary"
+ }
+ ],
+ "content": "[{\"id\":\"-bxX6Dwxxy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Background Job Activity\",\"col\":12}},{\"id\":\"gccD2r7Ut3\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Notification Summary\",\"col\":12}}]",
+ "creation": "2025-09-08 11:33:57.533875",
+ "custom_blocks": [],
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "for_user": "",
+ "hide_custom": 0,
+ "icon": "monitor-check",
+ "idx": 0,
+ "indicator_color": "green",
+ "is_hidden": 0,
+ "label": "System",
+ "link_type": "DocType",
+ "links": [],
+ "modified": "2025-10-30 18:22:58.416219",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "System",
+ "number_cards": [],
+ "owner": "Administrator",
+ "parent_page": "",
+ "public": 1,
+ "quick_lists": [],
+ "roles": [],
+ "sequence_id": 27.0,
+ "shortcuts": [],
+ "title": "System",
+ "type": "Workspace"
+}
diff --git a/frappe/custom/doctype/client_script/ui_test_client_script.js b/frappe/custom/doctype/client_script/ui_test_client_script.js
index 0d202d697c..f3ac811810 100644
--- a/frappe/custom/doctype/client_script/ui_test_client_script.js
+++ b/frappe/custom/doctype/client_script/ui_test_client_script.js
@@ -1,7 +1,7 @@
context("Client Script", () => {
before(() => {
cy.login();
- cy.visit("/app");
+ cy.visit("/desk");
});
it("should run form script in doctype form", () => {
@@ -16,7 +16,7 @@ context("Client Script", () => {
},
true
);
- cy.visit("/app/todo/new", {
+ cy.visit("/desk/todo/new", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
},
@@ -36,7 +36,7 @@ context("Client Script", () => {
},
true
);
- cy.visit("/app/todo", {
+ cy.visit("/desk/todo", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
},
@@ -56,7 +56,7 @@ context("Client Script", () => {
},
true
);
- cy.visit("/app/todo", {
+ cy.visit("/desk/todo", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
},
@@ -87,7 +87,7 @@ context("Client Script", () => {
},
true
);
- cy.visit("/app/todo/new", {
+ cy.visit("/desk/todo/new", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
},
diff --git a/frappe/desk/dashboard_chart/email_activity/email_activity.json b/frappe/desk/dashboard_chart/email_activity/email_activity.json
new file mode 100644
index 0000000000..4c5b142b5c
--- /dev/null
+++ b/frappe/desk/dashboard_chart/email_activity/email_activity.json
@@ -0,0 +1,34 @@
+{
+ "based_on": "creation",
+ "chart_name": "Email Activity",
+ "chart_type": "Count",
+ "creation": "2025-09-08 11:26:02.676908",
+ "currency": "",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Email Queue",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2025-09-08 11:26:32.458436",
+ "modified": "2025-09-08 11:26:42.394911",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Email Activity",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "roles": [],
+ "show_values_over_chart": 0,
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Week",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
diff --git a/frappe/desk/dashboard_chart/login/login.json b/frappe/desk/dashboard_chart/login/login.json
new file mode 100644
index 0000000000..948fa22747
--- /dev/null
+++ b/frappe/desk/dashboard_chart/login/login.json
@@ -0,0 +1,34 @@
+{
+ "based_on": "communication_date",
+ "chart_name": "Login",
+ "chart_type": "Count",
+ "creation": "2025-08-28 16:48:49.946848",
+ "currency": "INR",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Activity Log",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Activity Log\",\"status\",\"=\",\"Success\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2025-09-11 03:02:03.338607",
+ "modified": "2025-09-11 03:02:10.575994",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Login",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "roles": [],
+ "show_values_over_chart": 0,
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Week",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index ec57ef8e96..95d1c92556 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -707,3 +707,8 @@ def update_onboarding_step(name, field, value):
frappe.db.set_value("Onboarding Step", name, field, value)
capture(frappe.scrub(name), app="frappe_onboarding", properties={field: value})
+
+
+@frappe.whitelist()
+def get_installed_apps():
+ return frappe.get_installed_apps()
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.js b/frappe/desk/doctype/desktop_icon/desktop_icon.js
index 72ef1f7a12..28a2a680c5 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.js
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.js
@@ -2,5 +2,31 @@
// For license information, please see license.txt
frappe.ui.form.on("Desktop Icon", {
- refresh: function (frm) {},
+ setup: function (frm) {
+ frm.set_query("parent_icon", function () {
+ return {
+ filters: {
+ icon_type: ["in", ["Folder", "App"]],
+ },
+ };
+ });
+ },
+ refresh: function (frm) {
+ if (frm.doc.link_to && frm.doc.link_type) {
+ frm.add_custom_button(
+ __("Workspace Sidebar"),
+ function () {
+ frappe.new_doc("Workspace Sidebar", {}, (doc) => {
+ doc.title = frm.doc.label;
+ doc.header_icon = frm.doc.icon;
+ let sidebar_item = frappe.model.add_child(doc, "items");
+ sidebar_item.label = frm.doc.link_to;
+ sidebar_item.link_to = frm.doc.link_to;
+ sidebar_item.link_type = frm.doc.link_type;
+ });
+ },
+ __("Create")
+ );
+ }
+ },
});
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json
index 1e42a6d468..29ddf8aeb0 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.json
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json
@@ -1,41 +1,34 @@
{
"actions": [],
+ "allow_rename": 1,
+ "autoname": "field:label",
"creation": "2016-02-22 03:47:45.387068",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "module_name",
"label",
"standard",
- "custom",
+ "icon_type",
+ "link_type",
+ "link_to",
+ "parent_icon",
+ "sidebar",
"column_break_3",
"app",
- "description",
- "category",
- "hidden",
- "blocked",
- "force_show",
- "section_break_7",
- "type",
- "_doctype",
- "_report",
- "link",
- "column_break_10",
- "color",
"icon",
- "reverse",
- "idx"
+ "logo_url",
+ "idx",
+ "link",
+ "hidden",
+ "roles_tab",
+ "roles"
],
"fields": [
- {
- "fieldname": "module_name",
- "fieldtype": "Data",
- "label": "Module Name"
- },
{
"fieldname": "label",
"fieldtype": "Data",
- "label": "Label"
+ "label": "Label",
+ "unique": 1
},
{
"default": "0",
@@ -44,32 +37,54 @@
"in_list_view": 1,
"label": "Standard"
},
- {
- "default": "0",
- "fieldname": "custom",
- "fieldtype": "Check",
- "label": "Custom",
- "read_only": 1
- },
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
- "fieldname": "app",
- "fieldtype": "Data",
- "label": "App",
- "read_only": 1
- },
- {
- "fieldname": "description",
+ "fieldname": "link",
"fieldtype": "Small Text",
- "label": "Description"
+ "label": "Link"
},
{
- "fieldname": "category",
+ "fieldname": "icon",
+ "fieldtype": "Icon",
+ "label": "Icon"
+ },
+ {
+ "fieldname": "idx",
+ "fieldtype": "Int",
+ "label": "Idx"
+ },
+ {
+ "fieldname": "logo_url",
"fieldtype": "Data",
- "label": "Category"
+ "label": "Logo URL"
+ },
+ {
+ "fieldname": "icon_type",
+ "fieldtype": "Select",
+ "label": "Icon Type",
+ "options": "Folder\nApp\nLink"
+ },
+ {
+ "depends_on": "eval: doc.standard == 1",
+ "fieldname": "app",
+ "fieldtype": "Autocomplete",
+ "label": "App",
+ "options": "Installed Applications"
+ },
+ {
+ "fieldname": "link_to",
+ "fieldtype": "Dynamic Link",
+ "label": "Link To",
+ "options": "link_type"
+ },
+ {
+ "fieldname": "parent_icon",
+ "fieldtype": "Link",
+ "label": "Parent Icon",
+ "options": "Desktop Icon"
},
{
"default": "0",
@@ -78,79 +93,37 @@
"label": "Hidden"
},
{
- "default": "0",
- "fieldname": "blocked",
- "fieldtype": "Check",
- "label": "Blocked"
- },
- {
- "default": "0",
- "fieldname": "force_show",
- "fieldtype": "Check",
- "label": "Force Show",
- "read_only": 1
- },
- {
- "fieldname": "section_break_7",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "type",
+ "fieldname": "link_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Type",
- "options": "module\nlist\nlink\npage\nquery-report"
+ "label": "Link Type",
+ "options": "DocType\nWorkspace\nExternal"
},
{
- "fieldname": "_doctype",
+ "fieldname": "roles_tab",
+ "fieldtype": "Tab Break",
+ "label": "Roles"
+ },
+ {
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "label": "Roles",
+ "options": "Has Role"
+ },
+ {
+ "fieldname": "sidebar",
"fieldtype": "Link",
- "label": "_doctype",
- "options": "DocType"
- },
- {
- "fieldname": "_report",
- "fieldtype": "Link",
- "label": "_report",
- "options": "Report"
- },
- {
- "fieldname": "link",
- "fieldtype": "Small Text",
- "label": "Link"
- },
- {
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "color",
- "fieldtype": "Data",
- "label": "Color"
- },
- {
- "fieldname": "icon",
- "fieldtype": "Data",
- "label": "Icon"
- },
- {
- "default": "0",
- "fieldname": "reverse",
- "fieldtype": "Check",
- "label": "Reverse Icon Color"
- },
- {
- "fieldname": "idx",
- "fieldtype": "Int",
- "label": "Idx"
+ "label": "Sidebar",
+ "options": "Workspace Sidebar"
}
],
- "in_create": 1,
"links": [],
- "modified": "2024-03-23 16:02:17.847139",
+ "modified": "2025-11-15 22:10:10.463829",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desktop Icon",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -167,9 +140,10 @@
}
],
"read_only": 1,
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
- "title_field": "module_name",
+ "title_field": "label",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 77b7f01570..804df89516 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -2,12 +2,15 @@
# License: MIT. See LICENSE
import json
+import os
import random
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils.user import UserPermissions
+from frappe.modules.export_file import write_document_file
+from frappe.modules.import_file import import_file_by_path
+from frappe.modules.utils import create_directory_on_app_path, get_app_level_directory_path
class DesktopIcon(Document):
@@ -17,26 +20,22 @@ class DesktopIcon(Document):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
+ from frappe.core.doctype.has_role.has_role import HasRole
from frappe.types import DF
- _doctype: DF.Link | None
- _report: DF.Link | None
- app: DF.Data | None
- blocked: DF.Check
- category: DF.Data | None
- color: DF.Data | None
- custom: DF.Check
- description: DF.SmallText | None
- force_show: DF.Check
+ app: DF.Autocomplete | None
hidden: DF.Check
- icon: DF.Data | None
+ icon_type: DF.Literal["Folder", "App", "Link"]
idx: DF.Int
label: DF.Data | None
link: DF.SmallText | None
- module_name: DF.Data | None
- reverse: DF.Check
+ link_to: DF.DynamicLink | None
+ link_type: DF.Literal["DocType", "Workspace", "External"]
+ logo_url: DF.Data | None
+ parent_icon: DF.Link | None
+ roles: DF.Table[HasRole]
+ sidebar: DF.Link | None
standard: DF.Check
- type: DF.Literal["module", "list", "link", "page", "query-report"]
# end: auto-generated types
def validate(self):
@@ -45,10 +44,54 @@ class DesktopIcon(Document):
def on_trash(self):
clear_desktop_icons_cache()
+ if frappe.conf.developer_mode:
+ if self.standard == 1 and self.app:
+ self.delete_desktop_icon_file()
+
+ def on_update(self):
+ if frappe.conf.developer_mode:
+ if self.standard == 1 and self.app:
+ self.export_desktop_icon()
+
+ def export_desktop_icon(self):
+ folder_path = create_directory_on_app_path("desktop_icon", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json")
+ doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
+ # if self.parent_icon:
+ # print(self.parent_icon)
+ # doc_export["parent_icon"] = frappe.db.get_value("Desktop Icon", self.parent_icon, "label")
+ with open(file_path, "w+") as icon_file_doc:
+ icon_file_doc.write(frappe.as_json(doc_export) + "\n")
+
+ def delete_desktop_icon_file(self):
+ folder_path = create_directory_on_app_path("desktop_icon", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json")
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+ def is_permitted(self):
+ """Return True if `Has Role` is not set or the user is allowed."""
+ from frappe.utils import has_common
+
+ allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name})]
+
+ if not allowed:
+ return True
+
+ roles = frappe.get_roles()
+
+ if has_common(roles, allowed):
+ return True
+
+ def after_insert(self):
+ clear_desktop_icons_cache()
def after_doctype_insert():
- frappe.db.add_unique("Desktop Icon", ("module_name", "owner", "standard"))
+ pass
+
+
+# frappe.db.add_unique("Desktop Icon", ("owner", "standard"))
def get_desktop_icons(user=None):
@@ -60,23 +103,19 @@ def get_desktop_icons(user=None):
if not user_icons:
fields = [
- "module_name",
- "hidden",
"label",
"link",
- "type",
+ "link_type",
+ "icon_type",
+ "parent_icon",
"icon",
- "color",
- "description",
- "category",
- "_doctype",
- "_report",
+ "link_to",
"idx",
- "force_show",
- "reverse",
- "custom",
"standard",
- "blocked",
+ "logo_url",
+ "hidden",
+ "name",
+ "sidebar",
]
active_domains = frappe.get_active_domains()
@@ -92,6 +131,7 @@ def get_desktop_icons(user=None):
standard_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 1})
standard_map = {}
+
for icon in standard_icons:
if icon._doctype in blocked_doctypes:
icon.blocked = 1
@@ -141,12 +181,25 @@ def get_desktop_icons(user=None):
user_icons.sort(key=lambda a: a.idx)
# translate
- for d in user_icons:
- if d.label:
- d.label = _(d.label, context=d.parent)
+ # for d in user_icons:
+ # if d.label:
+ # d.label = _(d.label, context=d.parent)
+ # includes
+ permitted_icons = []
+ permitted_parent_labels = set()
+ for s in user_icons:
+ icon = frappe.get_doc("Desktop Icon", s)
+ if icon.is_permitted():
+ permitted_icons.append(s)
+
+ if not s.parent_icon:
+ permitted_parent_labels.add(s.label)
+
+ user_icons = [
+ s for s in permitted_icons if not s.parent_icon or s.parent_icon in permitted_parent_labels
+ ]
frappe.cache.hset("desktop_icons", user, user_icons)
-
return user_icons
@@ -394,7 +447,19 @@ def make_user_copy(module_name, user):
def sync_desktop_icons():
"""Sync desktop icons from all apps"""
for app in frappe.get_installed_apps():
- sync_from_app(app)
+ sync_icons(app)
+ # sync_from_app(app)
+
+
+def sync_icons(app_name):
+ icon_directory = get_app_level_directory_path("desktop_icon", app_name)
+ if os.path.exists(icon_directory):
+ icon_files = [os.path.join(icon_directory, filename) for filename in os.listdir(icon_directory)]
+ for doc_path in icon_files:
+ imported = import_file_by_path(doc_path)
+ if imported:
+ frappe.db.commit(chain=True)
+ # print(icon_directory)
def sync_from_app(app):
@@ -477,6 +542,8 @@ def get_module_icons(user=None):
def get_user_icons(user):
"""Get user icons for module setup page"""
+ from frappe.utils.user import UserPermissions
+
user_perms = UserPermissions(user)
user_perms.build_permissions()
@@ -494,10 +561,10 @@ def get_user_icons(user):
if icon.module_name == ["Help", "Settings"]:
pass
- elif icon.type == "page" and icon.link not in allowed_pages:
+ elif icon.link_type == "page" and icon.link not in allowed_pages:
add = False
- elif icon.type == "module" and icon.module_name not in user_perms.allow_modules:
+ elif icon.link_type == "module" and icon.module_name not in user_perms.allow_modules:
add = False
if add:
@@ -570,3 +637,91 @@ def hide(name, user=None):
return False
return True
+
+
+def create_desktop_icons_from_workspace():
+ workspaces = frappe.get_all(
+ "Workspace",
+ filters={"public": 1, "name": ["!=", "Welcome Workspace"]},
+ fields=["name", "icon", "app", "module"],
+ )
+
+ for w in workspaces:
+ icon = frappe.new_doc("Desktop Icon")
+ icon.link_type = "Workspace"
+ icon.label = w.name
+ icon.icon_type = "Link"
+ icon.standard = 1
+ icon.link_to = w.name
+ icon.icon = w.icon
+ if w.module:
+ app_name = frappe.db.get_value("Module Def", w.module, "app_name")
+ if app_name in frappe.get_installed_apps():
+ app_title = frappe.get_hooks("app_title", app_name=app_name)[0]
+ app_icon = frappe.db.exists("Desktop Icon", {"label": app_title, "icon_type": "App"})
+ if app_icon:
+ icon.parent_icon = app_icon
+
+ # Portal App With Desk Workspace
+ if frappe.db.get_value("Desktop Icon", app_icon, "link") and not frappe.db.get_value(
+ "Desktop Icon", app_icon, "link"
+ ).startswith("/app"):
+ icon.hidden = 1
+ icon.parent_icon = None
+
+ # If Desk App has one workspace with the same name
+ if icon.label == app_title and (
+ app_icon and frappe.db.get_value("Desktop Icon", app_icon, "link").startswith("/app")
+ ):
+ icon.hidden = 1
+ icon.parent_icon = None
+
+ try:
+ if not frappe.db.exists(
+ "Desktop Icon", [{"label": icon.label, "icon_type": icon.icon_type}]
+ ):
+ icon.insert(ignore_if_duplicate=True)
+ except Exception as e:
+ frappe.error_log(title="Creation of Desktop Icon Failed", message=e)
+
+
+def generate_color():
+ colors = ["orange", "pink", "blue", "green", "dark", "red", "yellow", "purple", "gray"]
+ return random.choice(colors)
+
+
+def create_desktop_icons_from_installed_apps():
+ apps = frappe.get_installed_apps()
+ index = 0
+ for a in apps:
+ app_title = frappe.get_hooks("app_title", app_name=a)[0]
+ app_details = frappe.get_hooks("add_to_apps_screen", app_name=a)
+ if len(app_details) != 0:
+ icon = frappe.new_doc("Desktop Icon")
+ icon.label = app_title
+ icon.link_type = "External"
+ icon.standard = 1
+ icon.idx = index
+ icon.icon_type = "App"
+ icon.link = app_details[0]["route"]
+ icon.logo_url = app_details[0]["logo"]
+ if not frappe.db.exists("Desktop Icon", [{"label": icon.label, "icon_type": icon.icon_type}]):
+ icon.save()
+ index += 1
+
+
+@frappe.whitelist()
+def set_sequence(desktop_icons):
+ cnt = 1
+ for item in json.loads(desktop_icons):
+ frappe.db.set_value("Workspace", item.get("name"), "sequence_id", cnt)
+ frappe.db.set_value("Workspace", item.get("name"), "parent_page", item.get("parent") or "")
+ cnt += 1
+
+ frappe.clear_cache()
+ frappe.toast(frappe._("Updated"))
+
+
+def create_desktop_icons():
+ create_desktop_icons_from_installed_apps()
+ create_desktop_icons_from_workspace()
diff --git a/frappe/desk/doctype/desktop_icon/test_desktop_icon.py b/frappe/desk/doctype/desktop_icon/test_desktop_icon.py
new file mode 100644
index 0000000000..d83eb7c9ed
--- /dev/null
+++ b/frappe/desk/doctype/desktop_icon/test_desktop_icon.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 IntegrationTestDesktopIcon(IntegrationTestCase):
+ """
+ Integration tests for DesktopIcon.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/frappe/desk/doctype/desktop_settings/__init__.py b/frappe/desk/doctype/desktop_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/desktop_settings/desktop_settings.js b/frappe/desk/doctype/desktop_settings/desktop_settings.js
new file mode 100644
index 0000000000..8824345717
--- /dev/null
+++ b/frappe/desk/doctype/desktop_settings/desktop_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Desktop Settings", {
+ refresh(frm) {
+ frm.add_custom_button(__("Visit Desktop"), () => frappe.set_route("desktop"));
+ },
+});
diff --git a/frappe/desk/doctype/desktop_settings/desktop_settings.json b/frappe/desk/doctype/desktop_settings/desktop_settings.json
new file mode 100644
index 0000000000..006f17018f
--- /dev/null
+++ b/frappe/desk/doctype/desktop_settings/desktop_settings.json
@@ -0,0 +1,58 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-09-07 17:00:48.624209",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "icon_style",
+ "navbar_style",
+ "show_app_icons_as_folder"
+ ],
+ "fields": [
+ {
+ "default": "Subtle Reverse",
+ "fieldname": "icon_style",
+ "fieldtype": "Select",
+ "label": "Icon Style",
+ "options": "Monochrome\nSubtle\nSubtle Reverse\nSubtle Reverse w Opacity"
+ },
+ {
+ "fieldname": "navbar_style",
+ "fieldtype": "Select",
+ "label": "Navbar Style",
+ "options": "Awesomebar\nmacOS Launchpad\nBrand Logo\nBrand Logo with Search\nTimeless Launchpad\nApps with Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_app_icons_as_folder",
+ "fieldtype": "Check",
+ "label": "Show App Icons As Folder"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2025-11-15 11:38:34.968344",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Desktop Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/desk/doctype/desktop_settings/desktop_settings.py b/frappe/desk/doctype/desktop_settings/desktop_settings.py
new file mode 100644
index 0000000000..920fc06129
--- /dev/null
+++ b/frappe/desk/doctype/desktop_settings/desktop_settings.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class DesktopSettings(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
+
+ icon_style: DF.Literal["Monochrome", "Subtle", "Subtle Reverse", "Subtle Reverse w Opacity"]
+ navbar_style: DF.Literal[
+ "Awesomebar",
+ "macOS Launchpad",
+ "Brand Logo",
+ "Brand Logo with Search",
+ "Timeless Launchpad",
+ "Apps with Search",
+ ]
+ show_app_icons_as_folder: DF.Check
+ # end: auto-generated types
+
+ pass
diff --git a/frappe/desk/doctype/desktop_settings/test_desktop_settings.py b/frappe/desk/doctype/desktop_settings/test_desktop_settings.py
new file mode 100644
index 0000000000..384ef8c5eb
--- /dev/null
+++ b/frappe/desk/doctype/desktop_settings/test_desktop_settings.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 IntegrationTestDesktopSettings(IntegrationTestCase):
+ """
+ Integration tests for DesktopSettings.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/frappe/desk/doctype/sidebar_item_group/__init__.py b/frappe/desk/doctype/sidebar_item_group/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.js b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.js
new file mode 100644
index 0000000000..bfef50150e
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Sidebar Item Group", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.json b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.json
new file mode 100644
index 0000000000..0134d09eaa
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.json
@@ -0,0 +1,62 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:sidebar",
+ "creation": "2025-11-10 12:49:52.421973",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sidebar",
+ "links",
+ "app"
+ ],
+ "fields": [
+ {
+ "fieldname": "sidebar",
+ "fieldtype": "Link",
+ "label": "Sidebar",
+ "options": "Workspace Sidebar",
+ "unique": 1
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "Sidebar Item Group Link"
+ },
+ {
+ "fieldname": "app",
+ "fieldtype": "Autocomplete",
+ "label": "App",
+ "options": "Installed Applications"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-11-13 10:03:42.852599",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Sidebar Item Group",
+ "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/sidebar_item_group/sidebar_item_group.py b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py
new file mode 100644
index 0000000000..7b72e2139f
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import os
+
+import frappe
+from frappe.boot import get_allowed_pages, get_allowed_reports
+from frappe.model.document import Document
+from frappe.modules.utils import create_directory_on_app_path, get_app_level_directory_path
+
+
+class SidebarItemGroup(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.sidebar_item_group_link.sidebar_item_group_link import SidebarItemGroupLink
+ from frappe.types import DF
+
+ app: DF.Autocomplete | None
+ links: DF.Table[SidebarItemGroupLink]
+ sidebar: DF.Link | None
+ # end: auto-generated types
+
+ def on_update(self):
+ if frappe.conf.developer_mode:
+ if self.app:
+ self.export_sidebar_item_group()
+
+ def export_sidebar_item_group(self):
+ folder_path = create_directory_on_app_path("sidebar_item_group", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.name)}.json")
+ doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
+ with open(file_path, "w+") as doc_file:
+ doc_file.write(frappe.as_json(doc_export) + "\n")
+
+ def on_trash(self):
+ if frappe.conf.developer_mode and self.app:
+ self.delete_file()
+
+ def delete_file(self):
+ folder_path = get_app_level_directory_path("sidebar_item_group", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.name)}.json")
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+
+@frappe.whitelist()
+def get_reports(module_name=None):
+ reports_info = []
+ if module_name:
+ sidebar_group = frappe.get_doc("Sidebar Item Group", module_name)
+ for report_links in sidebar_group.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/desk/doctype/sidebar_item_group/test_sidebar_item_group.py b/frappe/desk/doctype/sidebar_item_group/test_sidebar_item_group.py
new file mode 100644
index 0000000000..cb53c4ec6c
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group/test_sidebar_item_group.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 IntegrationTestSidebarItemGroup(IntegrationTestCase):
+ """
+ Integration tests for SidebarItemGroup.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/frappe/desk/doctype/sidebar_item_group_link/__init__.py b/frappe/desk/doctype/sidebar_item_group_link/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/sidebar_item_group_link/sidebar_item_group_link.json b/frappe/desk/doctype/sidebar_item_group_link/sidebar_item_group_link.json
new file mode 100644
index 0000000000..1afc714ba3
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group_link/sidebar_item_group_link.json
@@ -0,0 +1,35 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-11-13 10:02:13.869571",
+ "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-13 10:03:51.263771",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Sidebar Item Group 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/sidebar_item_group_link/sidebar_item_group_link.py b/frappe/desk/doctype/sidebar_item_group_link/sidebar_item_group_link.py
new file mode 100644
index 0000000000..ba84574a08
--- /dev/null
+++ b/frappe/desk/doctype/sidebar_item_group_link/sidebar_item_group_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 SidebarItemGroupLink(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/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 0a11f1ac84..d534041493 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -6,7 +6,9 @@ from json import loads
import frappe
from frappe import _
+from frappe.boot import get_sidebar_items
from frappe.desk.desktop import get_workspace_sidebar_items, save_new_widget
+from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import add_to_my_workspace
from frappe.desk.utils import validate_route_conflict
from frappe.model.document import Document
from frappe.model.rename_doc import rename_doc
@@ -125,6 +127,14 @@ class Workspace(Document):
def on_trash(self):
if self.public and not is_workspace_manager():
frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
+ self.delete_from_my_workspaces()
+
+ def delete_from_my_workspaces(self):
+ if not self.public:
+ my_workspaces = frappe.get_doc("Workspace Sidebar", "My Workspaces")
+ for w in my_workspaces.items:
+ if self.name == w.link_to:
+ frappe.delete_doc("Workspace Sidebar Item", w.name)
def after_delete(self):
if disable_saving_as_public():
@@ -294,7 +304,10 @@ def new_page(new_page):
doc.sequence_id = last_sequence_id(doc) + 1
doc.save(ignore_permissions=True)
- return get_workspace_sidebar_items()
+ # add to workspace sidebar items
+ if not doc.public:
+ add_to_my_workspace(doc)
+ return {"workspace_pages": get_workspace_sidebar_items(), "sidebar_items": get_sidebar_items()}
@frappe.whitelist()
diff --git a/frappe/desk/doctype/workspace_settings/workspace_settings.js b/frappe/desk/doctype/workspace_settings/workspace_settings.js
index 58bd6757b3..100743fbe0 100644
--- a/frappe/desk/doctype/workspace_settings/workspace_settings.js
+++ b/frappe/desk/doctype/workspace_settings/workspace_settings.js
@@ -18,9 +18,6 @@ frappe.ui.form.on("Workspace Settings", {
frm.docfields.push({
fieldtype: "Check",
fieldname: page.name,
- hidden: !frappe.boot.app_data_map[frappe.current_app].workspaces.includes(
- page.title
- ),
label: page.title + (page.parent_page ? ` (${page.parent_page})` : ""),
initial_value: workspace_visibilty[page.name] !== 0, // not set is also visible
});
diff --git a/frappe/desk/doctype/workspace_sidebar/__init__.py b/frappe/desk/doctype/workspace_sidebar/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py
new file mode 100644
index 0000000000..4482200436
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.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 IntegrationTestWorkspaceSidebar(IntegrationTestCase):
+ """
+ Integration tests for WorkspaceSidebar.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js
new file mode 100644
index 0000000000..f94ab3bb54
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2025, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Workspace Sidebar", {
+ refresh(frm) {
+ if (!frm.is_new()) {
+ frm.add_custom_button(__(`View Sidebar`), () => {
+ if (frm.doc.items[0].link_type === "DocType") {
+ frappe.set_route("List", frm.doc.items[0].link_to);
+ return;
+ } else if (frm.doc.items[0].link_type === "Workspace") {
+ frappe.set_route("Workspaces", frm.doc.items[0].link_to);
+ return;
+ }
+ });
+ }
+ },
+});
diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json
new file mode 100644
index 0000000000..a101799603
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json
@@ -0,0 +1,80 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:title",
+ "creation": "2025-08-12 12:06:45.016314",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "header_icon",
+ "app",
+ "for_user",
+ "items",
+ "module"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "unique": 1
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "Workspace Sidebar Item"
+ },
+ {
+ "fieldname": "header_icon",
+ "fieldtype": "Icon",
+ "label": "Header Icon"
+ },
+ {
+ "fieldname": "app",
+ "fieldtype": "Autocomplete",
+ "label": "App",
+ "options": "Installed Applications"
+ },
+ {
+ "fieldname": "for_user",
+ "fieldtype": "Link",
+ "label": "For User",
+ "options": "User"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Text",
+ "hidden": 1,
+ "label": "Module"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-11-17 01:17:20.583501",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Workspace Sidebar",
+ "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",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py
new file mode 100644
index 0000000000..b05827f509
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import os
+from json import JSONDecodeError, dumps, loads
+
+import click
+
+import frappe
+from frappe import _
+from frappe.boot import get_allowed_pages, get_allowed_reports
+from frappe.model.document import Document
+from frappe.modules.utils import create_directory_on_app_path
+
+
+class WorkspaceSidebar(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.workspace_sidebar_item.workspace_sidebar_item import WorkspaceSidebarItem
+ from frappe.types import DF
+
+ app: DF.Autocomplete | None
+ for_user: DF.Link | None
+ items: DF.Table[WorkspaceSidebarItem]
+ module: DF.Text | None
+ title: DF.Data | None
+ # end: auto-generated types
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not frappe.flags.in_migrate:
+ self.user = frappe.get_user()
+ self.can_read = self.get_cached("user_perm_can_read", self.get_can_read_items)
+
+ self.allowed_pages = get_allowed_pages(cache=True)
+ self.allowed_reports = get_allowed_reports(cache=True)
+ self.restricted_doctypes = frappe.cache.get_value("domain_restricted_doctypes")
+ self.restricted_pages = frappe.cache.get_value("domain_restricted_pages")
+
+ def get_can_read_items(self):
+ if not self.user.can_read:
+ self.user.build_permissions()
+
+ def before_save(self):
+ if frappe.conf.developer_mode:
+ if self.app:
+ self.export_sidebar()
+ self.set_module()
+
+ def export_sidebar(self):
+ folder_path = create_directory_on_app_path("workspace_sidebar", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.title)}.json")
+ doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
+ with open(file_path, "w+") as doc_file:
+ doc_file.write(frappe.as_json(doc_export) + "\n")
+
+ def delete_file(self):
+ folder_path = create_directory_on_app_path("workspace_sidebar", self.app)
+ file_path = os.path.join(folder_path, f"{frappe.scrub(self.title)}.json")
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+ def on_trash(self):
+ if is_workspace_manager():
+ if frappe.conf.developer_mode and self.app:
+ self.delete_file()
+ else:
+ frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
+
+ def is_item_allowed(self, name, item_type):
+ if frappe.session.user == "Administrator":
+ return True
+
+ item_type = item_type.lower()
+
+ if item_type == "doctype":
+ return (
+ name in (self.can_read or [])
+ and name in (self.restricted_doctypes or [])
+ and frappe.has_permission(name)
+ )
+ if item_type == "page":
+ return name in self.allowed_pages and name in self.restricted_pages
+ if item_type == "report":
+ return name in self.allowed_reports
+ if item_type == "help":
+ return True
+ if item_type == "dashboard":
+ return True
+ if item_type == "url":
+ return True
+
+ def get_cached(self, cache_key, fallback_fn):
+ value = frappe.cache.get_value(cache_key, user=frappe.session.user)
+ if value is not None:
+ return value
+
+ value = fallback_fn()
+
+ # Expire every six hour
+ frappe.cache.set_value(cache_key, value, frappe.session.user, 21600)
+ return value
+
+ def set_module(self):
+ if not self.module:
+ self.module = self.get_module_from_items()
+
+ def get_module_from_items(self):
+ all_modules_in_sidebars = []
+
+ for item in self.items:
+ if item.type != "Section Break" and item.type != "Sidebar Item Group" and item.link_type != "URL":
+ try:
+ all_modules_in_sidebars.append(frappe.get_doc(item.link_type, item.link_to).module)
+ except frappe.DoesNotExistError as e:
+ frappe.logger().error(e)
+ from collections import Counter
+
+ counts = Counter(all_modules_in_sidebars)
+ if counts and counts.most_common(1)[0]:
+ return counts.most_common(1)[0][0]
+
+
+def is_workspace_manager():
+ return "Workspace Manager" in frappe.get_roles()
+
+
+def create_workspace_sidebar_for_workspaces():
+ from frappe.query_builder import DocType
+
+ workspace = DocType("Workspace")
+
+ all_workspaces = (
+ frappe.qb.from_(workspace)
+ .select(workspace.name)
+ .where((workspace.public == 1) & (workspace.name != "Welcome Workspace"))
+ ).run(pluck=True)
+
+ existing_sidebars = frappe.get_all("Workspace Sidebar", pluck="title")
+ for workspace in all_workspaces:
+ if workspace not in existing_sidebars:
+ workspace_doc = frappe.get_doc("Workspace", workspace)
+ sidebar = frappe.new_doc("Workspace Sidebar")
+ sidebar.title = workspace
+ sidebar.header_icon = frappe.db.get_value("Workspace", workspace, "icon")
+ click.echo(f"Creating Sidebar Items for {workspace}")
+ shortcuts = workspace_doc.shortcuts
+
+ items = []
+ idx = 1
+ # Adding the workspace itself as home
+ workspace_sidebar_item = frappe.new_doc("Workspace Sidebar Item")
+ workspace_sidebar_item.update(
+ {"label": "Home", "link_to": workspace, "link_type": "Workspace", "type": "Link", "idx": 0}
+ )
+ items.append(workspace_sidebar_item)
+ # Process Shortcuts
+ for s in shortcuts:
+ workspace_sidebar_item = frappe.new_doc("Workspace Sidebar Item")
+ workspace_sidebar_item.update(
+ {"label": s.label, "link_to": s.link_to, "link_type": s.type, "type": "Link", "idx": idx}
+ )
+ items.append(workspace_sidebar_item)
+ idx += 1
+ try:
+ sidebar.items = items
+ sidebar.save()
+ except Exception as e:
+ frappe.log_error(title="Failed To Create Sidebar", message=e)
+
+
+@frappe.whitelist()
+def add_sidebar_items(sidebar_title, sidebar_items):
+ sidebar_items = loads(sidebar_items)
+ w = frappe.get_doc("Workspace Sidebar", sidebar_title)
+ items = []
+ current_idx = 1
+ for item in sidebar_items:
+ si = frappe.new_doc("Workspace Sidebar Item")
+ si.update(item)
+ items.append(si)
+ si.idx = current_idx
+ items.append(si)
+ current_idx += 1
+
+ nested_items = item.get("nested_items", [])
+ if nested_items:
+ for nested_item in nested_items:
+ new_nested_item = frappe.new_doc("Workspace Sidebar Item")
+ new_nested_item.update(nested_item)
+ new_nested_item.child = 1
+ new_nested_item.idx = current_idx
+ items.append(new_nested_item)
+ current_idx += 1
+
+ w.items = items
+ w.save()
+ return w
+
+
+def add_to_my_workspace(workspace):
+ try:
+ if not workspace.for_user:
+ return
+
+ sidebar_name = f"My Workspaces-{workspace.for_user}"
+ existing_sidebar = frappe.db.exists("Workspace Sidebar", sidebar_name)
+
+ if existing_sidebar:
+ private_sidebar = frappe.get_doc("Workspace Sidebar", existing_sidebar)
+ else:
+ # clone sidebar
+ base_sidebar = frappe.get_doc("Workspace Sidebar", "My Workspaces")
+ private_sidebar = frappe.copy_doc(base_sidebar)
+ private_sidebar.title = sidebar_name
+ private_sidebar.for_user = workspace.for_user
+ private_sidebar.owner = workspace.for_user
+ private_sidebar.items = []
+
+ sidebar_item = {
+ "label": workspace.title,
+ "type": "Link",
+ "link_to": f"{workspace.title}-{workspace.for_user}",
+ "link_type": "Workspace",
+ "icon": workspace.icon,
+ }
+
+ private_sidebar.append("items", sidebar_item)
+
+ if existing_sidebar:
+ private_sidebar.save()
+ else:
+ private_sidebar.insert()
+
+ except Exception as e:
+ frappe.log_error(title="Error in Adding Private Workspaces", message=e)
diff --git a/frappe/desk/doctype/workspace_sidebar_item/__init__.py b/frappe/desk/doctype/workspace_sidebar_item/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
new file mode 100644
index 0000000000..af32f74b36
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
@@ -0,0 +1,173 @@
+{
+ "actions": [],
+ "creation": "2025-08-12 12:46:41.926121",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "details_section",
+ "label",
+ "link_type",
+ "icon",
+ "column_break_krzu",
+ "type",
+ "link_to",
+ "child",
+ "url",
+ "display_section",
+ "collapsible_column",
+ "collapsible",
+ "indent",
+ "keep_closed",
+ "show_arrow",
+ "column_break_jexf",
+ "display_depends_on",
+ "section_break_whjq",
+ "filters",
+ "route_options"
+ ],
+ "fields": [
+ {
+ "default": "Link",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "options": "Link\nSection Break\nSpacer\nSidebar Item Group"
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label"
+ },
+ {
+ "default": "DocType",
+ "depends_on": "eval: doc.type == 'Link' || doc.indent == 1",
+ "fieldname": "link_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Link Type",
+ "options": "DocType\nPage\nReport\nWorkspace\nDashboard\nURL"
+ },
+ {
+ "depends_on": "eval: doc.link_type != \"URL\" && doc.type == 'Link' || doc.indent == 1",
+ "fieldname": "link_to",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Link To",
+ "options": "link_type"
+ },
+ {
+ "depends_on": "eval: doc.type == \"Link\"",
+ "fieldname": "icon",
+ "fieldtype": "Icon",
+ "in_list_view": 1,
+ "label": "Icon",
+ "options": "Emojis"
+ },
+ {
+ "default": "0",
+ "fieldname": "child",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Child Item"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.type == \"Section Break\"",
+ "fieldname": "indent",
+ "fieldtype": "Check",
+ "label": "Indent"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval: doc.type == \"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fieldname": "column_break_krzu",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.type == \"Section Break\"",
+ "fieldname": "display_section",
+ "fieldtype": "Section Break",
+ "label": "Display"
+ },
+ {
+ "depends_on": "eval: doc.type == \"Section Break\"",
+ "fieldname": "collapsible_column",
+ "fieldtype": "Column Break",
+ "label": "Collapsible"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.type == \"Section Break\"",
+ "fieldname": "keep_closed",
+ "fieldtype": "Check",
+ "label": "Keep Closed"
+ },
+ {
+ "fieldname": "column_break_jexf",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "display_depends_on",
+ "fieldtype": "Code",
+ "label": "Display Depends On (JS)",
+ "options": "JS",
+ "width": "30"
+ },
+ {
+ "depends_on": "eval: doc.link_type == \"URL\"",
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.indent == 1",
+ "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-13 10:30:13.048477",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Workspace Sidebar Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py
new file mode 100644
index 0000000000..e6e72b94dc
--- /dev/null
+++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class WorkspaceSidebarItem(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
+
+ 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
+ link_to: DF.DynamicLink | None
+ link_type: DF.Literal["DocType", "Page", "Report", "Workspace", "Dashboard", "URL"]
+ 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", "Sidebar Item Group"]
+ url: DF.Data | None
+ # end: auto-generated types
+
+ pass
diff --git a/frappe/desk/number_card/failed_login_attempts/failed_login_attempts.json b/frappe/desk/number_card/failed_login_attempts/failed_login_attempts.json
new file mode 100644
index 0000000000..0d8150bc03
--- /dev/null
+++ b/frappe/desk/number_card/failed_login_attempts/failed_login_attempts.json
@@ -0,0 +1,27 @@
+{
+ "aggregate_function_based_on": "",
+ "color": "#ff0000",
+ "creation": "2025-08-28 16:53:16.406273",
+ "currency": "",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Activity Log",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Activity Log\",\"status\",\"=\",\"Failed\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Failed Login Attempts",
+ "modified": "2025-08-31 19:21:55.040453",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Failed Login Attempts",
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "report_function": "Sum",
+ "show_full_number": 0,
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Weekly",
+ "type": "Document Type"
+}
diff --git a/frappe/desk/number_card/published_web_forms/published_web_forms.json b/frappe/desk/number_card/published_web_forms/published_web_forms.json
new file mode 100644
index 0000000000..eeabc90d34
--- /dev/null
+++ b/frappe/desk/number_card/published_web_forms/published_web_forms.json
@@ -0,0 +1,27 @@
+{
+ "aggregate_function_based_on": "",
+ "color": "#29CD42",
+ "creation": "2025-09-08 11:22:50.008022",
+ "currency": "INR",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Web Form",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Web Form\",\"published\",\"=\",1,false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Published Web Forms",
+ "modified": "2025-09-08 11:23:24.431998",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Published Web Forms",
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "report_function": "Sum",
+ "show_full_number": 0,
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
diff --git a/frappe/desk/number_card/published_web_pages/published_web_pages.json b/frappe/desk/number_card/published_web_pages/published_web_pages.json
new file mode 100644
index 0000000000..de41ff9fa6
--- /dev/null
+++ b/frappe/desk/number_card/published_web_pages/published_web_pages.json
@@ -0,0 +1,27 @@
+{
+ "aggregate_function_based_on": "",
+ "color": "#29CD42",
+ "creation": "2025-09-08 11:17:53.321489",
+ "currency": "",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Web Page",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Web Page\",\"published\",\"=\",1,false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Published Web Pages",
+ "modified": "2025-09-08 11:18:38.551335",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Published Web Pages",
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "report_function": "Sum",
+ "show_full_number": 0,
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
diff --git a/frappe/desk/page/desktop/__init__.py b/frappe/desk/page/desktop/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css
new file mode 100644
index 0000000000..727c586d04
--- /dev/null
+++ b/frappe/desk/page/desktop/desktop.css
@@ -0,0 +1,255 @@
+:root{
+ --desktop-blur: blur(10.2px);
+ --desktop-modal-width: 508px;
+ --desktop-modal-height: 448px;
+ --folder-thumbnail-icon-height: 12px;
+ --desktop-icon-dimension: 54px;
+}
+.desktop-wrapper{
+ max-width: 100%;
+ width: 100%;
+ margin-right: auto;
+ margin-left: auto;
+ padding: 0px;
+}
+
+.navbar-container{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 10px 20px 10px 20px; /* Add padding if needed */
+ box-sizing: border-box;
+ height: 52px;
+}
+
+.desktop-search-wrapper{
+ flex: 1;
+ max-width: 396px;
+ position: relative;
+}
+
+#navbar-search{
+ padding-left: 32px;
+}
+.desktop-search-icon{
+ position: absolute;
+ left: 10px;
+ top: 2px;
+}
+
+.desktop-search-icon > .icon {
+ stroke: var(--ink-gray-4);
+ stroke-width: 1px;
+}
+
+.desktop-container{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+ margin-top: 20px;
+}
+
+.icon-stroke{
+ stroke-width: 1.5px;
+}
+.icons{
+ gap: 30px;
+ display: grid;
+ justify-items: center;
+ margin-top: 50px;
+ grid-template-columns: repeat(6, 1fr);
+ grid-template-rows: repeat(3, 1fr);
+}
+.desktop-icon{
+ display: flex;
+ height: 100px;
+ width: 100px;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+.icon-container:has(.app-logo) {
+ padding: 0;
+ background-color: unset;
+}
+.icon-container img{
+ width: var(--desktop-icon-dimension);
+ height: var(--desktop-icon-dimension);
+}
+.icon-container{
+ padding: 10px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--desktop-icon-dimension);
+ height: var(--desktop-icon-dimension);
+}
+.icon-container:has(.icon){
+ background-color: var(--surface-gray-3);
+}
+.icon-container .icon{
+ width: 27px;
+ height: 27px;
+}
+.icon-container:hover{
+ transform: scale(1.05);
+ transition: transform 0.1s;
+}
+.icon-caption{
+ text-align: center;
+ text-wrap: nowrap;
+ display: flex;
+ flex-direction: column;
+}
+.icon-title{
+ font-weight: var(--weight-semibold);
+ font-size: var(--text-sm);
+}
+.icon-subtitle{
+ font-weight: var(--weight-regular);
+ font-size: var(--text-xs);
+ color: var(--ink-gray-5);
+}
+.timeless-style{
+ width: 100vw;
+ max-width: 600px;
+}
+.timeless-style input::placeholder{
+ text-align: center;
+}
+.apps-search{
+ align-items: center;
+ justify-content: space-between;
+ margin-left: 20px;
+}
+.small-margin{
+ margin-top: 30px;
+}
+.desktop-modal{
+ backdrop-filter: var(--desktop-blur);
+ display: flex !important;
+ & .modal-dialog{
+ & .modal-content {
+ top: 120px;
+ border-radius: var(--border-radius-2xl);
+ }
+ }
+}
+
+#desktop-modal{
+ transform: none;
+ transition: none;
+}
+
+.desktop-modal-body {
+ width: var(--desktop-modal-width);
+ padding: 23px 0px 23px 0px !important;
+ & .icons{
+ gap: 0px 0px;
+ }
+ .icon-container{
+ min-height: var(--desktop-icon-dimension);
+ }
+}
+.modal-heading{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--text-2xl);
+ font-weight: var(--weight-semibold);
+ color: var(--neutral-white);
+}
+.desktop-modal-heading {
+ all: unset !important;
+ position: absolute !important;
+ top: -75px !important;
+ width: 100% !important;
+ & .title-section{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ & .modal-title{
+ color: var(--neutral-white);
+ font-size: var(--text-2xl);
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+ }
+ & .modal-actions {
+ display: none !important;
+ }
+}
+.modal-body .icons{
+ margin-top: 0px;
+ place-self: start;
+ & .desktop-icon{
+ height: 126px;
+ width: 127px;
+ padding: 13px 16px 13px 16px;
+ & .icon-caption {
+ text-overflow: ellipsis;
+ text-wrap: wrap;
+ }
+ }
+}
+.desktop-context-menu{
+ position: absolute;
+}
+
+.folder-icon{
+ background-color: var(--gray-50) !important;
+ box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.14);
+ padding: 7px;
+ align-items: normal;
+
+ & .icons{
+ gap: 2.1px;
+ margin-top: 0px;
+ & .desktop-icon {
+ width: fit-content;
+ height: fit-content;
+ pointer-events: none;
+ & .icon-container{
+ height: var(--folder-thumbnail-icon-height);
+ width: var(--folder-thumbnail-icon-height);
+ padding: 0px;
+ border-radius: 2px;
+ & .icon{
+ width: 5px;
+ height: 5px;
+ }
+ & img{
+ width: 9px;
+ height: 9px;
+ }
+ }
+
+ }
+ }
+}
+.page-indicator-container{
+ display: flex;
+ justify-content: center;
+ gap: 5px;
+}
+.active-page{
+ background: black !important;
+}
+.page-indicator{
+ background: var(--gray-300);
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+
+}
+.no-apps-message{
+ grid-column: 1 / -1;
+ grid-row: 1 / -1;
+}
+.right-page-arrow, .left-page-arrow{
+ margin: 0px;
+}
\ No newline at end of file
diff --git a/frappe/desk/page/desktop/desktop.html b/frappe/desk/page/desktop/desktop.html
new file mode 100644
index 0000000000..3c56a587a7
--- /dev/null
+++ b/frappe/desk/page/desktop/desktop.html
@@ -0,0 +1,29 @@
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js
new file mode 100644
index 0000000000..40a8c0a852
--- /dev/null
+++ b/frappe/desk/page/desktop/desktop.js
@@ -0,0 +1,672 @@
+frappe.desktop_utils = {};
+$.extend(frappe.desktop_utils, {
+ modal: null,
+ modal_stack: [],
+ create_desktop_modal: function (icon, icon_title, icons_data, grid) {
+ if (!this.modal) {
+ this.modal = new DesktopModal(icon);
+ }
+ this.modal_stack.push(icon);
+ return this.modal;
+ },
+ close_desktop_modal: function () {
+ if (this.modal) {
+ this.modal.hide();
+ }
+ },
+});
+frappe.pages["desktop"].on_page_load = function (wrapper) {
+ var page = frappe.ui.make_app_page({
+ parent: wrapper,
+ title: "Desktop",
+ single_column: true,
+ hide_sidebar: true,
+ });
+ let desktop_page = new DesktopPage(page);
+ frappe.pages["desktop"].desktop_page = desktop_page;
+ // setup();
+};
+
+function get_workspaces_from_app_name(app_name) {
+ const app = frappe.boot.app_data.filter((a) => {
+ return a.app_title === app_name;
+ });
+ if (app.length > 0) return app[0].workspaces;
+}
+
+function get_route(desktop_icon) {
+ let route;
+ if (!desktop_icon) return;
+ let item = {};
+ if (desktop_icon.link_type == "External" && desktop_icon.link) {
+ route = window.location.origin + desktop_icon.link;
+ } else {
+ if (desktop_icon.link_type == "Workspace") {
+ item = {
+ type: desktop_icon.link_type,
+ link: frappe.router.slug(desktop_icon.link_to),
+ };
+ } else if (desktop_icon.link_type == "DocType" || desktop_icon.link_type == "list") {
+ item = {
+ type: desktop_icon.link_type,
+ name: desktop_icon.link_to,
+ };
+ }
+ route = frappe.utils.generate_route(item);
+ }
+
+ return route;
+}
+
+function get_desktop_icon_by_label(title, filters) {
+ if (!filters) {
+ return frappe.boot.desktop_icons.find((f) => f.label === title && f.hidden != 1);
+ } else {
+ return frappe.boot.desktop_icons.find((f) => {
+ return (
+ f.label === title &&
+ Object.keys(filters).every((key) => f[key] === filters[key]) &&
+ f.hidden != 1
+ );
+ });
+ }
+}
+
+function get_desktop_icon_by_idx(idx, parent_icon) {
+ return frappe.boot.desktop_icons.find((f) => f.idx == idx && f.parent_icon == parent_icon);
+}
+
+function save_desktop() {
+ // saving in localStorage;
+ localStorage.setItem(
+ `${frappe.session.user}:desktop`,
+ JSON.stringify(frappe.boot.desktop_icons)
+ );
+ frappe.toast("Desktop Saved");
+ frappe.pages["desktop"].desktop_page.update();
+}
+
+function reset_to_default() {
+ localStorage.setItem(`${frappe.session.user}:desktop`, null);
+}
+
+frappe.pages["desktop"].on_page_show = function () {
+ frappe.pages["desktop"].desktop_page.setup();
+};
+
+function toggle_icons(icons) {
+ icons.forEach((i) => {
+ $(i).parent().parent().show();
+ });
+}
+
+class DesktopPage {
+ constructor(page) {
+ this.page = page;
+ this.prepare();
+ this.make(page);
+ }
+ update() {
+ this.prepare();
+ this.make();
+ this.setup();
+ }
+
+ prepare() {
+ this.apps_icons = [];
+
+ const icon_map = {};
+ const all_icons = (
+ JSON.parse(localStorage.getItem(`${frappe.session.user}:desktop`)) ||
+ frappe.boot.desktop_icons
+ ).filter((icon) => {
+ if (icon.hidden != 1) {
+ icon.child_icons = [];
+ icon_map[icon.label] = icon;
+ return true;
+ }
+ return false;
+ });
+
+ all_icons.forEach((icon) => {
+ if (icon.parent_icon && icon_map[icon.parent_icon]) {
+ icon_map[icon.parent_icon].child_icons.push(icon);
+ }
+
+ if (!icon.parent_icon || !icon_map[icon.parent_icon]) {
+ this.apps_icons.push(icon);
+ }
+ });
+ }
+
+ make() {
+ this.page.page_head.hide();
+ $(this.page.body).empty();
+ $(frappe.render_template("desktop")).appendTo(this.page.body);
+ this.wrapper = this.page.body.find(".desktop-container");
+ this.icon_grid = new DesktopIconGrid({
+ wrapper: this.wrapper,
+ icons_data: this.apps_icons,
+ page_size: {
+ row: 6,
+ col: 3,
+ },
+ });
+ }
+
+ setup() {
+ this.setup_avatar();
+ this.setup_navbar();
+ this.setup_awesomebar();
+ this.handke_route_change();
+ }
+ setup_avatar() {
+ $(".desktop-avatar").html(frappe.avatar(frappe.session.user, "avatar-medium"));
+ $(".desktop-avatar").data("menu", "user-menu");
+ let menu_items = [
+ {
+ icon: "edit",
+ label: "Edit Profile",
+ url: `/update-profile/${frappe.session.user}`,
+ },
+ {
+ icon: "lock",
+ label: "Reset Password",
+ url: "/update-password",
+ },
+ {
+ icon: "rotate-ccw",
+ label: "Reset to Default",
+ onClick: function () {
+ reset_to_default();
+ window.location.reload();
+ },
+ },
+ {
+ icon: "log-out",
+ label: "Logout",
+ onClick: function () {
+ frappe.app.logout();
+ },
+ },
+ ];
+ frappe.ui.create_menu($(".desktop-avatar"), menu_items, null, true);
+ }
+ setup_navbar() {
+ $(".sticky-top > .navbar").hide();
+ }
+
+ setup_awesomebar() {
+ if (frappe.boot.desk_settings.search_bar) {
+ let awesome_bar = new frappe.search.AwesomeBar();
+ awesome_bar.setup(".desktop-search-wrapper #navbar-search");
+ }
+ frappe.ui.keys.add_shortcut({
+ shortcut: "ctrl+g",
+ action: function (e) {
+ $(".desktop-search-wrapper #navbar-search").focus();
+ e.preventDefault();
+ return false;
+ },
+ description: __("Open Awesomebar"),
+ });
+ frappe.ui.keys.add_shortcut({
+ shortcut: "ctrl+k",
+ action: function (e) {
+ $(".desktop-search-wrapper #navbar-search").focus();
+ e.preventDefault();
+ return false;
+ },
+ description: __("Open Awesomebar"),
+ });
+ }
+ handke_route_change() {
+ const me = this;
+ frappe.router.on("change", function () {
+ if (frappe.get_route()[0] == "desktop" || frappe.get_route()[0] == "")
+ me.setup_navbar();
+ else {
+ $(".navbar").show();
+ frappe.desktop_utils.close_desktop_modal();
+ }
+ });
+ }
+
+ // setup_icon_search() {
+ // let all_icons = $(".icon-title");
+ // let icons_to_show = [];
+ // $(".desktop-container .icons").append(
+ // " No apps found
"
+ // );
+ // $(".desktop-search-wrapper > #navbar-search").on("input", function (e) {
+ // let search_query = $(e.target).val().toLowerCase();
+ // console.log(search_query);
+ // icons_to_show = [];
+ // all_icons.each(function (index, element) {
+ // $(element).parent().parent().hide();
+ // let label = $(element).text().toLowerCase();
+ // if (label.includes(search_query)) {
+ // icons_to_show.push(element);
+ // }
+ // });
+
+ // if (icons_to_show.length == 0) {
+ // $(".desktop-container .icons").find(".no-apps-message").removeClass("hidden");
+ // } else {
+ // $(".desktop-container .icons").find(".no-apps-message").addClass("hidden");
+ // }
+ // toggle_icons(icons_to_show);
+ // });
+ // }
+}
+
+class DesktopIconGrid {
+ constructor(opts) {
+ $.extend(this, opts);
+ this.icons = [];
+ this.icons_html = [];
+ this.page_size = {
+ col: opts.page_size?.col || 4,
+ row: opts.page_size?.row || 3,
+ total: function () {
+ return this.col * this.row;
+ },
+ };
+ this.grids = [];
+ this.prepare();
+ this.make();
+ }
+
+ prepare() {
+ this.icons_data = this.icons_data.sort((a, b) => a.idx - b.idx);
+ this.total_pages = Math.ceil(this.icons_data.length / this.page_size.total());
+ this.icons_data_by_page = this.split_data(this.icons_data, this.page_size.total());
+ }
+ make() {
+ const me = this;
+ this.icons_container = $(``).appendTo(this.wrapper);
+ for (let i = 0; i < this.total_pages; i++) {
+ let template = ``;
+
+ if (this.row_size) {
+ template = ``;
+ }
+ this.grids.push($(template).appendTo(this.icons_container));
+ this.make_icons(this.icons_data_by_page[i], this.grids[i]);
+ if (!this.no_dragging) {
+ this.setup_reordering(this.grids[i]);
+ }
+ }
+ if (!this.in_folder && this.total_pages > 1) {
+ this.add_page_indicators();
+ this.setup_arrows();
+ this.setup_pagination();
+ this.setup_swipe_gesture();
+ } else {
+ this.grids[0] && this.grids[0].css("display", "grid");
+ }
+ }
+ setup_arrows() {
+ if (this.in_modal) {
+ const me = this;
+ this.wrapper
+ .parent()
+ .parent()
+ .parent()
+ .on("shown.bs.modal", function () {
+ me.add_arrows();
+ });
+ } else {
+ this.add_arrows(this.wrapper.find(".icons"));
+ }
+ }
+ setup_swipe_gesture() {
+ const me = this;
+ this.grids.forEach((grid) => {
+ $(grid).on("wheel", function (event) {
+ if (event.originalEvent) {
+ event = event.originalEvent; // for jQuery or wrapped events
+ }
+
+ if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
+ event.preventDefault();
+ if (event.deltaX > 0) {
+ if (me.current_page != me.total_pages - 1) me.current_page++;
+ me.change_to_page(me.current_page);
+ } else {
+ if (me.current_page != 0) me.current_page--;
+ me.change_to_page(me.current_page);
+ }
+ }
+ });
+ });
+ }
+ add_arrows(element) {
+ if (!element) element = this.wrapper;
+ const me = this;
+ let stroke_color = "black";
+ let horizontal_movement = 0;
+ if (this.in_modal) {
+ stroke_color = "white";
+ horizontal_movement = "-40px";
+ }
+ this.left_arrow = $(
+ frappe.utils.icon("chevron-left", "lg", "", "", "left-page-arrow", "", stroke_color)
+ );
+ this.right_arrow = $(
+ frappe.utils.icon("chevron-right", "lg", "", "", "right-page-arrow", "", stroke_color)
+ );
+
+ this.icons_container.before(this.left_arrow);
+ this.icons_container.after(this.right_arrow);
+
+ let wrapper_style = getComputedStyle(element.get(0));
+ let total_height = parseInt(wrapper_style.height) - 2 * parseInt(wrapper_style.paddingTop);
+
+ this.left_arrow.css("top", `${total_height / 2}px`);
+ this.right_arrow.css("top", `${total_height / 2}px`);
+ if (horizontal_movement) {
+ this.left_arrow.css("left", horizontal_movement);
+ this.right_arrow.css("right", horizontal_movement);
+ this.left_arrow.css("position", "absolute");
+ this.right_arrow.css("position", "absolute");
+ }
+ this.left_arrow.on("click", function () {
+ if (me.current_page != 0) me.current_page--;
+ me.change_to_page(me.current_page);
+ });
+ this.right_arrow.on("click", function () {
+ if (me.current_page != me.total_pages - 1) me.current_page++;
+ me.change_to_page(me.current_page);
+ });
+ }
+ add_page_indicators(tempplate) {
+ this.page_indicators = [];
+ if (this.total_pages > 1) {
+ this.pagination_indicator = $(``).appendTo(
+ this.icons_container
+ );
+ for (let i = 0; i < this.total_pages; i++) {
+ this.page_indicators.push(
+ $("").appendTo(this.pagination_indicator)
+ );
+ }
+ }
+ }
+ setup_pagination() {
+ this.current_page = this.old_index = 0;
+ this.change_to_page(this.current_page);
+ }
+ change_to_page(index) {
+ this.grids.forEach((g) => $(g).css("display", "none"));
+ this.grids[index].css("display", "grid");
+
+ if (this.page_indicators.length) {
+ this.page_indicators[this.old_index].removeClass("active-page");
+ this.page_indicators[this.current_page].addClass("active-page");
+ }
+ this.current_page = index;
+ this.old_index = index;
+ }
+
+ split_data(icons, size) {
+ const result = [];
+
+ for (let i = 0; i < icons.length; i += size) {
+ result.push(icons.slice(i, i + size));
+ }
+
+ return result;
+ }
+ make_icons(icons_data, grid) {
+ icons_data.forEach((icon) => {
+ let icon_obj = new DesktopIcon(icon, this.in_folder);
+ let icon_html = icon_obj.get_desktop_icon_html();
+ this.icons.push(icon_obj);
+ this.icons_html.push(icon_html);
+ grid.append(icon_html);
+ });
+ }
+
+ setup_reordering(grid) {
+ const me = this;
+ this.hoverTarget = null;
+ this.hoverTimer = null;
+ this.sortable = new Sortable($(grid).get(0), {
+ swapThreshold: 0.09,
+ animation: 150,
+ sort: true, // keep sorting normally
+ dragoverBubble: true,
+ group: {
+ name: "desktop",
+ put: true,
+ pull: true,
+ },
+ setData: function (/** DataTransfer */ dataTransfer, /** HTMLElement*/ dragEl) {
+ let title = $(dragEl).find(".icon-title").text();
+ let icon = me.icons.find((d) => {
+ return d.icon_title === title;
+ });
+ dataTransfer.setData("text/plain", JSON.stringify(icon.icon_data)); // `dataTransfer` object of HTML5 DragEvent
+ },
+ onEnd: function (evt) {
+ if (evt.oldIndex !== evt.newIndex) {
+ if (evt.to.parentElement == evt.from.parentElement) {
+ let reordered_icons = me.sortable.toArray();
+ let filters = {
+ parent_icon: me.parent_icon?.icon_data.label || null,
+ };
+ me.reorder_icons(reordered_icons, filters);
+ me.parent_icon?.render_folder_thumbnail();
+ } else {
+ let from = $(evt.from.parentElement);
+ let to = $(evt.to.parentElement);
+ let title = $(evt.item).find(".icon-title").text();
+ let selected_icon = get_desktop_icon_by_label(title);
+ if ($(to.get(0).parentElement)) {
+ me.reorder_icons(me.sortable.toArray());
+ me.reorder_icons(
+ frappe.pages["desktop"].desktop_page.icon_grid.sortable.toArray()
+ );
+ selected_icon.idx = evt.newIndex;
+ selected_icon.parent_icon = null;
+ }
+ }
+ } else {
+ frappe.toast("Nothing changed");
+ }
+ save_desktop();
+ },
+ });
+ }
+ reorder_icons(reordered_icons, filters) {
+ reordered_icons.forEach((d, idx) => {
+ let icon = get_desktop_icon_by_label(d, filters);
+ if (icon) {
+ icon.idx = idx;
+ }
+ });
+ }
+ add_to_main_screen(title) {
+ let icon = get_desktop_icon_by_label(title);
+ icon.parent_icon = null;
+ }
+}
+class DesktopIcon {
+ constructor(icon, in_folder) {
+ this.icon_data = icon;
+ this.icon_title = this.icon_data.label;
+ this.icon_subtitle = "";
+ this.icon_type = this.icon_data.icon_type;
+ this.in_folder = in_folder;
+ this.link_type = this.icon_data.link_type;
+ if (this.icon_type != "Folder" && !this.icon_data.sidebar) {
+ this.icon_route = get_route(this.icon_data);
+ }
+ this.icon = $(
+ frappe.render_template("desktop_icon", { icon: this.icon_data, in_folder: in_folder })
+ );
+
+ this.icon_caption_area = $(this.icon.get(0).children[1]);
+ this.child_icons = this.get_child_icons_data();
+ // this.child_icons = this.get_desktop_icon(this.icon_title).child_icons;
+ // this.child_icons_data = this.get_child_icons_data();
+ this.parent_icon = this.icon_data.icon;
+ this.setup_click();
+ this.render_folder_thumbnail();
+ this.setup_dragging();
+ this.child_icons = this.get_child_icons_data();
+ }
+
+ get_child_icons_data() {
+ return this.icon_data.child_icons.sort((a, b) => a.idx - b.idx);
+ }
+ get_desktop_icon_html() {
+ return this.icon;
+ }
+ setup_click() {
+ const me = this;
+ if (this.child_icons.length && (this.icon_type == "App" || this.icon_type == "Folder")) {
+ $(this.icon).on("click", () => {
+ let modal = frappe.desktop_utils.create_desktop_modal(me);
+ modal.setup(me.icon_title, me.child_icons, 4);
+ modal.show();
+ });
+ if (this.icon_type == "App") {
+ $($(this.icon_caption_area).children()[1]).html(
+ `${this.child_icons.length} Workspaces`
+ );
+ }
+ } else {
+ this.icon.attr("href", this.icon_route);
+ }
+ if (this.icon_data.sidebar) {
+ const me = this;
+ this.icon.on("click", function () {
+ if (me.icon_data.sidebar == "My Workspaces") {
+ let sidebar_name = me.icon_data.sidebar.toLowerCase();
+ if (frappe.boot.workspace_sidebar_item[sidebar_name].items.length == 0) {
+ frappe.toast("No Private Workspaces for user");
+ } else {
+ let workspace_name =
+ frappe.boot.workspace_sidebar_item[sidebar_name].items[0]["link_to"];
+ frappe.set_route("Workspaces", "private", workspace_name);
+ }
+ }
+ });
+ }
+ }
+
+ render_folder_thumbnail() {
+ let condition =
+ frappe.boot.show_app_icons_as_folder &&
+ this.icon_type == "App" &&
+ this.child_icons.length > 0;
+ if (this.icon_type == "Folder" || condition) {
+ if (!this.folder_wrapper) this.folder_wrapper = this.icon.find(".icon-container");
+ this.folder_wrapper.html("");
+ this.folder_grid = new DesktopIconGrid({
+ wrapper: this.folder_wrapper,
+ icons_data: this.child_icons,
+ row_size: 3,
+ page_size: {
+ row: 3,
+ col: 3,
+ },
+ in_folder: true,
+ in_modal: false,
+ no_dragging: true,
+ });
+ if (this.icon_type == "App") {
+ this.folder_wrapper.addClass("folder-icon");
+ }
+ }
+ }
+
+ setup_dragging() {
+ this.icon.on("drag", (event) => {
+ const mouse_x = event.clientX;
+ const mouse_y = event.clientY;
+ if (frappe.desktop_utils.modal) {
+ let modal = frappe.desktop_utils.modal.modal
+ .find(".modal-content")
+ .get(0)
+ .getBoundingClientRect();
+ if (
+ mouse_x > modal.right ||
+ mouse_x < modal.left ||
+ mouse_y > modal.bottom ||
+ mouse_y < modal.top
+ ) {
+ frappe.desktop_utils.close_desktop_modal();
+ }
+ }
+ });
+ }
+}
+
+class DesktopModal {
+ constructor(icon) {
+ this.parent_icon_obj = icon;
+ }
+ setup(icon_title, child_icons_data, grid_row_size) {
+ const me = this;
+ this.make_modal(icon_title);
+ this.child_icon_grid = new DesktopIconGrid({
+ wrapper: this.$child_icons_wrapper,
+ icons_data: child_icons_data,
+ row_size: grid_row_size,
+ in_folder: false,
+ in_modal: true,
+ parent_icon: this.parent_icon_obj,
+ });
+
+ this.modal.on("hidden.bs.modal", function () {
+ me.modal.remove();
+ frappe.desktop_utils.modal = null;
+ frappe.desktop_utils.modal_stack = [];
+ });
+ }
+ make_modal(icon_title) {
+ if ($(".desktop-modal").length == 0) {
+ this.modal = new frappe.get_modal(icon_title, "");
+ this.modal.find(".modal-header").addClass("desktop-modal-heading");
+ this.modal.addClass("desktop-modal");
+ this.modal.find(".modal-dialog").attr("id", "desktop-modal");
+ this.modal.find(".modal-body").addClass("desktop-modal-body");
+ this.$child_icons_wrapper = this.modal.find(".desktop-modal-body");
+ } else {
+ this.modal.find(".modal-title").text(icon_title);
+ $(this.modal.find(".modal-body")).empty();
+ if (frappe.desktop_utils.modal_stack.length == 1) {
+ this.title_section.find(".icon").remove();
+ } else {
+ this.add_back_button();
+ }
+ }
+ }
+ add_back_button() {
+ const me = this;
+ this.title_section = this.modal.find(".title-section").find(".modal-title");
+ $(this.title_section).prepend(
+ frappe.utils.icon("chevron-left", "md", "", "", "", "", "white")
+ );
+ $(this.title_section)
+ .find(".icon")
+ .on("click", function () {
+ const [prev] = frappe.desktop_utils.modal_stack.splice(-1, 1);
+ let icon =
+ frappe.desktop_utils.modal_stack[frappe.desktop_utils.modal_stack.length - 1];
+ if (icon) {
+ me.setup(icon.icon_title, icon.child_icons, 4);
+ me.show();
+ }
+ });
+ }
+ show() {
+ this.modal.modal("show");
+ }
+ hide() {
+ this.modal.modal("hide");
+ }
+}
diff --git a/frappe/desk/page/desktop/desktop.json b/frappe/desk/page/desktop/desktop.json
new file mode 100644
index 0000000000..ea61c81769
--- /dev/null
+++ b/frappe/desk/page/desktop/desktop.json
@@ -0,0 +1,24 @@
+{
+ "content": null,
+ "creation": "2025-08-18 16:17:07.259326",
+ "docstatus": 0,
+ "doctype": "Page",
+ "icon": "",
+ "idx": 0,
+ "modified": "2025-10-08 13:31:06.525425",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "desktop",
+ "owner": "Administrator",
+ "page_name": "desktop",
+ "roles": [
+ {
+ "role": "All"
+ }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 1,
+ "title": "Desktop"
+}
diff --git a/frappe/desk/page/desktop/desktop.py b/frappe/desk/page/desktop/desktop.py
new file mode 100644
index 0000000000..b73f1bfa37
--- /dev/null
+++ b/frappe/desk/page/desktop/desktop.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
+
+
+def get_context(context):
+ if frappe.session.user == "Guest":
+ frappe.local.flags.redirect_location = "/app"
+ raise frappe.Redirect
+ brand_logo = None
+ brand_logo = frappe.get_single_value("Navbar Settings", "app_logo")
+ if not brand_logo:
+ brand_logo = frappe.get_hooks("app_logo_url", app_name="frappe")[0]
+ context.brand_logo = brand_logo
+ context.desktop_icons = get_desktop_icons()
+ context.current_user = frappe.session.user
+ return context
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index 5f9a6a5283..05515f78dd 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -32,7 +32,7 @@ frappe.setup = {
frappe.pages["setup-wizard"].on_page_load = function (wrapper) {
if (frappe.boot.setup_complete) {
- window.location.href = frappe.boot.apps_data.default_path || "/app";
+ window.location.href = frappe.boot.apps_data.default_path || "/desk";
}
let requires = frappe.boot.setup_wizard_requires || [];
frappe.require(requires, function () {
@@ -219,7 +219,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
localStorage.current_route = "";
localStorage.current_app = "";
- window.location.href = current_route || frappe.boot.apps_data.default_path || "/app";
+ window.location.href = current_route || frappe.boot.apps_data.default_path || "/desk";
}, 2000);
}
diff --git a/frappe/desktop_icon/automation.json b/frappe/desktop_icon/automation.json
new file mode 100644
index 0000000000..d29b78d112
--- /dev/null
+++ b/frappe/desktop_icon/automation.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 23:58:08.187208",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "repeat-2",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Automation",
+ "link_to": "Assignment Rule",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:00:56.971828",
+ "modified_by": "Administrator",
+ "name": "Automation",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/build.json b/frappe/desktop_icon/build.json
new file mode 100644
index 0000000000..b3efab27ee
--- /dev/null
+++ b/frappe/desktop_icon/build.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 11:24:16.723769",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "hammer",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Build",
+ "link_to": "DocType",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:00:56.980768",
+ "modified_by": "Administrator",
+ "name": "Build",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/data.json b/frappe/desktop_icon/data.json
new file mode 100644
index 0000000000..2d82bbe300
--- /dev/null
+++ b/frappe/desktop_icon/data.json
@@ -0,0 +1,35 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-11 02:27:30.898706",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "table_2",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Data",
+ "link_to": "Data Import",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:46:11.428362",
+ "modified_by": "Administrator",
+ "name": "Data",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [
+ {
+ "creation": "2025-10-31 12:58:11.621125",
+ "docstatus": 0,
+ "doctype": "Has Role",
+ "idx": 1,
+ "modified": "2025-11-13 16:46:11.428362",
+ "modified_by": "Administrator",
+ "name": "q8qhb7fg13",
+ "owner": "Administrator",
+ "parent": "Data",
+ "parentfield": "roles",
+ "parenttype": "Desktop Icon",
+ "role": "Accounts User"
+ }
+ ],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/email.json b/frappe/desktop_icon/email.json
new file mode 100644
index 0000000000..da7e41b967
--- /dev/null
+++ b/frappe/desktop_icon/email.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 22:24:50.822193",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "mail",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Email",
+ "link_to": "Email Account",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:00:56.976167",
+ "modified_by": "Administrator",
+ "name": "Email",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/frappe_framework.json b/frappe/desktop_icon/frappe_framework.json
new file mode 100644
index 0000000000..a04a52e923
--- /dev/null
+++ b/frappe/desktop_icon/frappe_framework.json
@@ -0,0 +1,34 @@
+{
+ "app": "frappe",
+ "creation": "2025-10-31 00:06:17.823417",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon_type": "App",
+ "idx": 0,
+ "label": "Frappe Framework",
+ "link": "/app/build",
+ "link_type": "External",
+ "logo_url": "/assets/frappe/images/frappe-framework-logo.svg",
+ "modified": "2025-11-13 16:00:56.985039",
+ "modified_by": "Administrator",
+ "name": "Frappe Framework",
+ "owner": "Administrator",
+ "roles": [
+ {
+ "creation": "2025-10-31 00:06:17.823417",
+ "docstatus": 0,
+ "doctype": "Has Role",
+ "idx": 1,
+ "modified": "2025-11-13 16:00:56.985039",
+ "modified_by": "Administrator",
+ "name": "osnc6bpj4v",
+ "owner": "Administrator",
+ "parent": "Frappe Framework",
+ "parentfield": "roles",
+ "parenttype": "Desktop Icon",
+ "role": "System Manager"
+ }
+ ],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/integrations.json b/frappe/desktop_icon/integrations.json
new file mode 100644
index 0000000000..75ad56fb0f
--- /dev/null
+++ b/frappe/desktop_icon/integrations.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 11:24:16.753521",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "integration",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Integrations",
+ "link_to": "Connected App",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:00:56.967719",
+ "modified_by": "Administrator",
+ "name": "Integrations",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/my_workspaces.json b/frappe/desktop_icon/my_workspaces.json
new file mode 100644
index 0000000000..66dcb8b22a
--- /dev/null
+++ b/frappe/desktop_icon/my_workspaces.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-16 01:29:41.871347",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "user-round",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "My Workspaces",
+ "link_to": "",
+ "link_type": "DocType",
+ "modified": "2025-11-16 22:13:41.212628",
+ "modified_by": "Administrator",
+ "name": "My Workspaces",
+ "owner": "Administrator",
+ "roles": [],
+ "sidebar": "My Workspaces",
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/printing.json b/frappe/desktop_icon/printing.json
new file mode 100644
index 0000000000..d828526610
--- /dev/null
+++ b/frappe/desktop_icon/printing.json
@@ -0,0 +1,35 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-06 10:43:13.440877",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "printer",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Printing",
+ "link_to": "Print Format",
+ "link_type": "DocType",
+ "modified": "2025-11-13 16:39:42.995097",
+ "modified_by": "Administrator",
+ "name": "Printing",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [
+ {
+ "creation": "2025-10-31 13:08:16.747930",
+ "docstatus": 0,
+ "doctype": "Has Role",
+ "idx": 1,
+ "modified": "2025-11-13 16:39:42.995097",
+ "modified_by": "Administrator",
+ "name": "3sjtumcivg",
+ "owner": "Administrator",
+ "parent": "Printing",
+ "parentfield": "roles",
+ "parenttype": "Desktop Icon",
+ "role": "System Manager"
+ }
+ ],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/users.json b/frappe/desktop_icon/users.json
new file mode 100644
index 0000000000..932ec58a31
--- /dev/null
+++ b/frappe/desktop_icon/users.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 11:24:16.756563",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "users",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Users",
+ "link_to": "Users",
+ "link_type": "Workspace",
+ "modified": "2025-11-13 16:00:56.962638",
+ "modified_by": "Administrator",
+ "name": "Users",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/desktop_icon/website.json b/frappe/desktop_icon/website.json
new file mode 100644
index 0000000000..8846f81051
--- /dev/null
+++ b/frappe/desktop_icon/website.json
@@ -0,0 +1,20 @@
+{
+ "app": "frappe",
+ "creation": "2025-11-05 11:24:16.762628",
+ "docstatus": 0,
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "website",
+ "icon_type": "Link",
+ "idx": 0,
+ "label": "Website",
+ "link_to": "Website",
+ "link_type": "Workspace",
+ "modified": "2025-11-13 16:00:56.957106",
+ "modified_by": "Administrator",
+ "name": "Website",
+ "owner": "Administrator",
+ "parent_icon": "Frappe Framework",
+ "roles": [],
+ "standard": 1
+}
diff --git a/frappe/hooks.py b/frappe/hooks.py
index b8c8901320..b8c3932fdb 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -16,6 +16,8 @@ app_email = "developers@frappe.io"
before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"
+after_app_install = "frappe.utils.install.auto_generate_icons_and_sidebar"
+
page_js = {"setup-wizard": "public/js/frappe/setup_wizard.js"}
# website
@@ -37,6 +39,7 @@ app_include_css = [
app_include_icons = [
"/assets/frappe/icons/timeless/icons.svg",
"/assets/frappe/icons/espresso/icons.svg",
+ "/assets/frappe/icons/icons.svg",
]
doctype_js = {
@@ -56,11 +59,11 @@ email_css = ["email.bundle.css"]
website_route_rules = [
{"from_route": "/kb/", "to_route": "Help Article"},
{"from_route": "/profile", "to_route": "me"},
- {"from_route": "/app/", "to_route": "app"},
+ {"from_route": "/desk/", "to_route": "desk"},
]
website_redirects = [
- {"source": r"/desk(.*)", "target": r"/app\1"},
+ {"source": r"/app(.*)", "target": r"/desk\1"},
]
base_template = "templates/base.html"
@@ -416,6 +419,7 @@ ignore_links_on_delete = [
"Route History",
"Access Log",
"Permission Log",
+ "Desktop Icon",
]
# Request Hooks
@@ -581,3 +585,13 @@ user_invitation = {
"System Manager": [],
},
}
+
+
+add_to_apps_screen = [
+ {
+ "name": app_name,
+ "logo": app_logo_url,
+ "title": app_title,
+ "route": app_home,
+ }
+]
diff --git a/frappe/integrations/google_oauth.py b/frappe/integrations/google_oauth.py
index 448de20d03..5aab40ce0c 100644
--- a/frappe/integrations/google_oauth.py
+++ b/frappe/integrations/google_oauth.py
@@ -173,7 +173,7 @@ def callback(state: str, code: str | None = None, error: str | None = None) -> N
along with committing and redirecting us back to frappe site."""
state = json.loads(state)
- redirect = state.pop("redirect", "/app")
+ redirect = state.pop("redirect", "/desk")
success_query_param = state.pop("success_query_param", "")
failure_query_param = state.pop("failure_query_param", "")
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 72d644cd0b..fb6f449c92 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -9,9 +9,11 @@ import os
import frappe
from frappe.cache_manager import clear_controller_cache
+from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons
from frappe.model.base_document import get_controller
from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import _patch_mode
+from frappe.modules.utils import get_app_level_directory_path
from frappe.utils import update_progress_bar
IMPORTABLE_DOCTYPES = [
@@ -27,6 +29,7 @@ IMPORTABLE_DOCTYPES = [
("email", "notification"),
("printing", "print_style"),
("desk", "workspace"),
+ ("desk", "workspace_sidebar"),
("desk", "onboarding_step"),
("desk", "module_onboarding"),
("desk", "form_tour"),
@@ -39,7 +42,6 @@ IMPORTABLE_DOCTYPES = [
def sync_all(force=0, reset_permissions=False):
_patch_mode(True)
-
for app in frappe.get_installed_apps():
sync_for(app, force, reset_permissions=reset_permissions)
@@ -93,6 +95,8 @@ def sync_for(app_name, force=0, reset_permissions=False):
"workspace_number_card",
"workspace_custom_block",
"workspace",
+ "workspace_sidebar",
+ "workspace_sidebar_item",
]:
files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
@@ -105,8 +109,15 @@ def sync_for(app_name, force=0, reset_permissions=False):
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
files = get_doc_files(files=files, start_path=folder)
- l = len(files)
+ app_level_folders = ["desktop_icon", "workspace_sidebar", "sidebar_item_group"]
+ for folder_name in app_level_folders:
+ directory_path = get_app_level_directory_path(folder_name, app_name)
+ if os.path.exists(directory_path):
+ icon_files = [os.path.join(directory_path, filename) for filename in os.listdir(directory_path)]
+ for doc_path in icon_files:
+ files.append(doc_path)
+ l = len(files)
if l:
for i, doc_path in enumerate(files):
imported = import_file_by_path(
diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py
index 90cf1380b3..924b874be9 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -6,6 +6,7 @@ Utilities for using modules
import json
import os
+import shutil
from pathlib import Path
from textwrap import dedent, indent
from typing import TYPE_CHECKING, Union
@@ -358,3 +359,24 @@ def make_boilerplate(
custom_controller=controller_body,
)
target.write(frappe.as_unicode(controller_file_content))
+
+
+def create_directory_on_app_path(folder_name, app_name):
+ app_path = frappe.get_app_path(app_name)
+ folder_path = os.path.join(app_path, folder_name)
+
+ if not os.path.exists(folder_path):
+ frappe.create_folder(folder_path)
+
+ return folder_path
+
+
+def get_app_level_directory_path(folder_name, app_name):
+ app_path = frappe.get_app_path(app_name)
+ path = os.path.join(app_path, folder_name)
+ return path
+
+
+def delete_app_level_folder(folder_name, app_name):
+ path = get_app_level_directory_path(folder_name, app_name)
+ shutil.rmtree(path, ignore_errors=True)
diff --git a/frappe/patches.txt b/frappe/patches.txt
index b6374edd7b..3a20616fb0 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -196,6 +196,7 @@ execute:frappe.reload_doc("desk", "doctype", "Form Tour")
execute:frappe.delete_doc('Page', 'recorder', ignore_missing=True, force=True)
frappe.patches.v14_0.modify_value_column_size_for_singles
frappe.integrations.doctype.oauth_bearer_token.patches.clear_old_tokens
+execute:frappe.db.truncate("Desktop Icon")
[post_model_sync]
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
@@ -247,3 +248,5 @@ frappe.patches.v14_0.fix_user_settings_collation
execute:frappe.call("frappe.core.doctype.system_settings.system_settings.sync_system_settings")
frappe.patches.v15_0.migrate_to_utm
frappe.patches.v16_0.add_module_deprecation_warning
+frappe.patches.v16_0.auto_generate_desktop_icon_and_sidebar
+frappe.patches.v16_0.add_private_workspaces_to_sidebar
\ No newline at end of file
diff --git a/frappe/patches/v16_0/add_private_workspaces_to_sidebar.py b/frappe/patches/v16_0/add_private_workspaces_to_sidebar.py
new file mode 100644
index 0000000000..1f10e42af7
--- /dev/null
+++ b/frappe/patches/v16_0/add_private_workspaces_to_sidebar.py
@@ -0,0 +1,19 @@
+import click
+
+import frappe
+
+
+def execute():
+ from frappe.query_builder import DocType
+
+ workspace = DocType("Workspace")
+ all_workspaces = (frappe.qb.from_(workspace).select(workspace.name).where(workspace.public == 0)).run(
+ pluck=True
+ )
+ from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import add_to_my_workspace
+
+ for space in all_workspaces:
+ workspace_doc = frappe.get_doc("Workspace", space)
+ add_to_my_workspace(workspace_doc)
+ # save the sidebar items
+ frappe.db.commit() # nosemgrep
diff --git a/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py b/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py
new file mode 100644
index 0000000000..794438375a
--- /dev/null
+++ b/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py
@@ -0,0 +1,6 @@
+from frappe.utils.install import auto_generate_icons_and_sidebar
+
+
+def execute():
+ """Auto Create desktop icons and workspace sidebars."""
+ auto_generate_icons_and_sidebar()
diff --git a/frappe/public/icons/icons.svg b/frappe/public/icons/icons.svg
new file mode 100644
index 0000000000..58aef42fb3
--- /dev/null
+++ b/frappe/public/icons/icons.svg
@@ -0,0 +1,9791 @@
+
+
diff --git a/frappe/public/icons/timeless/icons.svg b/frappe/public/icons/timeless/icons.svg
index b8de8f59bc..804a77c568 100644
--- a/frappe/public/icons/timeless/icons.svg
+++ b/frappe/public/icons/timeless/icons.svg
@@ -19,6 +19,10 @@ Tip: use lucide.svg in /icons for all downloaded icons
+
+
+
+
@@ -30,19 +34,74 @@ Tip: use lucide.svg in /icons for all downloaded icons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -161,6 +220,22 @@ Tip: use lucide.svg in /icons for all downloaded icons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -184,11 +259,17 @@ Tip: use lucide.svg in /icons for all downloaded icons
-
+
+
+
+
+
+
+
@@ -329,10 +410,19 @@ Tip: use lucide.svg in /icons for all downloaded icons
+
+
+
+
+
+
+
+
+
@@ -390,6 +480,17 @@ Tip: use lucide.svg in /icons for all downloaded icons
+
+
+
+
+
+
+
+
+
+
+
@@ -428,10 +529,61 @@ Tip: use lucide.svg in /icons for all downloaded icons
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js
index cd7653fba3..91577b6bfa 100644
--- a/frappe/public/js/desk.bundle.js
+++ b/frappe/public/js/desk.bundle.js
@@ -9,10 +9,11 @@ import "./frappe/dom.js";
import "./frappe/ui/messages.js";
import "./frappe/ui/keyboard.js";
import "./frappe/ui/colors.js";
-import "./frappe/ui/sidebar.html";
-import "./frappe/ui/sidebar.js";
-import "./frappe/ui/apps_switcher.js";
-import "./frappe/ui/apps_switcher.html";
+import "./frappe/ui/sidebar/sidebar_header.js";
+import "./frappe/ui/sidebar/sidebar_header.html";
+import "./frappe/ui/sidebar/sidebar.html";
+import "./frappe/ui/sidebar/sidebar_item.html";
+import "./frappe/ui/sidebar/sidebar.js";
import "./frappe/ui/link_preview.js";
import "./frappe/request.js";
@@ -40,6 +41,7 @@ import "./frappe/ui/field_group.js";
import "./frappe/form/link_selector.js";
import "./frappe/form/multi_select_dialog.js";
import "./frappe/ui/dialog.js";
+import "./frappe/ui/menu.js";
import "./frappe/ui/capture.js";
import "./frappe/ui/app_icon.js";
import "./frappe/ui/theme_switcher.js";
@@ -108,3 +110,4 @@ import "./frappe/ui/driver.js";
import "./frappe/scanner";
import "./frappe/ui/address_autocomplete/autocomplete_dialog.js";
+import "./frappe/ui/desktop_icon.html";
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 9b5f095144..3e7bf70ccc 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -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;
diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js
index beb951a567..97844c4965 100644
--- a/frappe/public/js/frappe/form/controls/autocomplete.js
+++ b/frappe/public/js/frappe/form/controls/autocomplete.js
@@ -11,6 +11,9 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
set_options() {
if (this.df.options) {
let options = this.df.options || [];
+ if (options == "Installed Applications") {
+ this.load_installed_apps();
+ }
this.set_data(options);
}
}
@@ -241,4 +244,16 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
}
this._data = data;
}
+
+ async load_installed_apps(frm) {
+ const me = this;
+ await frappe.call({
+ method: "frappe.desk.desktop.get_installed_apps",
+ callback: function (r) {
+ if (r.message) {
+ me.set_data(r.message);
+ }
+ },
+ });
+ }
};
diff --git a/frappe/public/js/frappe/form/controls/icon.js b/frappe/public/js/frappe/form/controls/icon.js
index f3386ed813..a964153d2b 100644
--- a/frappe/public/js/frappe/form/controls/icon.js
+++ b/frappe/public/js/frappe/form/controls/icon.js
@@ -21,6 +21,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat
parent: picker_wrapper,
icon: this.get_icon(),
icons: frappe.symbols,
+ include_emoji: this.df.options == "Emojis",
});
this.$wrapper
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 48a954a637..f8d7e1f080 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -397,7 +397,7 @@ frappe.form.formatters = {
},
Icon: (value) => {
return value
- ? `
+ ? `
${frappe.utils.icon(value, "md")}
${value}
`
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 29b3f239f5..25b1087070 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -384,7 +384,6 @@ export default class GridRow {
add_column_configure_button() {
if (this.grid.df.in_place_edit && !this.frm) return;
-
if (this.configure_columns && this.frm) {
this.configure_columns_button = $(`
diff --git a/frappe/public/js/frappe/icon_picker/icon_picker.js b/frappe/public/js/frappe/icon_picker/icon_picker.js
index 8d8cccd748..0f2f5dc212 100644
--- a/frappe/public/js/frappe/icon_picker/icon_picker.js
+++ b/frappe/public/js/frappe/icon_picker/icon_picker.js
@@ -5,6 +5,7 @@ class Picker {
this.height = opts.height;
this.set_icon(opts.icon);
this.icons = opts.icons;
+ this.include_emoji = opts.include_emoji;
this.setup_picker();
}
@@ -19,7 +20,7 @@ class Picker {
${frappe.utils.icon("search", "sm")}
-
@@ -29,8 +30,106 @@ class Picker {
this.search_input = this.icon_picker_wrapper.find(".search-icons > input");
this.refresh();
this.setup_icons();
+ if (this.include_emoji) {
+ this.setup_emojis();
+ }
}
+ setup_emojis() {
+ console.log("Making emojis");
+ // setup tab
+ this.setup_tab();
+ // setup emoji container
+ this.setup_emoji_container();
+ // emojis
+ this.emoji_wrapper = this.icon_picker_wrapper.find(".emojis");
+ gemoji.forEach((emoji, i) => {
+ let $icon = $(
+ `
${gemoji[i].emoji}
`
+ );
+ this.emoji_wrapper.append($icon);
+ const set_values = () => {
+ this.set_icon(gemoji[i].emoji);
+ this.update_icon_selected();
+ };
+ $icon.on("click", () => {
+ set_values();
+ });
+ // $icon.keydown((e) => {
+ // const key_code = e.keyCode;
+ // if ([13, 32].includes(key_code)) {
+ // e.preventDefault();
+ // set_values();
+ // }
+ // });
+ });
+ this.search_input.on("input", (e) => {
+ e.preventDefault();
+ this.filter_emojis();
+ });
+ }
+ filter_emojis() {
+ let value = this.search_input.val();
+ let filtered_emoji_names = [];
+ if (value) {
+ gemoji.forEach((g) => {
+ g.tags.forEach((t) => {
+ if (t.includes(value)) {
+ filtered_emoji_names.push(g);
+ }
+ });
+ g.names.forEach((t) => {
+ if (t.includes(value)) {
+ filtered_emoji_names.push(g);
+ }
+ });
+ });
+ }
+ if (filtered_emoji_names.length == 0) {
+ this.emoji_wrapper.find(".emoji-wrapper").removeClass("hidden");
+ } else {
+ this.emoji_wrapper.find(".emoji-wrapper").addClass("hidden");
+ filtered_emoji_names.forEach((g) => {
+ this.emoji_wrapper.find(`.emoji-wrapper[id*='${g.emoji}']`).removeClass("hidden");
+ });
+ }
+ }
+ setup_emoji_container() {
+ this.icon_picker_wrapper.find(".icon-section")
+ .after(`
`);
+ }
+ setup_tab() {
+ this.icon_picker_wrapper.find(".search-icons").after(`
`);
+ let icon_types = ["icon", "emoji"];
+ const me = this;
+
+ this.icon_picker_wrapper.find(".nav-item").on("click", function (e) {
+ let container_name = $(this).text().trim().toLowerCase();
+
+ icon_types.forEach((type) => {
+ if (type === container_name) {
+ me.icon_picker_wrapper.find(`.${type}-section`).removeClass("hidden");
+ } else {
+ me.icon_picker_wrapper.find(`.${type}-section`).addClass("hidden");
+ }
+ });
+ });
+ }
setup_icons() {
this.icons.forEach((icon) => {
let $icon = $(
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index a96219d632..73f6c06b88 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1234,7 +1234,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return this.settings.get_form_link(doc);
}
- return `/app/${encodeURIComponent(
+ return `/desk/${encodeURIComponent(
frappe.router.slug(frappe.router.doctype_layout || this.doctype)
)}/${encodeURIComponent(cstr(doc.name))}`;
}
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index 01d983d493..b0ac7676e5 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -107,7 +107,7 @@ frappe.router = {
if (path.substr(0, 1) === "/") path = path.substr(1);
path = path.split("/");
if (path[0]) {
- return path[0] === "app";
+ return path[0] === "desk";
}
},
@@ -144,11 +144,11 @@ frappe.router = {
this.current_sub_path = sub_path;
this.current_route = await this.parse();
+
this.set_history(sub_path);
- this.set_active_sidebar_item();
this.render();
this.set_title(sub_path);
- this.trigger("change");
+ this.trigger("change", this);
},
async parse(route) {
@@ -202,7 +202,7 @@ frappe.router = {
return frappe.model.with_doctype(doctype_route.doctype).then(() => {
// doctype route
let meta = frappe.get_meta(doctype_route.doctype);
-
+ this.meta = meta;
if (route[1] && route[1] === "view" && route[2]) {
route = this.get_standard_route_for_list(
route,
@@ -289,10 +289,6 @@ frappe.router = {
frappe.ui.hide_open_dialog();
},
- async set_active_sidebar_item() {
- frappe.app.sidebar.set_active_workspace_item();
- },
-
render() {
if (this.current_route[0]) {
this.render_page();
@@ -308,7 +304,6 @@ frappe.router = {
const route = this.current_route;
const factory = frappe.utils.to_title_case(route[0]);
-
if (route[1] && frappe.views[factory + "Factory"]) {
route[0] = factory;
// has a view generator, generate!
@@ -467,9 +462,12 @@ frappe.router = {
}).join("/");
if (path_string) {
- return "/app/" + path_string;
+ return "/desk/" + path_string;
}
+ if (params.length == 0) {
+ return "/desk";
+ }
// Resolution order
// 1. User's default workspace in user doctype
// 2. Private home
@@ -488,11 +486,13 @@ frappe.router = {
if (workspace) {
return (
- "/app/" + (workspace.public ? "" : "private/") + frappe.router.slug(workspace.name)
+ "/desk/" +
+ (workspace.public ? "" : "private/") +
+ frappe.router.slug(workspace.name)
);
}
- return "/app";
+ return "/desk";
},
/**
@@ -525,8 +525,8 @@ frappe.router = {
strip_prefix(route) {
if (route.substr(0, 1) == "/") route = route.substr(1); // for /app/sub
- if (route == "app") route = route.substr(4); // for app
- if (route.startsWith("app/")) route = route.substr(4); // for desk/sub
+ if (route == "desk") route = route.substr(4); // for app
+ if (route.startsWith("desk/")) route = route.substr(4); // for desk/sub
if (route.substr(0, 1) == "/") route = route.substr(1);
if (route.substr(0, 1) == "#") route = route.substr(1);
if (route.substr(0, 1) == "!") route = route.substr(1);
diff --git a/frappe/public/js/frappe/ui/apps_switcher.html b/frappe/public/js/frappe/ui/apps_switcher.html
deleted file mode 100644
index 69ab6242d8..0000000000
--- a/frappe/public/js/frappe/ui/apps_switcher.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/frappe/public/js/frappe/ui/apps_switcher.js b/frappe/public/js/frappe/ui/apps_switcher.js
deleted file mode 100644
index 643e47cd07..0000000000
--- a/frappe/public/js/frappe/ui/apps_switcher.js
+++ /dev/null
@@ -1,197 +0,0 @@
-frappe.ui.AppsSwitcher = class AppsSwitcher {
- constructor(sidebar) {
- this.sidebar = sidebar;
- this.sidebar_wrapper = $(this.sidebar.wrapper.find(".body-sidebar"));
- this.drop_down_expanded = false;
- this.make();
- this.setup_app_switcher();
- this.set_hover();
- }
-
- make() {
- this.wrapper = $(
- frappe.render_template("apps_switcher", {
- app_logo_url: frappe.boot.app_data[0].app_logo_url,
- app_title: __(frappe.boot.app_data[0].app_title),
- })
- ).prependTo(this.sidebar_wrapper);
- this.app_switcher_dropdown = $(".app-switcher-dropdown");
- }
-
- setup_app_switcher() {
- this.app_switcher_menu = $(".app-switcher-menu");
- $(".app-switcher-dropdown").on("click", (e) => {
- this.toggle_app_menu();
- e.stopImmediatePropagation();
- });
- }
- toggle_app_menu() {
- this.toggle_active();
- this.app_switcher_menu.toggleClass("hidden");
- }
- create_app_data_map() {
- frappe.boot.app_data_map = {};
- for (var app of frappe.boot.app_data) {
- frappe.boot.app_data_map[app.app_name] = app;
- if (app.workspaces?.length) {
- this.add_app_item(app);
- }
- }
- }
- populate_apps_menu() {
- this.add_private_app();
-
- this.add_website_select();
- this.add_settings_select();
- this.setup_select_app();
- }
-
- add_app_item(app) {
- $(`
`).appendTo(this.app_switcher_menu);
- }
-
- add_private_app() {
- let private_pages = this.sidebar.all_pages.filter((p) => p.public === 0);
- if (private_pages.length === 0) return;
-
- const app = {
- app_name: "private",
- app_title: __("My Workspaces"),
- app_route: "/app/private",
- app_logo_url: "/assets/frappe/images/frappe-framework-logo.svg",
- workspaces: private_pages,
- };
-
- frappe.boot.app_data_map["private"] = app;
- $(`
`).prependTo(this.app_switcher_menu);
- $(`
`).prependTo(this.app_switcher_menu);
- }
-
- setup_select_app() {
- this.app_switcher_menu.find(".app-item").on("click", (e) => {
- let item = $(e.delegateTarget);
- let route = item.attr("data-app-route");
- this.app_switcher_menu.toggleClass("hidden");
- this.toggle_active();
-
- if (item.attr("data-app-name") == "settings") {
- frappe.quick_edit("Workspace Settings");
- return;
- }
- if (route.startsWith("/app/private")) {
- this.set_current_app("private");
- let ws = Object.values(frappe.workspace_map).find((ws) => ws.public === 0);
- route += "/" + frappe.router.slug(ws.title);
- frappe.set_route(route);
- } else if (route.startsWith("/app")) {
- frappe.set_route(route);
- this.set_current_app(item.attr("data-app-name"));
- } else {
- // new page
- window.open(route);
- }
- });
- }
- // refactor them into one single function
- add_website_select() {
- $(`
`).appendTo(this.app_switcher_menu);
- this.add_app_item(
- {
- app_name: "website",
- app_title: __("Website"),
- app_route: "/",
- app_logo_url: "/assets/frappe/images/web.svg",
- },
- this.app_switcher_menu
- );
- }
-
- add_settings_select() {
- $(`
`).appendTo(this.app_switcher_menu);
- this.add_app_item({
- app_name: "settings",
- app_title: __("Settings"),
- app_logo_url: "/assets/frappe/images/settings-gear.svg",
- });
- let settings_item = this.app_switcher_menu.children().last();
- }
-
- set_current_app(app) {
- if (!app) {
- console.warn("set_current_app: app not defined");
- return;
- }
- let app_data = frappe.boot.app_data_map[app] || frappe.boot.app_data_map["frappe"];
-
- this.sidebar_wrapper
- .find(".app-switcher-dropdown .sidebar-item-icon img")
- .attr("src", app_data.app_logo_url);
- this.sidebar_wrapper
- .find(".app-switcher-dropdown .sidebar-item-label")
- .html(app_data.app_title);
-
- frappe.frappe_toolbar.set_app_logo(app_data.app_logo_url);
-
- if (frappe.current_app === app) return;
- frappe.current_app = app;
-
- // re-render the sidebar
- frappe.app.sidebar.make_sidebar();
- }
-
- set_hover() {
- const me = this;
-
- this.app_switcher_dropdown.on("mouseover", function () {
- if ($(this).hasClass("active-sidebar")) return;
- $(this).addClass("hover");
-
- if (!me.sidebar.sidebar_expanded) {
- $(this).removeClass("hover");
- }
- });
-
- this.app_switcher_dropdown.on("mouseleave", function () {
- $(this).removeClass("hover");
- });
- }
-
- toggle_active() {
- this.toggle_dropdown();
- this.app_switcher_dropdown.toggleClass("active-sidebar");
- if (!this.sidebar.sidebar_expanded) {
- this.app_switcher_dropdown.removeClass("active-sidebar");
- }
- }
- toggle_dropdown() {
- if (this.drop_down_expanded) {
- this.drop_down_expanded = false;
- } else {
- this.drop_down_expanded = true;
- }
- }
-};
diff --git a/frappe/public/js/frappe/ui/desktop_icon.html b/frappe/public/js/frappe/ui/desktop_icon.html
new file mode 100644
index 0000000000..61489dfa15
--- /dev/null
+++ b/frappe/public/js/frappe/ui/desktop_icon.html
@@ -0,0 +1,21 @@
+
+ {% if (icon.logo_url) { %}
+
+

+
+ {% } else if (icon.icon_type == "Folder") { %}
+
+
+
+ {% } else { %}
+
+ {%= frappe.utils.icon(icon.icon || "list-alt" , "lg", "", "", "text-ink-gray-7 current-color", true)%}
+
+ {% } %}
+ {% if (!in_folder) { %}
+
+ {% } %}
+
\ No newline at end of file
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index d40385bb85..714edade37 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -249,7 +249,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
show() {
// show it
- if (window.location.pathname.startsWith("/app")) {
+ if (window.location.pathname.startsWith("/desk")) {
this.handle_focus();
}
diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js
index 020999f768..a824074298 100644
--- a/frappe/public/js/frappe/ui/keyboard.js
+++ b/frappe/public/js/frappe/ui/keyboard.js
@@ -217,6 +217,16 @@ frappe.ui.keys.add_shortcut({
description: __("Open Awesomebar"),
});
+frappe.ui.keys.add_shortcut({
+ shortcut: "ctrl+g",
+ action: function (e) {
+ $("#navbar-search").focus();
+ e.preventDefault();
+ return false;
+ },
+ description: __("Open Awesomebar"),
+});
+
frappe.ui.keys.add_shortcut({
shortcut: "alt+s",
action: function (e) {
diff --git a/frappe/public/js/frappe/ui/menu.js b/frappe/public/js/frappe/ui/menu.js
new file mode 100644
index 0000000000..c188fe7035
--- /dev/null
+++ b/frappe/public/js/frappe/ui/menu.js
@@ -0,0 +1,162 @@
+import "../dom";
+frappe.provide("frappe.ui");
+
+frappe.ui.menu = class ContextMenu {
+ constructor(menu_items, left) {
+ this.template = $(``);
+ this.menu_items = menu_items;
+ this.open_on_left = left;
+ }
+
+ make() {
+ this.template.empty();
+
+ this.menu_items.forEach((f) => {
+ this.add_menu_item(f);
+ });
+
+ if (!$.contains(document.body, this.template[0])) {
+ $(document.body).append(this.template);
+ }
+ }
+ add_menu_item(item) {
+ const me = this;
+ let item_wrapper = $(``);
+ if (!item.url) {
+ item_wrapper.on("click", function () {
+ item.onClick();
+ me.hide();
+ });
+ } else {
+ $(item_wrapper).attr("href", item.url);
+ }
+ item_wrapper.appendTo(this.template);
+ }
+ show(element) {
+ this.close_all_other_menu();
+
+ this.make();
+
+ const offset = $(element).offset();
+ const height = $(element).outerHeight();
+ this.left_offset = 0;
+
+ this.template.css({
+ display: "block",
+ position: "absolute",
+ top: offset.top + height + "px",
+ left: offset.left,
+ });
+ if (this.open_on_left) {
+ this.left_offset = element.getBoundingClientRect().width;
+ this.template.css({
+ left:
+ offset.left -
+ this.template.get(0).getBoundingClientRect().width +
+ this.left_offset +
+ "px",
+ });
+ }
+
+ this.visible = true;
+ }
+ close_all_other_menu() {
+ $(".context-menu").hide();
+ }
+ hide() {
+ this.template.css("display", "none");
+ this.visible = false;
+ }
+ mouseX(evt) {
+ if (evt.pageX) {
+ return evt.pageX;
+ } else if (evt.clientX) {
+ return (
+ evt.clientX +
+ (document.documentElement.scrollLeft
+ ? document.documentElement.scrollLeft
+ : document.body.scrollLeft)
+ );
+ } else {
+ return null;
+ }
+ }
+
+ mouseY(evt) {
+ if (evt.pageY) {
+ return evt.pageY;
+ } else if (evt.clientY) {
+ return (
+ evt.clientY +
+ (document.documentElement.scrollTop
+ ? document.documentElement.scrollTop
+ : document.body.scrollTop)
+ );
+ } else {
+ return null;
+ }
+ }
+};
+
+frappe.menu_map = {};
+
+frappe.ui.create_menu = function attachContextMenuToElement(
+ element,
+ menuItems,
+ right_click,
+ open_on_left
+) {
+ let contextMenu = new frappe.ui.menu(menuItems, open_on_left);
+
+ frappe.menu_map[$(element).data("menu")] = contextMenu;
+ if (right_click) {
+ $(element).on("contextmenu", function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (
+ frappe.menu_map[$(element).data("menu")] &&
+ frappe.menu_map[$(element).data("menu")].visible
+ ) {
+ frappe.menu_map[$(element).data("menu")].hide();
+ } else {
+ frappe.menu_map[$(element).data("menu")].show(this);
+ }
+ });
+ } else {
+ $(element).on("click", function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (frappe.menu_map[$(element).data("menu")].visible) {
+ frappe.menu_map[$(element).data("menu")].hide();
+ } else {
+ frappe.menu_map[$(element).data("menu")].show(this);
+ }
+ });
+ }
+
+ $(document).on("click", function () {
+ if (frappe.menu_map[$(element).data("menu")].visible) {
+ frappe.menu_map[$(element).data("menu")].hide();
+ }
+ });
+
+ $(document).on("keydown", function (e) {
+ if (e.key === "Escape" && frappe.menu_map[$(element).data("menu")].visible) {
+ frappe.menu_map[$(element).data("menu")].hide();
+ }
+ });
+};
diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js
index c069f5974c..ed0225a35b 100644
--- a/frappe/public/js/frappe/ui/page.js
+++ b/frappe/public/js/frappe/ui/page.js
@@ -36,6 +36,7 @@ frappe.ui.Page = class Page {
this.views = {};
this.make();
+ if (!Object.keys(opts).includes("hide_sidebar")) this.hide_sidebar = false;
frappe.ui.pages[frappe.get_route_str()] = this;
}
@@ -136,7 +137,7 @@ frappe.ui.Page = class Page {
this.page_actions = this.wrapper.find(".page-actions");
this.filters = this.wrapper.find(".filters");
-
+ this.page_head = this.wrapper.find(".page-head");
this.btn_primary = this.page_actions.find(".primary-action");
this.btn_secondary = this.page_actions.find(".btn-secondary");
diff --git a/frappe/public/js/frappe/ui/sidebar.html b/frappe/public/js/frappe/ui/sidebar.html
deleted file mode 100644
index 1f427d438f..0000000000
--- a/frappe/public/js/frappe/ui/sidebar.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
diff --git a/frappe/public/js/frappe/ui/sidebar.js b/frappe/public/js/frappe/ui/sidebar.js
deleted file mode 100644
index 372072a76b..0000000000
--- a/frappe/public/js/frappe/ui/sidebar.js
+++ /dev/null
@@ -1,501 +0,0 @@
-frappe.ui.Sidebar = class Sidebar {
- constructor() {
- this.items = {};
- this.parent_items = [];
- this.sidebar_expanded = false;
-
- if (!frappe.boot.setup_complete) {
- // no sidebar if setup is not complete
- return;
- }
-
- this.set_all_pages();
- this.make_dom();
- this.sidebar_items = {
- public: {},
- private: {},
- };
- this.indicator_colors = [
- "green",
- "cyan",
- "blue",
- "orange",
- "yellow",
- "gray",
- "grey",
- "red",
- "pink",
- "darkgrey",
- "purple",
- "light-blue",
- ];
-
- this.setup_pages();
- this.apps_switcher.populate_apps_menu();
- this.handle_outside_click();
- }
-
- make_dom() {
- this.set_default_app();
- this.wrapper = $(frappe.render_template("sidebar")).prependTo("body");
-
- this.$sidebar = this.wrapper.find(".sidebar-items");
-
- this.wrapper.find(".body-sidebar .collapse-sidebar-link").on("click", () => {
- this.toggle_sidebar();
- });
-
- this.wrapper.find(".overlay").on("click", () => {
- this.close_sidebar();
- });
- this.apps_switcher = new frappe.ui.AppsSwitcher(this);
- this.apps_switcher.create_app_data_map();
- }
-
- set_hover() {
- $(".standard-sidebar-item > .item-anchor").on("mouseover", function (event) {
- if ($(this).parent().hasClass("active-sidebar")) return;
- $(this).parent().addClass("hover");
- });
-
- $(".standard-sidebar-item > .item-anchor").on("mouseleave", function () {
- $(this).parent().removeClass("hover");
- });
- }
-
- 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;
- }
-
- set_default_app() {
- // sort apps based on # of workspaces
- frappe.boot.app_data.sort((a, b) => (a.workspaces.length < b.workspaces.length ? 1 : -1));
- frappe.current_app = frappe.boot.app_data[0].app_name;
- frappe.frappe_toolbar.set_app_logo(frappe.boot.app_data[0].app_logo_url);
- }
-
- 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)) {
- this.active_item.addClass("active-sidebar");
- }
- if (this.active_item) {
- if (this.is_nested_item(this.active_item.parent())) {
- let current_item = this.active_item.parent();
- this.expand_parent_item(current_item);
- }
- }
- if (!this.sidebar_expanded) this.close_children_item();
- }
- expand_parent_item(item) {
- let parent_title = item.attr("item-parent");
- if (!parent_title) return;
-
- let parent = this.get_sidebar_item(parent_title);
- if (parent) {
- let $drop_icon = $(parent).find(".drop-icon");
- if ($($(parent).children()[1]).hasClass("hidden")) {
- $drop_icon[0].click();
- if (this.is_nested_item($(parent))) {
- this.expand_parent_item($(parent));
- }
- }
- }
- }
- is_nested_item(item) {
- if (item.attr("item-parent")) {
- return true;
- } else {
- return false;
- }
- }
-
- get_sidebar_item(name) {
- let sidebar_item = "";
- $(".sidebar-item-container").each(function () {
- if ($(this).attr("item-name") == name) {
- sidebar_item = this;
- }
- });
- 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) {
- match = true;
- if (that.active_item) that.active_item.removeClass("active-sidebar");
- that.active_item = $(this).parent();
- // this exists the each loop
- return false;
- }
- });
- return match;
- }
-
- 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 = [];
- frappe.workspace_map = {};
- for (let page of this.all_pages) {
- frappe.workspaces[frappe.router.slug(page.name)] = {
- name: page.name,
- public: page.public,
- };
- if (!page.app && page.module) {
- page.app = frappe.boot.module_app[frappe.slug(page.module)];
- }
- frappe.workspace_map[page.name] = page;
- frappe.workspace_list.push(page);
- }
- this.make_sidebar();
- }
- this.set_hover();
- this.set_sidebar_state();
- }
- set_sidebar_state() {
- this.sidebar_expanded = true;
- if (localStorage.getItem("sidebar-expanded") !== null) {
- this.sidebar_expanded = JSON.parse(localStorage.getItem("sidebar-expanded"));
- }
- if (frappe.is_mobile()) {
- this.sidebar_expanded = false;
- }
- this.expand_sidebar();
- }
- make_sidebar() {
- 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);
- } else {
- parent_pages = parent_pages.filter((p) => p.public && app_workspaces.includes(p.name));
- }
-
- this.build_sidebar_section("All", parent_pages);
-
- // Scroll sidebar to selected page if it is not in viewport.
- this.wrapper.find(".selected").length &&
- !frappe.dom.is_element_in_viewport(this.wrapper.find(".selected")) &&
- this.wrapper.find(".selected")[0].scrollIntoView();
-
- this.setup_sorting();
- this.set_active_workspace_item();
- this.set_hover();
- }
-
- build_sidebar_section(title, root_pages) {
- let sidebar_section = $(
- ``
- );
-
- this.prepare_sidebar(root_pages, sidebar_section, this.wrapper.find(".sidebar-items"));
-
- if (Object.keys(root_pages).length === 0) {
- sidebar_section.addClass("hidden");
- }
-
- $(".item-anchor").on("click", () => {
- $(".list-sidebar.hidden-xs.hidden-sm").removeClass("opened");
- // $(".close-sidebar").css("display", "none");
- $("body").css("overflow", "auto");
- if (frappe.is_mobile()) {
- this.close_sidebar();
- }
- });
-
- if (
- sidebar_section.find(".sidebar-item-container").length &&
- sidebar_section.find("> [item-is-hidden='0']").length == 0
- ) {
- sidebar_section.addClass("hidden show-in-edit-mode");
- }
- }
-
- prepare_sidebar(items, child_container, item_container) {
- let last_item = null;
- for (let item of items) {
- if (item.public && last_item && !last_item.public) {
- $(``).appendTo(child_container);
- }
-
- // visibility not explicitly set to 0
- if (item.visibility !== 0) {
- this.append_item(item, child_container);
- }
- last_item = item;
- }
- child_container.appendTo(item_container);
- }
- toggle_sidebar() {
- if (!this.sidebar_expanded) {
- this.open_sidebar();
- } else {
- this.close_sidebar();
- }
- }
- expand_sidebar() {
- let direction;
- if (this.sidebar_expanded) {
- this.wrapper.addClass("expanded");
- // this.sidebar_expanded = false
- direction = "left";
- } else {
- this.wrapper.removeClass("expanded");
- // this.sidebar_expanded = true
- direction = "right";
- }
- localStorage.setItem("sidebar-expanded", this.sidebar_expanded);
- this.wrapper
- .find(".body-sidebar .collapse-sidebar-link")
- .find("use")
- .attr("href", `#icon-arrow-${direction}-to-line`);
- }
-
- append_item(item, container) {
- let is_current_page = false;
-
- item.selected = is_current_page;
-
- if (is_current_page) {
- this.current_page = { name: item.name, public: item.public };
- }
-
- 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);
- }
-
- $item_container.appendTo(container);
- this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container;
-
- if ($item_container.parent().hasClass("hidden") && is_current_page) {
- $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) {
- item.indicator_color =
- item.indicator_color || this.indicator_colors[Math.floor(Math.random() * 12)];
- let path;
- if (item.type === "Link") {
- if (item.link_type === "Report") {
- path = frappe.utils.generate_route({
- type: item.link_type,
- name: item.link_to,
- is_query_report: item.report.report_type === "Query Report",
- report_ref_doctype: item.report.ref_doctype,
- });
- } else {
- path = frappe.utils.generate_route({ type: item.link_type, name: item.link_to });
- }
- } 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]);
- }
- }
-
- return $(`
-
- `);
- }
-
- 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
- ) {
- drop_icon = "small-up";
- }
-
- let $child_item_section = item_container.find(".sidebar-child-item");
- let $drop_icon = $(`