Merge branch 'develop' into revert-34642-grid-css

This commit is contained in:
Ejaaz Khan 2025-11-17 14:16:15 +05:30 committed by GitHub
commit 4cf5f6312b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
206 changed files with 17680 additions and 2242 deletions

View file

@ -96,6 +96,7 @@
"hljs": true,
"Awesomplete": true,
"Sortable": true,
"gemoji": true,
"Showdown": true,
"Taggle": true,
"Gantt": true,

View file

@ -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",
],
},
});

View file

@ -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");

View file

@ -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")

View file

@ -1,7 +1,7 @@
context("Control Autocomplete", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
cy.wait(4000);
});

View file

@ -1,7 +1,7 @@
context("Control Barcode", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_barcode() {

View file

@ -1,7 +1,7 @@
context("Control Color", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_color() {

View file

@ -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 = {}) {

View file

@ -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");

View file

@ -1,7 +1,7 @@
context("Date Control", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function get_dialog(date_field_options) {

View file

@ -1,7 +1,7 @@
context("Date Range Control", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function get_dialog() {

View file

@ -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) {

View file

@ -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();

View file

@ -1,7 +1,7 @@
context("Control Float", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_float() {

View file

@ -1,7 +1,7 @@
context("Control Icon", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_icon() {

View file

@ -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");

View file

@ -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",

View file

@ -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

View file

@ -1,7 +1,7 @@
context("Control Rating", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_rating() {

View file

@ -1,7 +1,7 @@
context("Control Select", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_select() {

View file

@ -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();
});

View file

@ -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();

View file

@ -1,7 +1,7 @@
describe("Dashboard view", { scrollBehavior: false }, () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
it("should load", () => {

View file

@ -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", () => {

View file

@ -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");
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -1,7 +1,7 @@
context("Depends On", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")

View file

@ -4,7 +4,7 @@ context("FileUploader", () => {
});
beforeEach(() => {
cy.visit("/app");
cy.visit("/desk");
cy.wait(2000); // workspace can load async and clear active dialog
});

View file

@ -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) => {

View file

@ -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");
});

View file

@ -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", () => {

View file

@ -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);

View file

@ -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) => {

View file

@ -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();

View file

@ -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);

View file

@ -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");

View file

@ -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(

View file

@ -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");

View file

@ -1,7 +1,7 @@
context("List View", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")

View file

@ -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");

View file

@ -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");
});

View file

@ -1,7 +1,7 @@
context("MultiSelectDialog", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
const contact_template = {
doctype: "Contact",
first_name: "Test",

View file

@ -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");
});
});

View file

@ -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", () => {

View file

@ -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) => {

View file

@ -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");

View file

@ -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(

View file

@ -1,7 +1,7 @@
context("Rounding behaviour", () => {
before(() => {
cy.login();
cy.visit("/app/");
cy.visit("/desk/");
});
it("Commercial Rounding", () => {

View file

@ -1,4 +1,4 @@
const list_view = "/app/todo";
const list_view = "/desk/todo";
// test round trip with filter types

View file

@ -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");
});

View file

@ -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);

View file

@ -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);
});

View file

@ -1,7 +1,7 @@
context("Utils", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function run_util(name, ...args) {

View file

@ -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");
});
});

View file

@ -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();

View file

@ -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}");

View file

@ -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");
});

View file

@ -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"

View file

@ -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

View file

@ -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"
}

View file

@ -14,6 +14,7 @@ from frappe.core.doctype.installed_applications.installed_applications import (
)
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
@ -148,8 +149,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})

View file

@ -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,
]

View file

@ -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"):

View file

@ -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": []
}

View file

@ -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",

View file

@ -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()

View file

@ -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"
}

View file

@ -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");
},

View file

@ -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": []
}

View file

@ -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": []
}

View file

@ -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()

View file

@ -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")
);
}
},
});

View file

@ -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
}
}

View file

@ -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()

View file

@ -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

View file

@ -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"));
},
});

View file

@ -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": []
}

View file

@ -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

View file

@ -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

View file

@ -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) {
// },
// });

View file

@ -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": []
}

View file

@ -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

View file

@ -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

View file

@ -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": []
}

View file

@ -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

View file

@ -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()

View file

@ -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
});

View file

@ -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

View file

@ -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;
}
});
}
},
});

View file

@ -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": []
}

View file

@ -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)

View file

@ -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": []
}

View file

@ -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

View file

@ -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"
}

Some files were not shown because too many files have changed in this diff Show more