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( + // "" + // ); + // $(".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) { %} +
+ {{ icon.label }} +
+ {% } 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) { %} +
+
{{ icon.label }}
+
+
+ {% } %} +
\ 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 = $(` + + + + {%= frappe.utils.icon("arrow-left-to-line" , "sm", "", "", "text-ink-gray-7 current-color", true)%} + {%= __("Collapse") %} + + + +
+ + diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js new file mode 100644 index 0000000000..da50e3a0ae --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -0,0 +1,829 @@ +import "./sidebar_item"; +frappe.ui.Sidebar = class Sidebar { + constructor() { + if (!frappe.boot.setup_complete) { + // no sidebar if setup is not complete + return; + } + this.make_dom(); + // states + this.edit_mode = false; + this.sidebar_expanded = false; + this.all_sidebar_items = frappe.boot.workspace_sidebar_item; + this.$items = []; + this.fields_for_dialog = []; + this.workspace_sidebar_items = []; + this.new_sidebar_items = []; + this.$items_container = this.wrapper.find(".sidebar-items"); + this.$sidebar = this.wrapper.find(".body-sidebar"); + this.items = []; + this.setup_events(); + this.sidebar_module_map = {}; + this.build_sidebar_module_map(); + } + + prepare() { + try { + this.sidebar_data = + frappe.boot.workspace_sidebar_item[this.workspace_title.toLowerCase()]; + this.workspace_sidebar_items = this.sidebar_data.items; + if (this.edit_mode) { + this.workspace_sidebar_items = this.new_sidebar_items; + } + this.choose_app_name(); + this.find_nested_items(); + } catch (e) { + console.log(e); + } + } + build_sidebar_module_map() { + for (const [key, value] of Object.entries(frappe.boot.workspace_sidebar_item)) { + if (value.module) { + if (!this.sidebar_module_map[value.module]) { + this.sidebar_module_map[value.module] = []; + } + this.sidebar_module_map[value.module].push(value.label); + } + } + } + choose_app_name() { + if (frappe.boot.app_name_style === "Default") return; + + for (const app of frappe.boot.app_data) { + if (app.workspaces.includes(this.workspace_title)) { + this.header_subtitle = app.app_title; + this.app_logo_url = app.app_logo_url; + return; + } + } + + const icon = frappe.boot.desktop_icons.find((i) => i.label === this.workspace_title); + if (icon) { + this.header_subtitle = icon.parent_icon; + } + + if (this.workspace_title == "My Workspaces") { + this.header_subtitle = frappe.session.user; + } + } + + find_nested_items() { + const me = this; + let currentSection = null; + const updated_items = []; + + this.workspace_sidebar_items.forEach((item) => { + item.nested_items = []; + + if (item.type === "Section Break") { + currentSection = item; + updated_items.push(item); + } else if (currentSection && item.child) { + item.parent = currentSection; + currentSection.nested_items.push(item); + } else { + updated_items.push(item); + } + }); + this.workspace_sidebar_items = updated_items; + } + setup(workspace_title) { + this.workspace_title = workspace_title; + this.check_for_private_workspace(workspace_title); + this.prepare(); + this.$sidebar.attr("data-title", this.workspace_title); + this.sidebar_header = new frappe.ui.SidebarHeader(this); + this.make_sidebar(); + this.setup_complete = true; + } + check_for_private_workspace(workspace_title) { + if (workspace_title == "private" || workspace_title == "Personal") { + this.workspace_title = "My Workspaces"; + } + } + setup_events() { + const me = this; + frappe.router.on("change", function (router) { + frappe.app.sidebar.set_workspace_sidebar(router); + }); + $(document).on("page-change", function () { + frappe.app.sidebar.toggle(); + }); + $(document).on("form-refresh", function () { + frappe.app.sidebar.toggle(); + }); + } + + toggle() { + if (!frappe.container.page.page) return; + if (frappe.container.page.page.hide_sidebar) { + this.wrapper.hide(); + } else { + this.wrapper.show(); + this.set_sidebar_for_page(); + } + } + make_dom() { + this.load_sidebar_state(); + this.wrapper = $( + frappe.render_template("sidebar", { + expanded: this.sidebar_expanded, + }) + ).prependTo("body"); + this.$sidebar = this.wrapper.find(".sidebar-items"); + + this.wrapper.find(".body-sidebar .collapse-sidebar-link").on("click", () => { + this.toggle_width(); + }); + + this.wrapper.find(".overlay").on("click", () => { + this.close(); + }); + } + + set_active_workspace_item() { + if (this.is_route_in_sidebar()) { + this.active_item.addClass("active-sidebar"); + } + } + + is_route_in_sidebar() { + let match = false; + const that = this; + $(".item-anchor").each(function () { + let href = $(this).attr("href")?.split("?")[0]; + const path = decodeURIComponent(window.location.pathname); + + // Match only if path equals href or starts with it followed by "/" or end of string + const isActive = new RegExp(`^${href}(?:/|$)`).test(path); + if (href && isActive) { + 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; + } + + set_sidebar_state() { + this.load_sidebar_state(); + if (this.workspace_sidebar_items.length === 0) { + this.sidebar_expanded = true; + } + + this.expand_sidebar(); + } + + load_sidebar_state() { + this.sidebar_expanded = true; + if (localStorage.getItem("sidebar-expanded") !== null) { + this.sidebar_expanded = JSON.parse(localStorage.getItem("sidebar-expanded")); + } + + if (frappe.is_mobile()) { + this.sidebar_expanded = false; + } + } + empty() { + if (this.wrapper.find(".sidebar-items")[0]) { + this.wrapper.find(".sidebar-items").html(""); + } + } + make_sidebar() { + this.empty(); + this.wrapper.find(".collapse-sidebar-link").removeClass("hidden"); + this.create_sidebar(this.workspace_sidebar_items); + + // 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.set_active_workspace_item(); + this.set_sidebar_state(); + } + create_sidebar(items) { + this.empty(); + if (items && items.length > 0) { + items.forEach((w) => { + if (!w.display_depends_on || frappe.utils.eval(w.display_depends_on)) { + this.add_item(w); + } + }); + } else { + let no_items_message = $( + "
No Sidebar Items
" + ); + this.wrapper.find(".sidebar-items").append(no_items_message); + this.wrapper.find(".collapse-sidebar-link").addClass("hidden"); + } + if (this.edit_mode) { + $(".edit-menu").removeClass("hidden"); + } + this.handle_outside_click(); + } + + add_item(item) { + this.items.push( + this.make_sidebar_item({ + container: this.$items_container, + item: item, + }) + ); + } + make_sidebar_item(opts) { + let class_name = `Type${frappe.utils.to_title_case(opts.item.type).replace(/ /g, "")}`; + + return new frappe.ui.sidebar_item[class_name](opts); + } + update_item(item, index) {} + + remove_item(item, index) {} + + toggle_width() { + if (!this.sidebar_expanded) { + this.open(); + } else { + this.close(); + } + } + + 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`); + this.sidebar_header.toggle_width(this.sidebar_expanded); + $(document).trigger("sidebar-expand", { + sidebar_expand: this.sidebar_expanded, + }); + } + + close() { + this.sidebar_expanded = false; + + this.expand_sidebar(); + if (frappe.is_mobile()) frappe.app.sidebar.prevent_scroll(); + } + open() { + this.sidebar_expanded = true; + this.expand_sidebar(); + this.set_active_workspace_item(); + } + + set_height() { + $(".body-sidebar").css("height", window.innerHeight + "px"); + $(".overlay").css("height", window.innerHeight + "px"); + document.body.style.overflow = "hidden"; + } + + handle_outside_click() { + document.addEventListener("click", (e) => { + if (this.sidebar_header.drop_down_expanded) { + if (!e.composedPath().includes(this.sidebar_header.app_switcher_dropdown)) { + this.sidebar_header.toggle_dropdown_menu(); + } + } + }); + } + + prevent_scroll() { + let main_section = $(".main-section"); + if (this.sidebar_expanded) { + main_section.css("overflow", "hidden"); + } else { + main_section.css("overflow", ""); + } + } + + set_workspace_sidebar(router) { + let route = frappe.get_route(); + if (frappe.get_route()[0] == "setup-wizard") return; + if (route[0] == "Workspaces") { + let workspace; + if (!route[1]) { + workspace = "My Workspaces"; + } else { + workspace = route[1]; + } + + frappe.app.sidebar.setup(workspace); + } else if (route[0] == "List" || route[0] == "Form") { + let doctype = route[1]; + let sidebars = this.get_correct_workspace_sidebars(doctype); + // prevents switching of the sidebar if one item is linked in two sidebars + if (sidebars.includes(this.workspace_title)) { + frappe.app.sidebar.setup(this.workspace_title); + return; + } + if (sidebars.length == 0) { + let module_name = router.meta?.module; + if (module_name) { + frappe.app.sidebar.setup( + this.sidebar_module_map[module_name][0] || module_name + ); + } + } else { + if ( + this.workspace_title && + sidebars.includes(this.workspace_title.toLowerCase()) + ) { + frappe.app.sidebar.setup(this.workspace_title.toLowerCase()); + } else { + frappe.app.sidebar.setup(sidebars[0]); + } + } + } else if (route[0] == "query-report") { + let doctype = route[1]; + let sidebars = this.get_correct_workspace_sidebars(doctype); + if (this.workspace_title && sidebars.includes(this.workspace_title.toLowerCase())) { + frappe.app.sidebar.setup(this.workspace_title.toLowerCase()); + } else { + frappe.app.sidebar.setup(sidebars[0]); + } + } + + this.set_active_workspace_item(); + } + + set_sidebar_for_page() { + let route = frappe.get_route(); + let views = ["List", "Form", "Workspaces", "query-report"]; + let matches = views.some((view) => route.includes(view)); + if (matches) return; + let workspace_title; + if (route.length == 2) { + workspace_title = this.get_correct_workspace_sidebars(route[1]); + } else { + workspace_title = this.get_correct_workspace_sidebars(route); + } + let module_name = workspace_title[0]; + if (module_name) { + frappe.app.sidebar.setup(module_name || this.workspace_title); + } + } + + get_correct_workspace_sidebars(link_to) { + let sidebars = []; + Object.entries(this.all_sidebar_items).forEach(([name, sidebar]) => { + const { items, label } = sidebar; + items.forEach((item) => { + if (item.link_to === link_to) { + sidebars.push(label || name); + } + }); + }); + return sidebars; + } + + toggle_editing_mode() { + const me = this; + if (this.edit_mode) { + this.wrapper.attr("data-mode", "edit"); + this.new_sidebar_items = Array.from(me.workspace_sidebar_items); + $(this.active_item).removeClass("active-sidebar"); + $(".collapse-sidebar-link").addClass("hidden"); + this.wrapper.find(".edit-mode").removeClass("hidden"); + this.add_new_item_button = this.wrapper.find("[data-name='add-sidebar-item']"); + this.setup_sorting(); + + this.setup_editing_controls(); + this.add_new_item_button.on("click", function () { + me.show_new_dialog(); + }); + } else { + $(this.active_item).addClass("active-sidebar"); + $(".collapse-sidebar-link").removeClass("hidden"); + this.wrapper.find(".edit-mode").addClass("hidden"); + this.add_new_item_button = this.wrapper.find("[data-name='add-sidebar-item']"); + } + } + setup_sorting() { + const me = this; + this.sortable = Sortable.create($(".sidebar-items").get(0), { + handler: ".drag-handle", + onEnd: function (event) { + if (me.new_sidebar_items.length == 0) { + me.new_sidebar_items = Array.from(me.workspace_sidebar_items); + } + let old_index = event.oldIndex; + let new_index = event.newIndex; + me.new_sidebar_items[old_index]; + let b = me.new_sidebar_items[old_index]; + me.new_sidebar_items[old_index] = me.new_sidebar_items[new_index]; + me.new_sidebar_items[new_index] = b; + }, + }); + this.setup_sorting_for_nested_container(); + } + setup_sorting_for_nested_container() { + const me = this; + $(".nested-container").each(function (index, el) { + Sortable.create(el, { + handle: ".drag-handle", + onEnd: function (event) { + let new_index = event.newIndex; + let old_index = event.oldIndex; + let item_label = $(event.item).data("id"); + me.new_sidebar_items.forEach((item) => { + if (item.nested_items.length) { + let child = item.nested_items.find( + (child) => child.label === item_label + ); + if (child) { + let b = item.nested_items[old_index]; + item.nested_items[old_index] = item.nested_items[new_index]; + item.nested_items[new_index] = b; + } + } + }); + }, + }); + }); + } + make_dialog(opts) { + let title = "New Sidebar Item"; + + const me = this; + this.dialog_opts = opts; + + // Create the dialog + let dialog_fields = [ + { + fieldname: "label", + fieldtype: "Data", + in_list_view: 1, + label: "Label", + onchange: function (opts) { + let label = this.get_value(); + switch (label) { + case "Home": + d.set_value("icon", "home"); + d.set_value("link_type", "Workspace"); + d.set_value("link_to", me.workspace_title); + break; + + case "Reports": + d.set_value("type", "Section Break"); + d.set_value("link_to", null); + break; + + case "Dashboard": + d.set_value("link_type", "Dashboard"); + d.set_value("link_to", me.workspace_title); + d.set_value("icon", "layout-dashboard"); + break; + + case "Learn": + d.set_value("icon", "graduation-cap"); + d.set_value("link_type", "URL"); + break; + + case "Settings": + d.set_value("icon", "settings"); + break; + } + + if (d.get_value("type") == "Link" && d.get_value("link_type") !== "URL") { + d.set_value("link_to", label); + } + + if ( + me.dialog_opts && + me.dialog_opts.parent_item && + me.dialog_opts.parent_item.label == "Reports" + ) { + d.set_value("icon", "table"); + d.set_value("link_type", "Report"); + } + }, + }, + { + default: "Link", + fieldname: "type", + fieldtype: "Select", + in_list_view: 1, + label: "Type", + options: "Link\nSection Break\nSpacer\nSidebar Item Group", + onchange: function () { + let type = this.get_value(); + if (type == "Section Break") { + d.set_value("link_to", null); + } + }, + }, + { + default: "DocType", + depends_on: "eval: doc.type == 'Link'", + fieldname: "link_type", + fieldtype: "Select", + in_list_view: 1, + label: "Link Type", + options: "DocType\nPage\nReport\nWorkspace\nDashboard\nURL", + onchange: function () { + d.set_value("link_to", null); + }, + }, + { + depends_on: "eval: doc.link_type != \"URL\" && doc.type == 'Link'", + fieldname: "link_to", + fieldtype: "Dynamic Link", + in_list_view: 1, + label: "Link To", + options: "link_type", + onchange: function () { + if (d.get_value("link_type") == "DocType") { + let doctype = this.get_value(); + if (doctype) { + me.setup_filter(d, doctype); + } + } + }, + }, + { + depends_on: 'eval: doc.link_type == "URL"', + fieldname: "url", + fieldtype: "Data", + label: "URL", + }, + { + depends_on: + 'eval: doc.type == "Link" || (doc.indent == 1 && doc.type == "Section Break")', + fieldname: "icon", + fieldtype: "Icon", + options: "Emojis", + in_list_view: 1, + label: "Icon", + }, + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + { + depends_on: 'eval: doc.type == "Section Break"', + fieldname: "display_section", + fieldtype: "Section Break", + label: "Options", + }, + { + default: "0", + depends_on: 'eval: doc.type == "Section Break"', + fieldname: "indent", + fieldtype: "Check", + label: "Indent", + }, + { + depends_on: "eval: doc.indent == 1", + fieldname: "show_arrow", + fieldtype: "Check", + label: "Show Arrow", + }, + { + default: "1", + depends_on: 'eval: doc.type == "Section Break"', + fieldname: "collapsible", + fieldtype: "Check", + label: "Collapsible", + }, + { + fieldname: "column_break_krzu", + fieldtype: "Column Break", + }, + { + default: "0", + depends_on: 'eval: doc.type == "Section Break"', + fieldname: "keep_closed", + fieldtype: "Check", + label: "Keep Closed", + }, + { + fieldname: "details_section", + fieldtype: "Section Break", + label: "Details", + }, + + { + fieldtype: "Section Break", + }, + { + fieldname: "display_depends_on", + fieldtype: "Code", + label: "Display Depends On (JS)", + options: "JS", + max_height: "10px", + }, + { + fieldtype: "Section Break", + }, + { + fieldname: "route_options", + fieldtype: "Code", + display_depends_on: "eval: doc.link_type == 'Page'", + label: "Route Options", + options: "JSON", + max_height: "50px", + }, + ]; + if (opts && opts.item) { + dialog_fields.forEach((f) => { + if ( + opts.item[f.fieldname] !== undefined && + f.fieldtype !== "Section Break" && + f.fieldtype !== "Column Break" + ) { + f.default = opts.item[f.fieldname]; + } + }); + title = "Edit Sidebar Item"; + } + let d; + this.dialog = d = new frappe.ui.Dialog({ + title: title, + fields: dialog_fields, + primary_action_label: "Save", + size: "small", + primary_action(values) { + if (me.filter_group) { + me.filter_group.get_filters(); + } + + if (me.new_sidebar_items.length === 0) { + me.new_sidebar_items = Array.from(me.workspace_sidebar_items); + } + if (opts && opts.nested) { + values.child = 1; + console.log("Add it as a nested item"); + console.log(opts.parent_item); + let index = me.new_sidebar_items.findIndex((f) => { + return f.label == opts.parent_item.label; + }); + + if (!me.new_sidebar_items[index].nested_items) { + me.new_sidebar_items[index].nested_items = []; + } + me.new_sidebar_items[index].nested_items.push(values); + } else if (opts && opts.item) { + if (opts.item.child) { + let parent_icon = me.find_parent(me.new_sidebar_items, opts.item); + if (parent_icon) { + let index = parent_icon.nested_items.indexOf(opts.item); + let parent_icon_index = me.new_sidebar_items.indexOf(parent_icon); + me.new_sidebar_items[parent_icon_index].nested_items[index] = values; + } + } else { + let index = me.new_sidebar_items.indexOf(opts.item); + + me.new_sidebar_items[index] = { + ...me.new_sidebar_items[index], + ...values, + }; + } + } else { + me.new_sidebar_items.push(values); + } + me.create_sidebar(me.new_sidebar_items); + me.setup_sorting_for_nested_container(); + d.hide(); + }, + }); + + return d; + } + setup_filter(d, doctype) { + if (this.filter_group) { + this.filter_group.wrapper.empty(); + delete this.filter_group; + } + + // let $loading = this.dialog.get_field("filter_area_loading").$wrapper; + // $(`${__("Loading Filters...")}`).appendTo($loading); + + this.filters = []; + + this.generate_filter_from_json && this.generate_filter_from_json(); + + this.filter_group = new frappe.ui.FilterGroup({ + parent: d.get_field("filter_area").$wrapper, + doctype: doctype, + on_change: () => {}, + }); + + frappe.model.with_doctype(doctype, () => { + this.filter_group.add_filters_to_filter_group(this.filters); + }); + } + hide_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", true); + } + + show_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", false); + } + setup_editing_controls() { + const me = this; + this.save_sidebar_button = this.wrapper.find(".save-sidebar"); + this.discard_button = this.wrapper.find(".discard-button"); + this.save_sidebar_button.on("click", async function (event) { + frappe.show_alert({ + message: __("Saving Sidebar"), + indicator: "success", + }); + + await frappe.call({ + type: "POST", + method: "frappe.desk.doctype.workspace_sidebar.workspace_sidebar.add_sidebar_items", + args: { + sidebar_title: + me.workspace_title || frappe.app.sidebar.sidebar_header.workspace_title, + sidebar_items: me.new_sidebar_items, + }, + callback: function (r) { + frappe.boot.workspace_sidebar_item[me.workspace_title.toLowerCase()] = [ + ...me.new_sidebar_items, + ]; + frappe.ui.toolbar.clear_cache(); + me.edit_mode = false; + me.toggle_editing_mode(); + me.make_sidebar(me); + }, + }); + }); + + this.discard_button.on("click", function () { + me.edit_mode = false; + me.toggle_editing_mode(); + me.make_sidebar(me); + }); + } + + find_parent(sidebar_items, item) { + for (const f of sidebar_items) { + if (f.nested_items && f.nested_items.includes(item)) { + return f; + } + } + } + + delete_item(item) { + let index; + if (item.child) { + let parent_icon = this.find_parent(this.new_sidebar_items, item); + index = parent_icon.nested_items.indexOf(item); + parent_icon.nested_items.splice(index, 1); + } else { + index = this.new_sidebar_items.indexOf(item); + this.new_sidebar_items.splice(index, 1); + } + this.create_sidebar(this.new_sidebar_items); + } + + add_below(item) { + let index = this.workspace_sidebar_items.indexOf(item); + this.show_new_dialog(index); + this.create_sidebar(this.new_sidebar_items); + } + + duplicate_item(item) { + let index = this.workspace_sidebar_items.indexOf(item); + this.new_sidebar_items.splice(index, 0, item); + this.create_sidebar(this.new_sidebar_items); + } + + edit_item(item) { + let d = this.make_dialog({ + item: item, + }); + d.show(); + } + + show_new_dialog(opts) { + let d = this.make_dialog(opts); + d.show(); + } + make_fields_for_grids(fields) { + let doc_fields = Array.from(fields); + doc_fields = doc_fields + .filter((f) => f.fieldtype !== "Section Break" && f.fieldtype !== "Column Break") + .map((f, i) => ({ + ...f, + in_list_view: i < 5 ? 1 : 0, + })); + let link_to_field = doc_fields.find((f) => f.label == "Link To"); + link_to_field.field_in_dialog = true; + return doc_fields; + } +}; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.html b/frappe/public/js/frappe/ui/sidebar/sidebar_header.html new file mode 100644 index 0000000000..1bf076fd8d --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.html @@ -0,0 +1,29 @@ + + + +
+ + + +
+ +
+ + \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js new file mode 100644 index 0000000000..4b5bb19b23 --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js @@ -0,0 +1,162 @@ +frappe.ui.SidebarHeader = class SidebarHeader { + constructor(sidebar) { + this.sidebar = sidebar; + this.sidebar_wrapper = $(".body-sidebar"); + this.drop_down_expanded = false; + this.workspace_title = this.sidebar.workspace_title; + const me = this; + this.dropdown_items = [ + { + name: "desktop", + label: __("Desktop"), + icon: "layout-grid", + onClick: function (el) { + frappe.set_route("/desk"); + }, + }, + { + name: "edit-sidebar", + label: __("Edit Sidebar"), + icon: "edit", + onClick: function () { + me.sidebar.edit_mode = true; + me.sidebar.toggle_editing_mode(); + }, + }, + { + name: "website", + label: __("Website"), + icon: "web", + onClick: function () { + window.open(window.location.origin); + }, + }, + ]; + this.make(); + this.setup_app_switcher(); + this.populate_dropdown_menu(); + this.setup_select_options(); + } + + make() { + $(".sidebar-header").remove(); + $(".sidebar-header-menu").remove(); + this.set_header_icon(); + $( + frappe.render_template("sidebar_header", { + workspace_title: this.workspace_title, + header_icon: frappe.utils.icon( + this.header_icon, + "lg", + "", + "", + "", + false, + `var(${this.header_bg_color})` + ), + header_bg_color: this.header_stroke_color, + }) + ).prependTo(this.sidebar_wrapper); + this.wrapper = $(".sidebar-header"); + this.dropdown_menu = this.wrapper.find(".sidebar-header-menu"); + this.$header_title = this.wrapper.find(".header-title"); + this.$drop_icon = this.wrapper.find(".drop-icon"); + } + set_header_icon() { + if (this.sidebar.sidebar_data) this.header_icon = this.sidebar.sidebar_data.header_icon; + } + setup_app_switcher() { + this.dropdown_menu = $(".sidebar-header-menu"); + $(".sidebar-header").on("click", (e) => { + this.toggle_dropdown_menu(); + e.stopImmediatePropagation(); + }); + } + + toggle_dropdown_menu() { + this.toggle_active(); + this.dropdown_menu.toggleClass("hidden"); + } + + populate_dropdown_menu() { + const me = this; + this.check_editing_access(); + this.dropdown_items.forEach((d) => { + me.add_app_item(d); + }); + } + check_editing_access() { + if (!frappe.boot.developer_mode) { + this.dropdown_items.splice(1, 1); + } + } + add_app_item(item) { + $(``).appendTo(this.dropdown_menu); + } + + setup_select_options() { + this.dropdown_menu.find(".dropdown-menu-item").on("click", (e) => { + let item = $(e.delegateTarget); + let name = item.attr("data-name"); + let current_item = this.dropdown_items.find((f) => f.name == name); + this.dropdown_menu.toggleClass("hidden"); + this.toggle_active(); + current_item.onClick(item); + }); + } + + toggle_active() { + this.toggle_dropdown(); + this.wrapper.toggleClass("active-sidebar"); + if (!this.sidebar.sidebar_expanded) { + this.wrapper.removeClass("active-sidebar"); + } + } + + toggle_dropdown() { + if (this.drop_down_expanded) { + this.drop_down_expanded = false; + } else { + this.drop_down_expanded = true; + } + } + + setup_hover() { + $(".sidebar-header").on("mouseover", function (event) { + if ($(this).parent().hasClass("active-sidebar")) return; + $(this).addClass("hover"); + }); + + $(".sidebar-header").on("mouseleave", function () { + $(this).removeClass("hover"); + }); + } + + toggle_width(expand) { + if (!expand) { + $(this.wrapper[0]).off("mouseleave"); + $(this.wrapper[0]).off("mouseover"); + this.wrapper.css("padding-left", "0px"); + this.wrapper.css("padding-right", "0px"); + } else { + this.setup_hover(); + this.wrapper.css("padding-left", "8px"); + this.wrapper.css("padding-right", "8px"); + } + } +}; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.html b/frappe/public/js/frappe/ui/sidebar/sidebar_item.html new file mode 100644 index 0000000000..f448294c1d --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.html @@ -0,0 +1,52 @@ + \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js new file mode 100644 index 0000000000..50e3010403 --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -0,0 +1,354 @@ +frappe.provide("frappe.ui.sidebar_item"); +frappe.ui.sidebar_item.TypeLink = class SidebarItem { + constructor(opts) { + this.item = opts.item; + this.container = opts.container; + this.nested_items = opts.item.nested_items || []; + this.workspace_title = $(".body-sidebar").attr("data-title").toLowerCase(); + this.prepare(opts); + this.make(); + } + get_path() { + let path; + if (this.item.type === "Link") { + if (this.item.link_type === "Report") { + let args = { + type: this.item.link_type, + name: this.item.link_to, + }; + + if (this.item.report || !frappe.app.sidebar.edit_mode) { + args.is_query_report = + this.item.report.report_type === "Query Report" || + this.item.report.report_type == "Script Report"; + args.report_ref_doctype = this.item.report.ref_doctype; + } + + path = frappe.utils.generate_route(args); + } else if (this.item.link_type == "Workspace") { + let workspaces = frappe.workspaces[this.item.link_to.toLowerCase()]; + if (workspaces.public) { + path = "/desk/" + frappe.router.slug(this.item.link_to); + } else { + path = "/desk/private/" + frappe.router.slug(workspaces.title); + } + + if (this.item.route) { + path = this.item.route; + } + } else if (this.item.link_type === "URL") { + path = this.item.url; + } else if (this.item.link_type == "Page" && this.item.route_options) { + path = frappe.utils.generate_route({ + type: this.item.link_type, + name: this.item.link_to, + route_options: JSON.parse(this.item.route_options), + }); + } else { + path = frappe.utils.generate_route({ + type: this.item.link_type, + name: this.item.link_to, + }); + } + } + return path; + } + prepare() {} + make() { + this.path = this.get_path(); + if (!this.item.icon && !(this.item.child && this.item.parent.indent)) { + this.item.icon = "list-alt"; + } + this.wrapper = $( + frappe.render_template("sidebar_item", { + item: this.item, + path: this.path, + edit_mode: frappe.app.sidebar.edit_mode, + }) + ); + $(this.container).append(this.wrapper); + this.setup_editing_controls(); + } + setup_editing_controls() { + this.menu_items = this.get_menu_items(); + this.$edit_menu = this.wrapper.find(".edit-menu"); + this.$sidebar_container = this.$edit_menu.parent(); + frappe.ui.create_menu(this.$edit_menu, this.menu_items); + } + get_menu_items() { + let me = this; + let menu_items = [ + { + label: "Edit Item", + icon: "pen", + onClick: () => { + frappe.app.sidebar.edit_item(me.item); + }, + }, + { + label: "Add Item Below", + icon: "add", + onClick: () => { + frappe.app.sidebar.add_below(me.item); + }, + }, + { + label: "Duplicate", + icon: "copy", + onClick: () => { + console.log("Start Deleting"); + frappe.app.sidebar.duplicate_item(me.item); + }, + }, + { + label: "Delete", + icon: "trash-2", + onClick: () => { + console.log(me.item); + frappe.app.sidebar.delete_item(me.item); + console.log("Start Deleting"); + }, + }, + ]; + return menu_items; + } + add_menu_items() {} +}; + +frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends ( + frappe.ui.sidebar_item.TypeLink +) { + prepare(opts) { + this.collapsed = false; + this.nested_items = opts.item.nested_items || this.nested_items; + this.items = []; + this.$items = []; + const storedState = localStorage.getItem("section-breaks-state"); + this.section_breaks_state = storedState ? JSON.parse(storedState) : {}; + } + add_items() { + this.$item_control = this.wrapper.find(".sidebar-item-control"); + this.$nested_items = this.wrapper.find(".nested-container").first(); + this.nested_items.forEach((f) => { + this.items.push( + frappe.app.sidebar.make_sidebar_item({ + container: this.$nested_items, + item: f, + }) + ); + }); + this.full_template = $(this.wrapper); + } + make() { + if (this.item.nested_items.length == 0) return; + super.make(); + this.add_items(); + this.toggle_on_collapse(); + this.enable_collapsible(this.item, this.full_template); + $(this.container).append(this.full_template); + } + open() { + this.collapsed = false; + this.toggle(); + } + close() { + this.collapsed = true; + this.toggle(); + } + toggle() { + if (this.collapsed) { + this.$drop_icon + .attr("data-state", "closed") + .find("use") + .attr("href", "#icon-chevron-right"); + $(this.$nested_items).addClass("hidden"); + } else { + this.$drop_icon + .attr("data-state", "opened") + .find("use") + .attr("href", "#icon-chevron-down"); + $(this.$nested_items).removeClass("hidden"); + } + } + toggle_on_collapse() { + const me = this; + this.old_state; + $(document).on("sidebar-expand", function (event, expand) { + if (expand.sidebar_expand) { + $(me.wrapper.find(".section-break")).removeClass("hidden"); + $(me.wrapper.find(".divider")).addClass("hidden"); + if (me.old_state) { + me.collapsed = me.old_state; + me.toggle(); + } + } else { + $(me.wrapper.find(".section-break")).addClass("hidden"); + $(me.wrapper.find(".divider")).removeClass("hidden"); + me.old_state = me.collapsed; + me.open(); + } + }); + } + + enable_collapsible(item, $item_container) { + let sidebar_control = this.$item_control; + let drop_icon = "chevron-down"; + if (item.collapsible) { + let stroke_color = window + .getComputedStyle(document.body) + .getPropertyValue("--ink-gray-5"); + this.$drop_icon = $(`