diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js new file mode 100644 index 0000000000..9701e54c5e --- /dev/null +++ b/cypress/integration/workspace.js @@ -0,0 +1,90 @@ +context('Workspace 2.0', () => { + before(() => { + cy.visit('/login'); + cy.login(); + cy.visit('/app/website'); + }); + + it('Navigate to page from sidebar', () => { + cy.visit('/app/build'); + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.sidebar-item-container[item-name="Settings"]').first().click(); + cy.location('pathname').should('eq', '/app/settings'); + }); + + it('Create Private Page', () => { + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.custom-actions button[data-label="Create%20Page"]').click(); + cy.fill_field('title', 'Test Private Page', 'Data'); + cy.fill_field('icon', 'edit', 'Icon'); + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + // check if sidebar item is added in pubic section + cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); + + cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); + + cy.wait(500); + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.standard-actions .btn-secondary[data-label=Customize]').click(); + }); + + it('Add New Block', () => { + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click(); + cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click(); + cy.get(".ce-block:last").find('h2').click({force: true}).type('Header'); + cy.get(".ce-block:last").find('.ce-header').should('exist'); + + cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click(); + cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click(); + cy.get(".ce-block:last").find('.ce-paragraph').click({force: true}).type('Paragraph text'); + cy.get(".ce-block:last").find('.ce-paragraph').should('exist'); + }); + + it('Delete A Block', () => { + cy.get(".ce-block:last").find('.delete-paragraph').click(); + cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist'); + }); + + it('Shrink and Expand A Block', () => { + cy.get(".ce-block:last").find('.tune-btn').click(); + cy.get('.ce-settings--opened .ce-shrink-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-11'); + cy.get('.ce-settings--opened .ce-shrink-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-10'); + cy.get('.ce-settings--opened .ce-shrink-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-9'); + cy.get('.ce-settings--opened .ce-expand-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-10'); + cy.get('.ce-settings--opened .ce-expand-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-11'); + cy.get('.ce-settings--opened .ce-expand-button').click(); + cy.get(".ce-block:last").should('have.class', 'col-12'); + }); + + it('Change Header Text Size', () => { + cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click(); + cy.get(".ce-block:last").find('.widget-head h3').should('exist'); + cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click(); + cy.get(".ce-block:last").find('.widget-head h4').should('exist'); + + cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); + }); + + it('Delete Private Page', () => { + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.standard-actions .btn-secondary[data-label=Customize]').click(); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click(); + cy.wait(300); + cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click(); + cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); + }); + +}); \ No newline at end of file diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json index 4a0835657b..f556be1c07 100644 --- a/frappe/automation/workspace/tools/tools.json +++ b/frappe/automation/workspace/tools/tools.json @@ -1,22 +1,27 @@ { - "category": "Administration", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]", "creation": "2020-03-02 14:53:24.980279", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "tool", "idx": 0, - "is_standard": 1, + "is_default": 0, + "is_standard": 0, "label": "Tools", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Tools", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -25,6 +30,7 @@ "hidden": 0, "is_query_report": 0, "label": "To Do", + "link_count": 0, "link_to": "ToDo", "link_type": "DocType", "onboard": 1, @@ -35,6 +41,7 @@ "hidden": 0, "is_query_report": 0, "label": "Calendar", + "link_count": 0, "link_to": "Event", "link_type": "DocType", "onboard": 1, @@ -45,6 +52,7 @@ "hidden": 0, "is_query_report": 0, "label": "Note", + "link_count": 0, "link_to": "Note", "link_type": "DocType", "onboard": 1, @@ -55,6 +63,7 @@ "hidden": 0, "is_query_report": 0, "label": "Files", + "link_count": 0, "link_to": "File", "link_type": "DocType", "onboard": 0, @@ -65,6 +74,7 @@ "hidden": 0, "is_query_report": 0, "label": "Activity", + "link_count": 0, "link_to": "activity", "link_type": "Page", "onboard": 0, @@ -74,6 +84,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -82,6 +93,7 @@ "hidden": 0, "is_query_report": 0, "label": "Newsletter", + "link_count": 0, "link_to": "Newsletter", "link_type": "DocType", "onboard": 1, @@ -92,6 +104,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email Group", + "link_count": 0, "link_to": "Email Group", "link_type": "DocType", "onboard": 0, @@ -101,6 +114,7 @@ "hidden": 0, "is_query_report": 0, "label": "Automation", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -109,6 +123,7 @@ "hidden": 0, "is_query_report": 0, "label": "Assignment Rule", + "link_count": 0, "link_to": "Assignment Rule", "link_type": "DocType", "onboard": 0, @@ -119,6 +134,7 @@ "hidden": 0, "is_query_report": 0, "label": "Milestone", + "link_count": 0, "link_to": "Milestone", "link_type": "DocType", "onboard": 0, @@ -129,6 +145,7 @@ "hidden": 0, "is_query_report": 0, "label": "Auto Repeat", + "link_count": 0, "link_to": "Auto Repeat", "link_type": "DocType", "onboard": 0, @@ -138,6 +155,7 @@ "hidden": 0, "is_query_report": 0, "label": "Event Streaming", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -146,6 +164,7 @@ "hidden": 0, "is_query_report": 0, "label": "Event Producer", + "link_count": 0, "link_to": "Event Producer", "link_type": "DocType", "onboard": 0, @@ -156,6 +175,7 @@ "hidden": 0, "is_query_report": 0, "label": "Event Consumer", + "link_count": 0, "link_to": "Event Consumer", "link_type": "DocType", "onboard": 0, @@ -166,6 +186,7 @@ "hidden": 0, "is_query_report": 0, "label": "Event Update Log", + "link_count": 0, "link_to": "Event Update Log", "link_type": "DocType", "onboard": 0, @@ -176,6 +197,7 @@ "hidden": 0, "is_query_report": 0, "label": "Event Sync Log", + "link_count": 0, "link_to": "Event Sync Log", "link_type": "DocType", "onboard": 0, @@ -186,19 +208,26 @@ "hidden": 0, "is_query_report": 0, "label": "Document Type Mapping", + "link_count": 0, "link_to": "Document Type Mapping", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2020-12-01 13:38:39.950350", + "modified": "2021-08-05 12:16:02.839180", "modified_by": "Administrator", "module": "Automation", "name": "Tools", + "onboarding": "", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 26, "shortcuts": [ { "label": "ToDo", @@ -225,5 +254,6 @@ "link_to": "Auto Repeat", "type": "DocType" } - ] + ], + "title": "Tools" } \ No newline at end of file diff --git a/frappe/boot.py b/frappe/boot.py index 0589e32ac8..c46709d3d7 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -105,8 +105,8 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_data(bootinfo): - from frappe.desk.desktop import get_desk_sidebar_items - bootinfo.allowed_workspaces = get_desk_sidebar_items() + from frappe.desk.desktop import get_wspace_sidebar_items + bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages') bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index aefda698b1..464052ba39 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -1,24 +1,28 @@ { "cards_label": "Elements", - "category": "Modules", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"DocType\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Workspace\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Report\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Elements\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Modules\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Models\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Views\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Scripting\", \"col\": 4}}]", "creation": "2021-01-02 10:51:16.579957", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "tool", "idx": 0, "is_default": 0, - "is_standard": 1, + "is_standard": 0, "label": "Build", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Modules", + "link_count": 0, "link_type": "DocType", "onboard": 0, "only_for": "", @@ -28,6 +32,7 @@ "hidden": 0, "is_query_report": 0, "label": "Module Def", + "link_count": 0, "link_to": "Module Def", "link_type": "DocType", "onboard": 0, @@ -38,6 +43,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workspace", + "link_count": 0, "link_to": "Workspace", "link_type": "DocType", "onboard": 0, @@ -48,6 +54,7 @@ "hidden": 0, "is_query_report": 0, "label": "Module Onboarding", + "link_count": 0, "link_to": "Module Onboarding", "link_type": "DocType", "onboard": 0, @@ -58,6 +65,7 @@ "hidden": 0, "is_query_report": 0, "label": "Block Module", + "link_count": 0, "link_to": "Block Module", "link_type": "DocType", "onboard": 0, @@ -68,6 +76,7 @@ "hidden": 0, "is_query_report": 0, "label": "Models", + "link_count": 0, "link_type": "DocType", "onboard": 0, "only_for": "", @@ -77,6 +86,7 @@ "hidden": 0, "is_query_report": 0, "label": "DocType", + "link_count": 0, "link_to": "DocType", "link_type": "DocType", "onboard": 0, @@ -87,6 +97,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workflow", + "link_count": 0, "link_to": "Workflow", "link_type": "DocType", "onboard": 0, @@ -97,6 +108,7 @@ "hidden": 0, "is_query_report": 0, "label": "Views", + "link_count": 0, "link_type": "DocType", "onboard": 0, "only_for": "", @@ -106,6 +118,7 @@ "hidden": 0, "is_query_report": 0, "label": "Report", + "link_count": 0, "link_to": "Report", "link_type": "DocType", "onboard": 0, @@ -116,6 +129,7 @@ "hidden": 0, "is_query_report": 0, "label": "Print Format", + "link_count": 0, "link_to": "Print Format", "link_type": "DocType", "onboard": 0, @@ -126,6 +140,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workspace", + "link_count": 0, "link_to": "Workspace", "link_type": "DocType", "onboard": 0, @@ -136,6 +151,7 @@ "hidden": 0, "is_query_report": 0, "label": "Dashboard", + "link_count": 0, "link_to": "Dashboard", "link_type": "DocType", "onboard": 0, @@ -146,6 +162,7 @@ "hidden": 0, "is_query_report": 0, "label": "Scripting", + "link_count": 0, "link_type": "DocType", "onboard": 0, "only_for": "", @@ -155,6 +172,7 @@ "hidden": 0, "is_query_report": 0, "label": "Server Script", + "link_count": 0, "link_to": "Server Script", "link_type": "DocType", "onboard": 0, @@ -165,6 +183,7 @@ "hidden": 0, "is_query_report": 0, "label": "Client Script", + "link_count": 0, "link_to": "Client Script", "link_type": "DocType", "onboard": 0, @@ -175,6 +194,7 @@ "hidden": 0, "is_query_report": 0, "label": "Scheduled Job Type", + "link_count": 0, "link_to": "Scheduled Job Type", "link_type": "DocType", "onboard": 0, @@ -182,13 +202,19 @@ "type": "Link" } ], - "modified": "2021-02-04 13:48:48.493146", + "modified": "2021-08-05 12:15:55.793022", "modified_by": "Administrator", "module": "Core", "name": "Build", + "onboarding": "", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 5, "shortcuts": [ { "doc_view": "", @@ -208,5 +234,6 @@ "link_to": "Report", "type": "DocType" } - ] + ], + "title": "Build" } \ No newline at end of file diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json index fb26b73cfc..93a6c81c90 100644 --- a/frappe/core/workspace/settings/settings.json +++ b/frappe/core/workspace/settings/settings.json @@ -1,22 +1,27 @@ { - "category": "Modules", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Settings\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"System Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Print Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Website Settings\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Data\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email / Notifications\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Website\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Core\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Printing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Workflow\", \"col\": 4}}]", "creation": "2020-03-02 15:09:40.527211", "developer_mode_only": 0, - "disable_user_customization": 1, + "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "setting", "idx": 0, - "is_standard": 1, + "is_default": 0, + "is_standard": 0, "label": "Settings", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Data", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -25,6 +30,7 @@ "hidden": 0, "is_query_report": 0, "label": "Import Data", + "link_count": 0, "link_to": "Data Import", "link_type": "DocType", "onboard": 0, @@ -35,6 +41,7 @@ "hidden": 0, "is_query_report": 0, "label": "Export Data", + "link_count": 0, "link_to": "Data Export", "link_type": "DocType", "onboard": 0, @@ -45,6 +52,7 @@ "hidden": 0, "is_query_report": 0, "label": "Bulk Update", + "link_count": 0, "link_to": "Bulk Update", "link_type": "DocType", "onboard": 0, @@ -55,6 +63,7 @@ "hidden": 0, "is_query_report": 0, "label": "Download Backups", + "link_count": 0, "link_to": "backups", "link_type": "Page", "onboard": 0, @@ -65,6 +74,7 @@ "hidden": 0, "is_query_report": 0, "label": "Deleted Documents", + "link_count": 0, "link_to": "Deleted Document", "link_type": "DocType", "onboard": 0, @@ -74,6 +84,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email / Notifications", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -82,6 +93,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email Account", + "link_count": 0, "link_to": "Email Account", "link_type": "DocType", "onboard": 0, @@ -92,6 +104,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email Domain", + "link_count": 0, "link_to": "Email Domain", "link_type": "DocType", "onboard": 0, @@ -102,6 +115,7 @@ "hidden": 0, "is_query_report": 0, "label": "Notification", + "link_count": 0, "link_to": "Notification", "link_type": "DocType", "onboard": 0, @@ -112,6 +126,7 @@ "hidden": 0, "is_query_report": 0, "label": "Email Template", + "link_count": 0, "link_to": "Email Template", "link_type": "DocType", "onboard": 0, @@ -122,6 +137,7 @@ "hidden": 0, "is_query_report": 0, "label": "Auto Email Report", + "link_count": 0, "link_to": "Auto Email Report", "link_type": "DocType", "onboard": 0, @@ -132,6 +148,7 @@ "hidden": 0, "is_query_report": 0, "label": "Newsletter", + "link_count": 0, "link_to": "Newsletter", "link_type": "DocType", "onboard": 0, @@ -142,6 +159,7 @@ "hidden": 0, "is_query_report": 0, "label": "Notification Settings", + "link_count": 0, "link_to": "Notification Settings", "link_type": "DocType", "onboard": 0, @@ -151,6 +169,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -159,6 +178,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Settings", + "link_count": 0, "link_to": "Website Settings", "link_type": "DocType", "onboard": 1, @@ -169,6 +189,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Theme", + "link_count": 0, "link_to": "Website Theme", "link_type": "DocType", "onboard": 1, @@ -179,6 +200,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Script", + "link_count": 0, "link_to": "Website Script", "link_type": "DocType", "onboard": 0, @@ -189,6 +211,7 @@ "hidden": 0, "is_query_report": 0, "label": "About Us Settings", + "link_count": 0, "link_to": "About Us Settings", "link_type": "DocType", "onboard": 0, @@ -199,6 +222,7 @@ "hidden": 0, "is_query_report": 0, "label": "Contact Us Settings", + "link_count": 0, "link_to": "Contact Us Settings", "link_type": "DocType", "onboard": 0, @@ -208,6 +232,7 @@ "hidden": 0, "is_query_report": 0, "label": "Core", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -216,6 +241,7 @@ "hidden": 0, "is_query_report": 0, "label": "System Settings", + "link_count": 0, "link_to": "System Settings", "link_type": "DocType", "onboard": 0, @@ -226,6 +252,7 @@ "hidden": 0, "is_query_report": 0, "label": "Error Log", + "link_count": 0, "link_to": "Error Log", "link_type": "DocType", "onboard": 0, @@ -236,6 +263,7 @@ "hidden": 0, "is_query_report": 0, "label": "Error Snapshot", + "link_count": 0, "link_to": "Error Snapshot", "link_type": "DocType", "onboard": 0, @@ -246,6 +274,7 @@ "hidden": 0, "is_query_report": 0, "label": "Domain Settings", + "link_count": 0, "link_to": "Domain Settings", "link_type": "DocType", "onboard": 0, @@ -255,6 +284,7 @@ "hidden": 0, "is_query_report": 0, "label": "Printing", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -263,6 +293,7 @@ "hidden": 0, "is_query_report": 0, "label": "Print Format Builder", + "link_count": 0, "link_to": "print-format-builder", "link_type": "Page", "onboard": 0, @@ -273,6 +304,7 @@ "hidden": 0, "is_query_report": 0, "label": "Print Settings", + "link_count": 0, "link_to": "Print Settings", "link_type": "DocType", "onboard": 0, @@ -283,6 +315,7 @@ "hidden": 0, "is_query_report": 0, "label": "Print Format", + "link_count": 0, "link_to": "Print Format", "link_type": "DocType", "onboard": 0, @@ -293,6 +326,7 @@ "hidden": 0, "is_query_report": 0, "label": "Print Style", + "link_count": 0, "link_to": "Print Style", "link_type": "DocType", "onboard": 0, @@ -302,6 +336,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workflow", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -310,6 +345,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workflow", + "link_count": 0, "link_to": "Workflow", "link_type": "DocType", "onboard": 0, @@ -320,6 +356,7 @@ "hidden": 0, "is_query_report": 0, "label": "Workflow State", + "link_count": 0, "link_to": "Workflow State", "link_type": "DocType", "onboard": 0, @@ -330,19 +367,26 @@ "hidden": 0, "is_query_report": 0, "label": "Workflow Action", + "link_count": 0, "link_to": "Workflow Action", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2020-12-01 13:38:40.235323", + "modified": "2021-08-05 12:16:03.456173", "modified_by": "Administrator", "module": "Core", "name": "Settings", + "onboarding": "", "owner": "Administrator", - "pin_to_bottom": 1, + "parent_page": "", + "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 29, "shortcuts": [ { "icon": "setting", @@ -363,5 +407,6 @@ "type": "DocType" } ], - "shortcuts_label": "Settings" + "shortcuts_label": "Settings", + "title": "Settings" } \ No newline at end of file diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json index ba82461b57..09a835ea2c 100644 --- a/frappe/core/workspace/users/users.json +++ b/frappe/core/workspace/users/users.json @@ -1,23 +1,27 @@ { - "category": "Administration", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]", "creation": "2020-03-02 15:12:16.754449", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "users", "idx": 0, "is_default": 0, - "is_standard": 1, + "is_standard": 0, "label": "Users", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Users", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -26,6 +30,7 @@ "hidden": 0, "is_query_report": 0, "label": "User", + "link_count": 0, "link_to": "User", "link_type": "DocType", "onboard": 0, @@ -36,6 +41,7 @@ "hidden": 0, "is_query_report": 0, "label": "Role", + "link_count": 0, "link_to": "Role", "link_type": "DocType", "onboard": 0, @@ -46,6 +52,7 @@ "hidden": 0, "is_query_report": 0, "label": "Role Profile", + "link_count": 0, "link_to": "Role Profile", "link_type": "DocType", "onboard": 0, @@ -55,6 +62,7 @@ "hidden": 0, "is_query_report": 0, "label": "Logs", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -63,6 +71,7 @@ "hidden": 0, "is_query_report": 0, "label": "Activity Log", + "link_count": 0, "link_to": "Activity Log", "link_type": "DocType", "onboard": 0, @@ -73,6 +82,7 @@ "hidden": 0, "is_query_report": 0, "label": "Access Log", + "link_count": 0, "link_to": "Access Log", "link_type": "DocType", "onboard": 0, @@ -82,6 +92,7 @@ "hidden": 0, "is_query_report": 0, "label": "Permissions", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -90,6 +101,7 @@ "hidden": 0, "is_query_report": 0, "label": "Role Permissions Manager", + "link_count": 0, "link_to": "permission-manager", "link_type": "Page", "onboard": 0, @@ -100,6 +112,7 @@ "hidden": 0, "is_query_report": 0, "label": "User Permissions", + "link_count": 0, "link_to": "User Permission", "link_type": "DocType", "onboard": 0, @@ -110,6 +123,7 @@ "hidden": 0, "is_query_report": 0, "label": "Role Permission for Page and Report", + "link_count": 0, "link_to": "Role Permission for Page and Report", "link_type": "DocType", "onboard": 0, @@ -120,6 +134,7 @@ "hidden": 0, "is_query_report": 1, "label": "Permitted Documents For User", + "link_count": 0, "link_to": "Permitted Documents For User", "link_type": "Report", "onboard": 0, @@ -130,19 +145,26 @@ "hidden": 0, "is_query_report": 0, "label": "Document Share Report", + "link_count": 0, "link_to": "Document Share Report", "link_type": "Report", "onboard": 0, "type": "Link" } ], - "modified": "2021-03-25 23:02:34.582569", + "modified": "2021-08-05 12:16:03.010204", "modified_by": "Administrator", "module": "Core", "name": "Users", + "onboarding": "", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 27, "shortcuts": [ { "label": "User", @@ -170,5 +192,6 @@ "link_to": "User Type", "type": "DocType" } - ] + ], + "title": "Users" } \ No newline at end of file diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json index cdc3b73366..136b1a57eb 100644 --- a/frappe/custom/workspace/customization/customization.json +++ b/frappe/custom/workspace/customization/customization.json @@ -1,23 +1,27 @@ { - "category": "Administration", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customize Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Custom Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Client Script\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Server Script\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Dashboards\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Form Customization\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other\", \"col\": 4}}]", "creation": "2020-03-02 15:15:03.839594", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "customization", "idx": 0, "is_default": 0, - "is_standard": 1, + "is_standard": 0, "label": "Customization", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Dashboards", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -26,6 +30,7 @@ "hidden": 0, "is_query_report": 0, "label": "Dashboard", + "link_count": 0, "link_to": "Dashboard", "link_type": "DocType", "onboard": 0, @@ -36,6 +41,7 @@ "hidden": 0, "is_query_report": 0, "label": "Dashboard Chart", + "link_count": 0, "link_to": "Dashboard Chart", "link_type": "DocType", "onboard": 0, @@ -46,6 +52,7 @@ "hidden": 0, "is_query_report": 0, "label": "Dashboard Chart Source", + "link_count": 0, "link_to": "Dashboard Chart Source", "link_type": "DocType", "onboard": 0, @@ -55,6 +62,7 @@ "hidden": 0, "is_query_report": 0, "label": "Form Customization", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -63,6 +71,7 @@ "hidden": 0, "is_query_report": 0, "label": "Customize Form", + "link_count": 0, "link_to": "Customize Form", "link_type": "DocType", "onboard": 0, @@ -73,6 +82,7 @@ "hidden": 0, "is_query_report": 0, "label": "Custom Field", + "link_count": 0, "link_to": "Custom Field", "link_type": "DocType", "onboard": 0, @@ -83,6 +93,7 @@ "hidden": 0, "is_query_report": 0, "label": "Client Script", + "link_count": 0, "link_to": "Client Script", "link_type": "DocType", "onboard": 0, @@ -93,6 +104,7 @@ "hidden": 0, "is_query_report": 0, "label": "DocType", + "link_count": 0, "link_to": "DocType", "link_type": "DocType", "onboard": 0, @@ -102,6 +114,7 @@ "hidden": 0, "is_query_report": 0, "label": "Other", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -110,19 +123,26 @@ "hidden": 0, "is_query_report": 0, "label": "Custom Translations", + "link_count": 0, "link_to": "Translation", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2021-02-04 13:50:35.750463", + "modified": "2021-08-05 12:15:57.486112", "modified_by": "Administrator", "module": "Custom", "name": "Customization", + "onboarding": "", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 8, "shortcuts": [ { "label": "Customize Form", @@ -145,5 +165,6 @@ "link_to": "Server Script", "type": "DocType" } - ] + ], + "title": "Customization" } \ No newline at end of file diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ca53e6cba4..27b985e429 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -6,6 +6,7 @@ import frappe from json import loads, dumps from frappe import _, DoesNotExistError, ValidationError, _dict from frappe.boot import get_allowed_pages, get_allowed_reports +from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles from functools import wraps from frappe.cache_manager import ( build_domain_restriced_doctype_cache, @@ -27,18 +28,21 @@ def handle_not_exist(fn): class Workspace: - def __init__(self, page_name, minimal=False): - self.page_name = page_name + def __init__(self, page, minimal=False): + self.page_name = page.get('name') + self.page_title = page.get('title') + self.public_page = page.get('public') self.extended_links = [] self.extended_charts = [] self.extended_shortcuts = [] + self.workspace_manager = "Workspace Manager" in frappe.get_roles() self.user = frappe.get_user() self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules) - self.doc = self.get_page_for_user() + self.doc = frappe.get_cached_doc("Workspace", self.page_name) - if self.doc.module and self.doc.module not in self.allowed_modules: + if self.doc and self.doc.module and self.doc.module not in self.allowed_modules and not self.workspace_manager: raise frappe.PermissionError self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items) @@ -47,16 +51,17 @@ class Workspace: self.allowed_reports = get_allowed_reports(cache=True) if not minimal: - self.onboarding_doc = self.get_onboarding_doc() - self.onboarding = None + if self.doc.content: + self.onboarding_list = [x['data']['onboarding_name'] for x in loads(self.doc.content) if x['type'] == 'onboarding'] + self.onboardings = [] self.table_counts = get_table_with_counts() self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() def is_page_allowed(self): - cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links - shortcuts = self.doc.shortcuts + self.extended_shortcuts + cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + shortcuts = self.doc.shortcuts for section in cards: links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links') @@ -74,8 +79,28 @@ class Workspace: if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): return True + if not shortcuts and not self.doc.links: + return True + return False + def is_permitted(self): + """Returns 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.doc.name})] + + custom_roles = get_custom_allowed_roles('page', self.doc.name) + allowed.extend(custom_roles) + + if not allowed: + return True + + roles = frappe.get_roles() + + if has_common(roles, allowed): + return True + def get_cached(self, cache_key, fallback_fn): _cache = frappe.cache() @@ -101,39 +126,18 @@ class Workspace: return self.user.allow_modules - def get_page_for_user(self): - filters = { - 'extends': self.page_name, - 'for_user': frappe.session.user - } - user_pages = frappe.get_all("Workspace", filters=filters, limit=1) - if user_pages: - return frappe.get_cached_doc("Workspace", user_pages[0]) - - filters = { - 'extends_another_page': 1, - 'extends': self.page_name, - 'is_default': 1 - } - default_page = frappe.get_all("Workspace", filters=filters, limit=1) - if default_page: - return frappe.get_cached_doc("Workspace", default_page[0]) - - self.get_pages_to_extend() - return frappe.get_cached_doc("Workspace", self.page_name) - - def get_onboarding_doc(self): + def get_onboarding_doc(self, onboarding): # Check if onboarding is enabled if not frappe.get_system_settings("enable_onboarding"): return None - if not self.doc.onboarding: + if not self.onboarding_list: return None - if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"): + if frappe.db.get_value("Module Onboarding", onboarding, "is_complete"): return None - doc = frappe.get_doc("Module Onboarding", self.doc.onboarding) + doc = frappe.get_doc("Module Onboarding", onboarding) # Check if user is allowed allowed_roles = set(doc.get_allowed_roles()) @@ -197,14 +201,9 @@ class Workspace: 'items': self.get_shortcuts() } - if self.onboarding_doc: - self.onboarding = { - 'label': _(self.onboarding_doc.title), - 'subtitle': _(self.onboarding_doc.subtitle), - 'success': _(self.onboarding_doc.success_message), - 'docs_url': self.onboarding_doc.documentation_url, - 'items': self.get_onboarding_steps() - } + self.onboardings = { + 'items': self.get_onboardings() + } def _doctype_contains_a_record(self, name): exists = self.table_counts.get(name, False) @@ -333,9 +332,26 @@ class Workspace: return items @handle_not_exist - def get_onboarding_steps(self): + def get_onboardings(self): + if self.onboarding_list: + for onboarding in self.onboarding_list: + onboarding_doc = self.get_onboarding_doc(onboarding) + if onboarding_doc: + item = { + 'label': _(onboarding), + 'title': _(onboarding_doc.title), + 'subtitle': _(onboarding_doc.subtitle), + 'success': _(onboarding_doc.success_message), + 'docs_url': onboarding_doc.documentation_url, + 'items': self.get_onboarding_steps(onboarding_doc) + } + self.onboardings.append(item) + return self.onboardings + + @handle_not_exist + def get_onboarding_steps(self, onboarding_doc): steps = [] - for doc in self.onboarding_doc.get_steps(): + for doc in onboarding_doc.get_steps(): step = doc.as_dict().copy() step.label = _(doc.title) if step.action == "Create Entry": @@ -358,13 +374,13 @@ def get_desktop_page(page): dict: dictionary of cards, charts and shortcuts to be displayed on website """ try: - wspace = Workspace(page) + wspace = Workspace(loads(page)) wspace.build_workspace() return { 'charts': wspace.charts, 'shortcuts': wspace.shortcuts, 'cards': wspace.cards, - 'onboarding': wspace.onboarding, + 'onboardings': wspace.onboardings, 'allow_customization': not wspace.doc.disable_user_customization } except DoesNotExistError: @@ -372,39 +388,45 @@ def get_desktop_page(page): return {} @frappe.whitelist() -def get_desk_sidebar_items(): +def get_wspace_sidebar_items(): """Get list of sidebar items for desk""" + has_access = "Workspace Manager" in frappe.get_roles() # don't get domain restricted pages blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + blocked_modules.append('Dummy Module') filters = { 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', 'module': ['not in', blocked_modules] } - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' + if has_access: + filters = [] - # pages sorted based on pinned to top and then by name - order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"], - filters=filters, order_by=order_by, ignore_permissions=True) + # pages sorted based on sequence id + order_by = "sequence_id asc" + fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"] + all_pages = frappe.get_all("Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True) pages = [] + private_pages = [] # Filter Page based on Permission for page in all_pages: try: - wspace = Workspace(page.get('name'), True) - if wspace.is_page_allowed(): - pages.append(page) + wspace = Workspace(page) + if wspace.is_permitted() and wspace.is_page_allowed() or has_access: + if page.public: + pages.append(page) + elif page.for_user == frappe.session.user: + private_pages.append(page) page['label'] = _(page.get('name')) except frappe.PermissionError: pass + if private_pages: + pages.extend(private_pages) - return pages + return {'pages': pages, 'has_access': has_access} def get_table_with_counts(): counts = frappe.cache().get_value("information_schema:counts") @@ -471,7 +493,7 @@ def get_custom_workspace_for_user(page): """ filters = { 'extends': page, - 'for_user': frappe.session.user + 'for_user': frappe.session.user, } pages = frappe.get_list("Workspace", filters=filters) if pages: @@ -481,7 +503,6 @@ def get_custom_workspace_for_user(page): doc.for_user = frappe.session.user return doc - @frappe.whitelist() def save_customization(page, config): """Save customizations as a separate doctype in Workspace per user @@ -540,6 +561,80 @@ def save_customization(page, config): return True +def save_new_widget(doc, page, blocks, new_widgets): + + widgets = _dict(loads(new_widgets)) + + if widgets.chart: + doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) + if widgets.shortcut: + doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) + if widgets.card: + doc.build_links_table_from_card(widgets.card) + + # remove duplicate and unwanted widgets + if widgets: + clean_up(doc, blocks) + + try: + doc.save(ignore_permissions=True) + except (ValidationError, TypeError) as e: + # Create a json string to log + json_config = dumps(widgets, sort_keys=True, indent=4) + + # Error log body + log = \ + """ + page: {0} + config: {1} + exception: {2} + """.format(page, json_config, e) + frappe.log_error(log, _("Could not save customization")) + return False + + return True +def clean_up(original_page, blocks): + page_widgets = {} + + for wid in ['shortcut', 'card', 'chart']: + # get list of widget's name from blocks + page_widgets[wid] = [x['data'][wid + '_name'] for x in loads(blocks) if x['type'] == wid] + + # shortcut & chart cleanup + for wid in ['shortcut', 'chart']: + updated_widgets = [] + original_page.get(wid+'s').reverse() + + for w in original_page.get(wid+'s'): + if w.label in page_widgets[wid] and w.label not in [x.label for x in updated_widgets]: + updated_widgets.append(w) + original_page.set(wid+'s', updated_widgets) + + # card cleanup + for i, v in enumerate(original_page.links): + if v.type == 'Card Break' and v.label not in page_widgets['card']: + del original_page.links[i : i+v.link_count+1] + +def new_widget(config, doctype, parentfield): + if not config: + return [] + prepare_widget_list = [] + for idx, widget in enumerate(config): + # Some cleanup + widget.pop("name", None) + + # New Doc + doc = frappe.new_doc(doctype) + doc.update(widget) + + # Manually Set IDX + doc.idx = idx + 1 + + # Set Parent Field + doc.parentfield = parentfield + + prepare_widget_list.append(doc) + return prepare_widget_list def prepare_widget(config, doctype, parentfield): """Create widget child table entries with parent details diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index 10bd8926ce..2336ff52f8 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -2,10 +2,26 @@ # Copyright (c) 2020, Frappe Technologies and contributors # For license information, please see license.txt -# import frappe +import frappe +from frappe import _ +import json from frappe.model.document import Document class OnboardingStep(Document): def before_export(self, doc): doc.is_complete = 0 doc.is_skipped = 0 + + +@frappe.whitelist() +def get_onboarding_steps(ob_steps): + steps = [] + for s in json.loads(ob_steps): + doc = frappe.get_doc('Onboarding Step', s.get('step')) + step = doc.as_dict().copy() + step.label = _(doc.title) + if step.action == "Create Entry": + step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True) + steps.append(step) + + return steps diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 386267b699..e2ae38faf1 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -8,8 +8,11 @@ "engine": "InnoDB", "field_order": [ "label", + "title", + "sequence_id", "for_user", "extends", + "parent_page", "module", "category", "icon", @@ -24,6 +27,9 @@ "pin_to_top", "pin_to_bottom", "hide_custom", + "public", + "content_section", + "content", "section_break_2", "charts_label", "charts", @@ -32,7 +38,8 @@ "shortcuts", "section_break_18", "cards_label", - "links" + "links", + "roles" ], "fields": [ { @@ -199,7 +206,7 @@ }, { "fieldname": "icon", - "fieldtype": "Data", + "fieldtype": "Icon", "label": "Icon" }, { @@ -209,16 +216,53 @@ "options": "Workspace Link" }, { - "default": "0", - "depends_on": "extends_another_page", - "description": "Sets the current page as default for all users", - "fieldname": "is_default", - "fieldtype": "Check", - "label": "Is Default" - } + "default": "0", + "depends_on": "extends_another_page", + "description": "Sets the current page as default for all users", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "default": "0", + "fieldname": "public", + "fieldtype": "Check", + "label": "Public" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "parent_page", + "fieldtype": "Data", + "label": "Parent Page" + }, + { + "fieldname": "content_section", + "fieldtype": "Section Break", + "label": "Content" + }, + { + "fieldname": "content", + "fieldtype": "Long Text", + "label": "Content" + }, + { + "fieldname": "sequence_id", + "fieldtype": "Int", + "label": "Sequence Id" + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + } ], "links": [], - "modified": "2021-01-21 12:09:36.156614", + "modified": "2021-08-05 11:49:09.028243", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", @@ -232,7 +276,7 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Workspace Manager", "share": 1, "write": 1 }, @@ -248,4 +292,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 41b0227f2a..0821ae03c4 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.modules.export_file import export_to_files from frappe.model.document import Document +from frappe.desk.desktop import save_new_widget from frappe.desk.utils import validate_route_conflict from json import loads @@ -27,7 +28,7 @@ class Workspace(Document): if disable_saving_as_standard(): return - if frappe.conf.developer_mode and self.is_standard: + if frappe.conf.developer_mode and self.module and self.public: export_to_files(record_list=[['Workspace', self.name]], record_module=self.module) @staticmethod @@ -98,6 +99,37 @@ class Workspace(Document): "is_query_report": link.get('is_query_report') }) + def build_links_table_from_card(self, config): + + for idx, card in enumerate(config): + links = loads(card.get('links')) + + # remove duplicate before adding + for idx, link in enumerate(self.links): + if link.label == card.get('label') and link.type == 'Card Break': + del self.links[idx : idx + link.link_count + 1] + + self.append('links', { + "label": card.get('label'), + "type": "Card Break", + "icon": card.get('icon'), + "hidden": card.get('hidden') or False, + "link_count": card.get('link_count'), + "idx": 1 if not self.links else self.links[-1].idx + 1 + }) + + for link in links: + self.append('links', { + "label": link.get('label'), + "type": "Link", + "link_type": link.get('link_type'), + "link_to": link.get('link_to'), + "onboard": link.get('onboard'), + "only_for": link.get('only_for'), + "dependencies": link.get('dependencies'), + "is_query_report": link.get('is_query_report'), + "idx": self.links[-1].idx + 1 + }) def disable_saving_as_standard(): return frappe.flags.in_install or \ @@ -123,3 +155,84 @@ def get_link_type(key): def get_report_type(report): report_type = frappe.get_value("Report", report, "report_type") return report_type in ["Query Report", "Script Report", "Custom Report"] + + +@frappe.whitelist() +def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save): + save = frappe.parse_json(save) + public = frappe.parse_json(public) + if save: + doc = frappe.new_doc('Workspace') + doc.title = title + doc.icon = icon + doc.content = blocks + doc.parent_page = parent + + if public: + doc.label = title + doc.public = 1 + else: + doc.label = title + "-" + frappe.session.user + doc.for_user = frappe.session.user + doc.save(ignore_permissions=True) + else: + if public: + filters = { + 'public': public, + 'label': title + } + else: + filters = { + 'for_user': frappe.session.user, + 'label': title + "-" + frappe.session.user + } + pages = frappe.get_list("Workspace", filters=filters) + if pages: + doc = frappe.get_doc("Workspace", pages[0]) + + doc.content = blocks + doc.save(ignore_permissions=True) + + if loads(new_widgets): + save_new_widget(doc, title, blocks, new_widgets) + + if loads(sb_public_items) or loads(sb_private_items): + sort_pages(loads(sb_public_items), loads(sb_private_items)) + + if loads(deleted_pages): + return delete_pages(loads(deleted_pages)) + + return {"name": title, "public": public} + +def delete_pages(deleted_pages): + for page in deleted_pages: + if page.get("public") and "Workspace Manager" not in frappe.get_roles(): + return {"name": page.get("title"), "public": 1} + + if frappe.db.exists("Workspace", page.get("name")): + frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) + + return {"name": "Home", "public": 1} + +def sort_pages(sb_public_items, sb_private_items): + wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) + wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user}) + + if sb_private_items: + sort_page(wspace_private_pages, sb_private_items) + + if sb_public_items and "Workspace Manager" in frappe.get_roles(): + sort_page(wspace_public_pages, sb_public_items) + +def sort_page(wspace_pages, pages): + for seq, d in enumerate(pages): + for page in wspace_pages: + if page.title == d.get('title'): + doc = frappe.get_doc('Workspace', page.name) + doc.sequence_id = seq + 1 + doc.parent_page = d.get('parent_page') or "" + doc.save(ignore_permissions=True) + break + +def get_page_list(fields, filters): + return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc') diff --git a/frappe/desk/doctype/workspace_link/workspace_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json index 53dadad83d..a7b217be9e 100644 --- a/frappe/desk/doctype/workspace_link/workspace_link.json +++ b/frappe/desk/doctype/workspace_link/workspace_link.json @@ -8,15 +8,16 @@ "type", "label", "icon", - "only_for", "hidden", "link_details_section", "link_type", "link_to", "column_break_7", "dependencies", + "only_for", "onboard", - "is_query_report" + "is_query_report", + "link_count" ], "fields": [ { @@ -99,12 +100,19 @@ "fieldname": "is_query_report", "fieldtype": "Check", "label": "Is Query Report" + }, + { + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "link_count", + "fieldtype": "Int", + "hidden": 1, + "label": "Link Count" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-13 13:10:18.128512", + "modified": "2021-06-01 11:23:28.990593", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Link", diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json index db96304207..4167858db2 100644 --- a/frappe/integrations/workspace/integrations/integrations.json +++ b/frappe/integrations/workspace/integrations/integrations.json @@ -1,22 +1,27 @@ { - "category": "Administration", + "category": "", "charts": [], + "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Backup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Google Services\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Authentication\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", "creation": "2020-03-02 15:16:18.714190", "developer_mode_only": 0, - "disable_user_customization": 1, + "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "integration", "idx": 0, - "is_standard": 1, + "is_default": 0, + "is_standard": 0, "label": "Integrations", "links": [ { "hidden": 0, "is_query_report": 0, "label": "Backup", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -25,6 +30,7 @@ "hidden": 0, "is_query_report": 0, "label": "Dropbox Settings", + "link_count": 0, "link_to": "Dropbox Settings", "link_type": "DocType", "onboard": 0, @@ -35,6 +41,7 @@ "hidden": 0, "is_query_report": 0, "label": "S3 Backup Settings", + "link_count": 0, "link_to": "S3 Backup Settings", "link_type": "DocType", "onboard": 0, @@ -45,6 +52,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Drive", + "link_count": 0, "link_to": "Google Drive", "link_type": "DocType", "onboard": 0, @@ -54,6 +62,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Services", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -62,6 +71,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Settings", + "link_count": 0, "link_to": "Google Settings", "link_type": "DocType", "onboard": 0, @@ -72,6 +82,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Contacts", + "link_count": 0, "link_to": "Google Contacts", "link_type": "DocType", "onboard": 0, @@ -82,6 +93,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Calendar", + "link_count": 0, "link_to": "Google Calendar", "link_type": "DocType", "onboard": 0, @@ -92,6 +104,7 @@ "hidden": 0, "is_query_report": 0, "label": "Google Drive", + "link_count": 0, "link_to": "Google Drive", "link_type": "DocType", "onboard": 0, @@ -101,6 +114,7 @@ "hidden": 0, "is_query_report": 0, "label": "Authentication", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -109,6 +123,7 @@ "hidden": 0, "is_query_report": 0, "label": "Social Login Key", + "link_count": 0, "link_to": "Social Login Key", "link_type": "DocType", "onboard": 0, @@ -119,6 +134,7 @@ "hidden": 0, "is_query_report": 0, "label": "LDAP Settings", + "link_count": 0, "link_to": "LDAP Settings", "link_type": "DocType", "onboard": 0, @@ -129,6 +145,7 @@ "hidden": 0, "is_query_report": 0, "label": "OAuth Client", + "link_count": 0, "link_to": "OAuth Client", "link_type": "DocType", "onboard": 0, @@ -139,6 +156,7 @@ "hidden": 0, "is_query_report": 0, "label": "OAuth Provider Settings", + "link_count": 0, "link_to": "OAuth Provider Settings", "link_type": "DocType", "onboard": 0, @@ -148,6 +166,7 @@ "hidden": 0, "is_query_report": 0, "label": "Payments", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -156,6 +175,7 @@ "hidden": 0, "is_query_report": 0, "label": "Braintree Settings", + "link_count": 0, "link_to": "Braintree Settings", "link_type": "DocType", "onboard": 0, @@ -166,6 +186,7 @@ "hidden": 0, "is_query_report": 0, "label": "PayPal Settings", + "link_count": 0, "link_to": "PayPal Settings", "link_type": "DocType", "onboard": 0, @@ -176,6 +197,7 @@ "hidden": 0, "is_query_report": 0, "label": "Razorpay Settings", + "link_count": 0, "link_to": "Razorpay Settings", "link_type": "DocType", "onboard": 0, @@ -186,6 +208,7 @@ "hidden": 0, "is_query_report": 0, "label": "Stripe Settings", + "link_count": 0, "link_to": "Stripe Settings", "link_type": "DocType", "onboard": 0, @@ -196,6 +219,7 @@ "hidden": 0, "is_query_report": 0, "label": "Paytm Settings", + "link_count": 0, "link_to": "Paytm Settings", "link_type": "DocType", "onboard": 0, @@ -205,6 +229,7 @@ "hidden": 0, "is_query_report": 0, "label": "Settings", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -213,6 +238,7 @@ "hidden": 0, "is_query_report": 0, "label": "Webhook", + "link_count": 0, "link_to": "Webhook", "link_type": "DocType", "onboard": 0, @@ -223,38 +249,37 @@ "hidden": 0, "is_query_report": 0, "label": "Slack Webhook URL", + "link_count": 0, "link_to": "Slack Webhook URL", "link_type": "DocType", "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Twilio Settings", - "link_to": "Twilio Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, "is_query_report": 0, "label": "SMS Settings", + "link_count": 0, "link_to": "SMS Settings", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2020-12-01 13:38:39.706680", + "modified": "2021-08-05 12:16:00.355267", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", + "onboarding": "", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, - "shortcuts": [] + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 15, + "shortcuts": [], + "title": "Integrations" } \ No newline at end of file diff --git a/frappe/patches.txt b/frappe/patches.txt index 989b13e049..87919b0247 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -182,3 +182,4 @@ frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents +frappe.patches.v14_0.update_workspace2 diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py new file mode 100644 index 0000000000..2d7eb4cc76 --- /dev/null +++ b/frappe/patches/v14_0/update_workspace2.py @@ -0,0 +1,69 @@ +import frappe +import json +from frappe import _ + +def execute(): + frappe.reload_doc('desk', 'doctype', 'workspace', force=True) + order_by = "pin_to_top desc, pin_to_bottom asc, name asc" + for seq, wspace in enumerate(frappe.get_all('Workspace', order_by=order_by)): + doc = frappe.get_doc('Workspace', wspace.name) + content = create_content(doc) + update_wspace(doc, seq, content) + frappe.db.commit() + +def create_content(doc): + content = [] + if doc.onboarding: + content.append({"type":"onboarding","data":{"onboarding_name":doc.onboarding,"col":12}}) + if doc.charts: + invalid_links = [] + for c in doc.charts: + if c.get_invalid_links()[0]: + invalid_links.append(c) + else: + content.append({"type":"chart","data":{"chart_name":c.label,"col":12}}) + for l in invalid_links: + del doc.charts[doc.charts.index(l)] + if doc.shortcuts: + invalid_links = [] + if doc.charts: + content.append({"type":"spacer","data":{"col":12}}) + content.append({"type":"header","data":{"text":doc.shortcuts_label or _("Your Shortcuts"),"level":4,"col":12}}) + for s in doc.shortcuts: + if s.get_invalid_links()[0]: + invalid_links.append(s) + else: + content.append({"type":"shortcut","data":{"shortcut_name":s.label,"col":4}}) + for l in invalid_links: + del doc.shortcuts[doc.shortcuts.index(l)] + if doc.links: + invalid_links = [] + content.append({"type":"spacer","data":{"col":12}}) + content.append({"type":"header","data":{"text":doc.cards_label or _("Reports & Masters"),"level":4,"col":12}}) + for l in doc.links: + if l.type == 'Card Break': + content.append({"type":"card","data":{"card_name":l.label,"col":4}}) + if l.get_invalid_links()[0]: + invalid_links.append(l) + for l in invalid_links: + del doc.links[doc.links.index(l)] + return content + +def update_wspace(doc, seq, content): + if not doc.is_standard and not doc.public: + doc.sequence_id = seq + 1 + doc.content = json.dumps(content) + doc.public = 0 + doc.title = doc.extends + doc.extends = '' + doc.category = '' + doc.onboarding = '' + doc.extends_another_page = 0 + doc.is_default = 0 + doc.is_standard = 0 + doc.developer_mode_only = 0 + doc.disable_user_customization = 0 + doc.pin_to_top = 0 + doc.pin_to_bottom = 0 + doc.hide_custom = 0 + doc.save(ignore_permissions=True) \ No newline at end of file diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index 0e8e24b768..f216374526 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -57,6 +57,9 @@ + + + diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index c4ecf67c4f..294ac013fb 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -82,6 +82,7 @@ import "./frappe/ui/toolbar/toolbar.js"; import "./frappe/views/communication.js"; import "./frappe/views/translation_manager.js"; import "./frappe/views/workspace/workspace.js"; +import "./frappe/views/workspace/blocks/index.js"; import "./frappe/widgets/widget_group.js"; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 9d106f46f4..810b6a404a 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -283,7 +283,7 @@ frappe.Application = class Application { frappe.workspaces = {}; for (let page of frappe.boot.allowed_workspaces || []) { frappe.modules[page.module]=page; - frappe.workspaces[frappe.router.slug(page.name)] = page; + frappe.workspaces[frappe.router.slug(page.title)] = page; } if (!frappe.workspaces['home']) { // default workspace is settings for Frappe diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index d0f93882fb..4360f3e887 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -118,6 +118,7 @@ frappe.router = { convert_to_standard_route(route) { // /app/settings = ["Workspaces", "Settings"] + // /app/private/settings = ["Workspaces", "private", "Settings"] // /app/user = ["List", "User"] // /app/user/view/report = ["List", "User", "Report"] // /app/user/view/tree = ["Tree", "User"] @@ -126,8 +127,11 @@ frappe.router = { // /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] if (frappe.workspaces[route[0]]) { - // workspace - route = ['Workspaces', frappe.workspaces[route[0]].name]; + // public workspace + route = ['Workspaces', frappe.workspaces[route[0]].title]; + } else if (frappe.workspaces[route[1]]) { + // private workspace + route = ['Workspaces', 'private', frappe.workspaces[route[1]].title]; } else if (this.routes[route[0]]) { // route route = this.set_doctype_route(route); @@ -136,6 +140,11 @@ frappe.router = { return route; }, + doctype_route_exist(route) { + route = this.get_sub_path_string(route).split('/'); + return this.routes[route[0]]; + }, + set_doctype_route(route) { let doctype_route = this.routes[route[0]]; // doctype route diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 65635ec1dd..21841296dc 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1325,6 +1325,19 @@ Object.assign(frappe.utils, { return clipboard_data.getData('Text'); }, + add_custom_button(html, action, class_name = "", title="", btn_type, wrapper, prepend) { + if (!btn_type) btn_type = 'btn-secondary'; + let button = $( + `` + ); + button.click(event => { + event.stopPropagation(); + action && action(event); + }); + !prepend && button.appendTo(wrapper); + prepend && wrapper.prepend(button); + }, + sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } diff --git a/frappe/public/js/frappe/views/workspace/blocks/block.js b/frappe/public/js/frappe/views/workspace/blocks/block.js new file mode 100644 index 0000000000..aed3c2f727 --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/block.js @@ -0,0 +1,115 @@ +import get_dialog_constructor from "../../../widgets/widget_dialog.js"; + +export default class Block { + constructor(opts) { + Object.assign(this, opts); + } + + make(block, block_name, widget_type = block) { + let block_data = this.config.page_data[block+'s'].items.find(obj => { + return obj.label == block_name; + }); + if (!block_data) return false; + this.wrapper.innerHTML = ''; + block_data.in_customize_mode = !this.readOnly; + this.block_widget = new frappe.widget.SingleWidgetGroup({ + container: this.wrapper, + type: widget_type, + class_name: block == 'chart' ? 'widget-charts' : '', + options: this.options, + widgets: block_data, + api: this.api, + block: this.block + }); + this.wrapper.setAttribute(block+'_name', block_name); + if (!this.readOnly) { + this.block_widget.customize(); + } + return true; + } + + rendered() { + var e = this.wrapper.closest('.ce-block'); + e.classList.add("col-" + this.get_col()); + } + + new(block, widget_type = block) { + const dialog_class = get_dialog_constructor(widget_type); + let block_name = block+'_name'; + this.dialog = new dialog_class({ + label: this.label, + type: widget_type, + primary_action: (widget) => { + widget.in_customize_mode = 1; + this.block_widget = frappe.widget.make_widget({ + ...widget, + widget_type: widget_type, + container: this.wrapper, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.block_widget) + } + }); + this.block_widget.customize(this.options); + this.wrapper.setAttribute(block_name, this.block_widget.label); + this.new_block_widget = this.block_widget.get_config(); + this.add_tune_button(); + }, + }); + + if (!this.readOnly && this.data && !this.data[block_name]) { + this.dialog.make(); + } + } + + on_edit(block_obj) { + let block_name = block_obj.edit_dialog.type+'_name'; + if (block_obj.edit_dialog.type == 'links') { + block_name = 'card_name'; + } + let block = block_obj.get_config(); + this.block_widget.widgets = block; + this.wrapper.setAttribute(block_name, block.label); + this.new_block_widget = block_obj.get_config(); + } + + add_tune_button() { + let $widget_control = $(this.wrapper).find('.widget-control'); + frappe.utils.add_custom_button( + frappe.utils.icon('dot-horizontal', 'xs'), + (event) => { + let evn = event; + !$('.ce-settings.ce-settings--opened').length && + setTimeout(() => { + this.api.toolbar.toggleBlockSettings(); + var position = $(evn.target).offset(); + $('.ce-settings.ce-settings--opened').offset({ + top: position.top + 25, + left: position.left - 77 + }); + }, 50); + }, + "tune-btn", + `${__('Tune')}`, + null, + $widget_control, + true + ); + } + + get_col() { + let col = this.col || 12; + let class_name = "col-12"; + let wrapper = this.wrapper.closest('.ce-block'); + const col_class = new RegExp(/\bcol-.+?\b/, "g"); + if (wrapper && wrapper.className.match(col_class)) { + wrapper.classList.forEach(function (cn) { + cn.match(col_class) && (class_name = cn); + }); + let parts = class_name.split("-"); + col = parseInt(parts[1]); + } + return col; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js new file mode 100644 index 0000000000..975b32eea7 --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -0,0 +1,59 @@ +import Block from "./block.js"; +export default class Card extends Block { + static get toolbox() { + return { + title: 'Card', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.sections = {}; + this.col = this.data.col ? this.data.col : "12"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + }; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('card', 'links'); + + if (this.data && this.data.card_name) { + let has_data = this.make('card', this.data.card_name, 'links'); + if (!has_data) return; + } + + if (!this.readOnly) { + this.add_tune_button(); + } + + return this.wrapper; + } + + validate(savedData) { + if (!savedData.card_name) { + return false; + } + + return true; + } + + save(blockContent) { + return { + card_name: blockContent.getAttribute('card_name'), + col: this.get_col(), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js new file mode 100644 index 0000000000..e41063e6fc --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -0,0 +1,59 @@ +import Block from "./block.js"; +export default class Chart extends Block { + static get toolbox() { + return { + title: 'Chart', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.col = this.data.col ? this.data.col : "12"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + max_widget_count: 2, + }; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('chart'); + + if (this.data && this.data.chart_name) { + let has_data = this.make('chart', this.data.chart_name); + if (!has_data) return; + } + + if (!this.readOnly) { + this.add_tune_button(); + } + + return this.wrapper; + } + + validate(savedData) { + if (!savedData.chart_name) { + return false; + } + + return true; + } + + save(blockContent) { + return { + chart_name: blockContent.getAttribute('chart_name'), + col: this.get_col(), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/header.js b/frappe/public/js/frappe/views/workspace/blocks/header.js new file mode 100644 index 0000000000..356f9c3244 --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/header.js @@ -0,0 +1,339 @@ +import Block from "./block.js"; +export default class Header extends Block { + + constructor({ data, config, api, readOnly }) { + super({ config, api, readOnly }); + + this._CSS = { + block: this.api.styles.block, + settingsButton: this.api.styles.settingsButton, + settingsButtonActive: this.api.styles.settingsButtonActive, + wrapper: 'ce-header', + }; + + this._settings = this.config; + this._data = this.normalizeData(data); + this.settingsButtons = []; + this._element = this.getTag(); + + this.data = data; + this.col = this.data.col ? this.data.col : "12"; + } + + normalizeData(data) { + const newData = {}; + + if (typeof data !== 'object') { + data = {}; + } + + newData.text = data.text || ''; + newData.level = parseInt(data.level) || this.defaultLevel.number; + newData.col = parseInt(data.col) || 12; + + return newData; + } + + render() { + this.wrapper = document.createElement('div'); + this.wrapper.contentEditable = this.readOnly ? 'false' : 'true'; + if (!this.readOnly) { + let $widget_head = $(`
`); + let $widget_control = $(`
`); + + $widget_head[0].appendChild(this._element); + $widget_control.appendTo($widget_head); + $widget_head.appendTo(this.wrapper); + + this.wrapper.classList.add('widget', 'header'); + + frappe.utils.add_custom_button( + frappe.utils.icon('dot-horizontal', 'xs'), + (event) => { + let evn = event; + !$('.ce-settings.ce-settings--opened').length && + setTimeout(() => { + this.api.toolbar.toggleBlockSettings(); + var position = $(evn.target).offset(); + $('.ce-settings.ce-settings--opened').offset({ + top: position.top + 25, + left: position.left - 77 + }); + }, 50); + }, + "tune-btn", + `${__('Tune')}`, + null, + $widget_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('drag', 'xs'), + null, + "drag-handle", + `${__('Drag')}`, + null, + $widget_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('delete', 'xs'), + () => this.api.blocks.delete(), + "delete-header", + `${__('Delete')}`, + null, + $widget_control + ); + + return this.wrapper; + } + return this._element; + } + + renderSettings() { + const holder = document.createElement('DIV'); + + if (this.levels.length <= 1) { + return holder; + } + + this.levels.forEach(level => { + const selectTypeButton = document.createElement('SPAN'); + + selectTypeButton.classList.add(this._CSS.settingsButton); + + if (this.currentLevel.number === level.number) { + selectTypeButton.classList.add(this._CSS.settingsButtonActive); + } + + selectTypeButton.innerHTML = level.svg; + + selectTypeButton.dataset.level = level.number; + + selectTypeButton.addEventListener('click', () => { + this.setLevel(level.number); + }); + + holder.appendChild(selectTypeButton); + + this.settingsButtons.push(selectTypeButton); + }); + + return holder; + } + + setLevel(level) { + this.data = { + level: level, + text: this.data.text, + }; + + this.settingsButtons.forEach(button => { + button.classList.toggle(this._CSS.settingsButtonActive, parseInt(button.dataset.level) === level); + }); + } + + merge(data) { + const newData = { + text: this.data.text + data.text, + level: this.data.level, + }; + + this.data = newData; + } + + validate(blockData) { + return blockData.text.trim() !== ''; + } + + save(toolsContent) { + this.wrapper = this._element; + return { + text: toolsContent.innerText, + level: this.currentLevel.number, + col: this.get_col() + }; + } + + rendered() { + var e = this._element.closest('.ce-block'); + e.classList.add("col-" + this.get_col()); + } + + static get conversionConfig() { + return { + export: 'text', // use 'text' property for other blocks + import: 'text', // fill 'text' property from other block's export string + }; + } + + static get sanitize() { + return { + level: false, + text: {}, + }; + } + + static get isReadOnlySupported() { + return true; + } + + get data() { + this._data.text = this._element.innerHTML; + this._data.level = this.currentLevel.number; + + return this._data; + } + + set data(data) { + this._data = this.normalizeData(data); + + if (data.level !== undefined && this._element.parentNode) { + const newHeader = this.getTag(); + newHeader.innerHTML = this._element.innerHTML; + this._element.parentNode.replaceChild(newHeader, this._element); + this._element = newHeader; + } + + if (data.text !== undefined) { + this._element.innerHTML = this._data.text || ''; + } + + if (!this.readOnly && this.wrapper) { + this.wrapper.classList.add('widget', 'header'); + } + } + + getTag() { + const tag = document.createElement(this.currentLevel.tag); + + tag.innerHTML = this._data.text || ''; + + tag.classList.add(this._CSS.wrapper); + + if (!this.readOnly) { + tag.contentEditable = true; + } + + tag.dataset.placeholder = this.api.i18n.t(this._settings.placeholder || ''); + + return tag; + } + + get currentLevel() { + let level = this.levels.find(levelItem => levelItem.number === this._data.level); + + if (!level) { + level = this.defaultLevel; + } + + return level; + } + + get defaultLevel() { + if (this._settings.defaultLevel) { + const userSpecified = this.levels.find(levelItem => { + return levelItem.number === this._settings.defaultLevel; + }); + + if (userSpecified) { + return userSpecified; + } else { + // console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels'); + } + } + + return this.levels[1]; + } + + get levels() { + const availableLevels = [ + { + number: 1, + tag: 'H1', + svg: '', + }, + { + number: 2, + tag: 'H2', + svg: '', + }, + { + number: 3, + tag: 'H3', + svg: '', + }, + { + number: 4, + tag: 'H4', + svg: '', + }, + { + number: 5, + tag: 'H5', + svg: '', + }, + { + number: 6, + tag: 'H6', + svg: '', + }, + ]; + + return this._settings.levels ? availableLevels.filter( + l => this._settings.levels.includes(l.number) + ) : availableLevels; + } + + onPaste(event) { + const content = event.detail.data; + + let level = this.defaultLevel.number; + + switch (content.tagName) { + case 'H1': + level = 1; + break; + case 'H2': + level = 2; + break; + case 'H3': + level = 3; + break; + case 'H4': + level = 4; + break; + case 'H5': + level = 5; + break; + case 'H6': + level = 6; + break; + } + + if (this._settings.levels) { + // Fallback to nearest level when specified not available + level = this._settings.levels.reduce((prevLevel, currLevel) => { + return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel; + }); + } + + this.data = { + level, + text: content.innerHTML, + }; + } + + static get pasteConfig() { + return { + tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'], + }; + } + + static get toolbox() { + return { + icon: '', + title: 'Heading', + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/index.js b/frappe/public/js/frappe/views/workspace/blocks/index.js new file mode 100644 index 0000000000..00a9b8c83a --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/index.js @@ -0,0 +1,27 @@ +// import blocks +import Header from "./header"; +import Paragraph from "./paragraph"; +import Card from "./card"; +import Chart from "./chart"; +import Shortcut from "./shortcut"; +import Spacer from "./spacer"; +import Onboarding from "./onboarding"; + +// import tunes +import SpacingTune from "./spacing_tune"; + +frappe.provide("frappe.wspace_block"); + +frappe.wspace_block.blocks = { + header: Header, + paragraph: Paragraph, + card: Card, + chart: Chart, + shortcut: Shortcut, + spacer: Spacer, + onboarding: Onboarding, +}; + +frappe.wspace_block.tunes = { + spacing_tune: SpacingTune +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js new file mode 100644 index 0000000000..7176b7726d --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -0,0 +1,129 @@ +import get_dialog_constructor from "../../../widgets/widget_dialog.js"; +import Block from "./block.js"; +export default class Onboarding extends Block { + static get toolbox() { + return { + title: 'Onboarding', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.col = this.data.col ? this.data.col : "12"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true + }; + } + + rendered() { + var e = this.wrapper.closest('.ce-block'); + if (this.readOnly && !$(this.wrapper).find('.onboarding-widget-box').is(':visible')) { + $(e).hide(); + } + e.classList.add("col-" + this.get_col()); + } + + new(block, widget_type = block) { + const dialog_class = get_dialog_constructor(widget_type); + let block_name = block+'_name'; + this.dialog = new dialog_class({ + label: this.label, + type: widget_type, + primary_action: (widget) => { + widget.in_customize_mode = 1; + this.block_widget = frappe.widget.make_widget({ + ...widget, + widget_type: widget_type, + container: this.wrapper, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.block_widget) + }, + new: true + }); + this.block_widget.customize(this.options); + this.wrapper.setAttribute(block_name, this.block_widget.label || this.block_widget.onboarding_name); + this.new_block_widget = this.block_widget.get_config(); + this.add_tune_button(); + }, + }); + + if (!this.readOnly && this.data && !this.data[block_name]) { + this.dialog.make(); + } + } + + make(block, block_name) { + let block_data = this.config.page_data['onboardings'].items.find(obj => { + return obj.label == block_name; + }); + if (!block_data) return false; + this.wrapper.innerHTML = ''; + block_data.in_customize_mode = !this.readOnly; + this.block_widget = frappe.widget.make_widget({ + container: this.wrapper, + widget_type: 'onboarding', + in_customize_mode: block_data.in_customize_mode, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.block_widget) + }, + label: block_data.label, + title: block_data.title || __("Let's Get Started"), + subtitle: block_data.subtitle, + steps: block_data.items, + success: block_data.success, + docs_url: block_data.docs_url, + user_can_dismiss: block_data.user_can_dismiss, + }); + this.wrapper.setAttribute(block+'_name', block_name); + if (!this.readOnly) { + this.block_widget.customize(this.options); + } + return true; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('onboarding'); + + if (this.data && this.data.onboarding_name) { + let has_data = this.make('onboarding', this.data.onboarding_name); + if (!has_data) return; + } + + if (!this.readOnly) { + this.add_tune_button(); + } + $(this.wrapper).css("padding-bottom", "20px"); + return this.wrapper; + } + + validate(savedData) { + if (!savedData.onboarding_name) { + return false; + } + + return true; + } + + save(blockContent) { + return { + onboarding_name: blockContent.getAttribute('onboarding_name'), + col: this.get_col(), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js new file mode 100644 index 0000000000..b594f3459a --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js @@ -0,0 +1,192 @@ +import Block from "./block.js"; +export default class Paragraph extends Block { + + static get DEFAULT_PLACEHOLDER() { + return ''; + } + + constructor({ data, config, api, readOnly }) { + super({ config, api, readOnly }); + + this._CSS = { + block: this.api.styles.block, + wrapper: 'ce-paragraph' + }; + + if (!this.readOnly) { + this.onKeyUp = this.onKeyUp.bind(this); + } + + this._placeholder = this.config.placeholder ? this.config.placeholder : Paragraph.DEFAULT_PLACEHOLDER; + this._data = {}; + this._element = this.drawView(); + this._preserveBlank = this.config.preserveBlank !== undefined ? this.config.preserveBlank : false; + + this.data = data; + this.col = this.data.col ? this.data.col : "12"; + } + + onKeyUp(e) { + if (e.code !== 'Backspace' && e.code !== 'Delete') { + return; + } + + const {textContent} = this._element; + + if (textContent === '') { + this._element.innerHTML = ''; + } + } + + drawView() { + let div = document.createElement('DIV'); + + div.classList.add(this._CSS.wrapper, this._CSS.block, 'widget'); + div.contentEditable = false; + div.dataset.placeholder = this.api.i18n.t(this._placeholder); + + if (!this.readOnly) { + div.contentEditable = true; + div.addEventListener('keyup', this.onKeyUp); + } + return div; + } + + render() { + this.wrapper = document.createElement('div'); + this.wrapper.contentEditable = this.readOnly ? 'false' : 'true'; + if (!this.readOnly) { + let $para_control = $(`
`); + + this.wrapper.appendChild(this._element); + this._element.classList.remove('widget'); + $para_control.appendTo(this.wrapper); + + this.wrapper.classList.add('widget'); + + frappe.utils.add_custom_button( + frappe.utils.icon('dot-horizontal', 'xs'), + (event) => { + let evn = event; + !$('.ce-settings.ce-settings--opened').length && + setTimeout(() => { + this.api.toolbar.toggleBlockSettings(); + var position = $(evn.target).offset(); + $('.ce-settings.ce-settings--opened').offset({ + top: position.top + 25, + left: position.left - 77 + }); + }, 50); + }, + "tune-btn", + `${__('Tune')}`, + null, + $para_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('drag', 'xs'), + null, + "drag-handle", + `${__('Drag')}`, + null, + $para_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('delete', 'xs'), + () => this.api.blocks.delete(), + "delete-paragraph", + `${__('Delete')}`, + null, + $para_control + ); + + return this.wrapper; + } + return this._element; + } + + merge(data) { + let newData = { + text: this.data.text + data.text + }; + + this.data = newData; + } + + validate(savedData) { + if (savedData.text.trim() === '' && !this._preserveBlank) { + return false; + } + + return true; + } + + save(toolsContent) { + this.wrapper = this._element; + return { + text: toolsContent.innerText, + col: this.get_col(), + }; + } + + rendered() { + var e = this._element.closest('.ce-block'); + e.classList.add("col-" + this.get_col()); + } + + onPaste(event) { + const data = { + text: event.detail.data.innerHTML + }; + + this.data = data; + } + + static get conversionConfig() { + return { + export: 'text', // to convert Paragraph to other block, use 'text' property of saved data + import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data + }; + } + + static get sanitize() { + return { + text: { + br: true, + } + }; + } + + static get isReadOnlySupported() { + return true; + } + + get data() { + let text = this._element.innerHTML; + + this._data.text = text; + + return this._data; + } + + set data(data) { + this._data = data || {}; + + this._element.innerHTML = this._data.text || ''; + } + + static get pasteConfig() { + return { + tags: [ 'P' ] + }; + } + + static get toolbox() { + return { + icon: '', + title: 'Text' + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js new file mode 100644 index 0000000000..0943de202d --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -0,0 +1,57 @@ +import Block from "./block.js"; +export default class Shortcut extends Block { + static get toolbox() { + return { + title: 'Shortcut', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.col = this.data.col ? this.data.col : "12"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true + }; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('shortcut'); + + if (this.data && this.data.shortcut_name) { + let has_data = this.make('shortcut', this.data.shortcut_name); + if (!has_data) return; + } + + if (!this.readOnly) { + this.add_tune_button(); + } + return this.wrapper; + } + + validate(savedData) { + if (!savedData.shortcut_name) { + return false; + } + + return true; + } + + save(blockContent) { + return { + shortcut_name: blockContent.getAttribute('shortcut_name'), + col: this.get_col(), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/spacer.js b/frappe/public/js/frappe/views/workspace/blocks/spacer.js new file mode 100644 index 0000000000..3309cad4a4 --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/spacer.js @@ -0,0 +1,82 @@ +import Block from './block.js'; +export default class Spacer extends Block { + static get toolbox() { + return { + title: 'Spacer', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly }) { + super({ data, api, config, readOnly }); + this.col = this.data.col ? this.data.col : "12"; + } + + render() { + this.wrapper = document.createElement('div'); + if (!this.readOnly) { + let $spacer = $(` +
+
+
Spacer
+
+
+ `); + $spacer.appendTo(this.wrapper); + + this.wrapper.classList.add('widget', 'new-widget'); + this.wrapper.style.minHeight = 50 + 'px'; + + let $widget_control = $spacer.find('.widget-control'); + + frappe.utils.add_custom_button( + frappe.utils.icon('dot-horizontal', 'xs'), + (event) => { + let evn = event; + !$('.ce-settings.ce-settings--opened').length && + setTimeout(() => { + this.api.toolbar.toggleBlockSettings(); + var position = $(evn.target).offset(); + $('.ce-settings.ce-settings--opened').offset({ + top: position.top + 25, + left: position.left - 77 + }); + }, 50); + }, + "tune-btn", + `${__('Tune')}`, + null, + $widget_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('drag', 'xs'), + null, + "drag-handle", + `${__('Drag')}`, + null, + $widget_control + ); + + frappe.utils.add_custom_button( + frappe.utils.icon('delete', 'xs'), + () => this.api.blocks.delete(), + "delete-spacer", + `${__('Delete')}`, + null, + $widget_control + ); + } + return this.wrapper; + } + + save() { + return { + col: this.get_col() + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js b/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js new file mode 100644 index 0000000000..365f7f590e --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js @@ -0,0 +1,123 @@ +export default class SpacingTune { + static get isTune() { + return true; + } + + constructor({api, settings}) { + this.api = api; + this.settings = settings; + this.CSS = { + button: 'ce-settings__button', + wrapper: 'ce-tune-layout', + sidebar: 'cdx-settings-sidebar', + animation: 'wobble', + }; + this.data = { colWidth: 12 }; + this.wrapper = undefined; + this.sidebar = undefined; + } + + render() { + let me = this; + let layoutWrapper = document.createElement('div'); + layoutWrapper.classList.add(this.CSS.wrapper); + let decreaseWidthButton = document.createElement('div'); + decreaseWidthButton.classList.add(this.CSS.button, 'ce-shrink-button'); + let increaseWidthButton = document.createElement('div'); + increaseWidthButton.classList.add(this.CSS.button, 'ce-expand-button'); + + layoutWrapper.appendChild(decreaseWidthButton); + layoutWrapper.appendChild(increaseWidthButton); + + decreaseWidthButton.innerHTML = ``; + this.api.tooltip.onHover(decreaseWidthButton, 'Shrink', { + placement: 'top', + hidingDelay: 500, + }); + this.api.listeners.on( + decreaseWidthButton, + 'click', + () => me.decreaseWidth(), + false + ); + + increaseWidthButton.innerHTML = ``; + this.api.tooltip.onHover(increaseWidthButton, 'Expand', { + placement: 'top', + hidingDelay: 500, + }); + this.api.listeners.on( + increaseWidthButton, + 'click', + () => me.increaseWidth(), + false + ); + + this.wrapper = layoutWrapper; + return layoutWrapper; + } + + decreaseWidth() { + const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); + + if (currentBlockIndex < 0) { + return; + } + + let currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); + if (!currentBlock) { + return; + } + + let currentBlockElement = currentBlock.holder; + + let className = 'col-12'; + let colClass = new RegExp(/\bcol-.+?\b/, 'g'); + if (currentBlockElement.className.match(colClass)) { + currentBlockElement.classList.forEach( cn => { + if (cn.match(colClass)) { + className = cn; + } + }); + let parts = className.split('-'); + let width = parseInt(parts[1]); + if (width >= 4) { + currentBlockElement.classList.remove('col-'+width); + width = width - 1; + currentBlockElement.classList.add('col-'+width); + } + } + } + + increaseWidth() { + const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); + + if (currentBlockIndex < 0) { + return; + } + + const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); + if (!currentBlock) { + return; + } + + const currentBlockElement = currentBlock.holder; + + let className = 'col-12'; + const colClass = new RegExp(/\bcol-.+?\b/, 'g'); + if (currentBlockElement.className.match(colClass)) { + currentBlockElement.classList.forEach( cn => { + if (cn.match(colClass)) { + className = cn; + } + }); + let parts = className.split('-'); + let width = parseInt(parts[1]); + if (width <= 11) { + currentBlockElement.classList.remove('col-'+width); + width = width + 1; + currentBlockElement.classList.add('col-'+width); + } + } + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index f16228dca0..b46c220d9d 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -1,3 +1,6 @@ +import EditorJS from '@editorjs/editorjs'; +import Undo from 'editorjs-undo'; + frappe.standard_pages['Workspaces'] = function() { var wrapper = frappe.container.add_page('Workspaces'); @@ -17,37 +20,60 @@ frappe.views.Workspace = class Workspace { constructor(wrapper) { this.wrapper = $(wrapper); this.page = wrapper.page; - this.prepare_container(); - this.show_or_hide_sidebar(); - this.setup_dropdown(); - this.pages = {}; - this.sidebar_items = {}; + this.blocks = frappe.wspace_block.blocks; + this.is_read_only = true; + this.new_page = null; + this.sorted_public_items = []; + this.sorted_private_items = []; + this.deleted_sidebar_items = []; + this.current_page = {}; + this.sidebar_items = { + 'public': {}, + 'private': {} + }; this.sidebar_categories = [ - "Modules", - "Domains", - "Places", - "Administration" + 'Public', + frappe.user.first_name() || 'Private' ]; + this.tools = { + header: { + class: this.blocks['header'], + inlineToolbar: true + }, + paragraph: { + class: this.blocks['paragraph'], + inlineToolbar: true + }, + chart: { + class: this.blocks['chart'], + config: { + page_data: this.page_data || [] + } + }, + card: { + class: this.blocks['card'], + config: { + page_data: this.page_data || [] + } + }, + shortcut: { + class: this.blocks['shortcut'], + config: { + page_data: this.page_data || [] + } + }, + onboarding: { + class: this.blocks['onboarding'], + config: { + page_data: this.page_data || [] + } + }, + spacer: this.blocks['spacer'], + spacingTune: frappe.wspace_block.tunes['spacing_tune'], + }; - this.setup_workspaces(); - this.make_sidebar(); - } - - setup_workspaces() { - // workspaces grouped by categories - this.workspaces = {}; - for (let page of frappe.boot.allowed_workspaces) { - if (!this.workspaces[page.category]) { - this.workspaces[page.category] = []; - } - this.workspaces[page.category].push(page); - } - } - - show() { - let page = this.get_page_to_show(); - this.page.set_title(`${__(page)}`); - this.show_page(page); + this.prepare_container(); + this.setup_pages(); } prepare_container() { @@ -57,229 +83,183 @@ frappe.views.Workspace = class Workspace { `).appendTo(this.wrapper.find(".layout-side-section")); this.sidebar = list_sidebar.find(".desk-sidebar"); - this.body = this.wrapper.find(".layout-main-section"); } - get_page_to_show() { - let default_page; + setup_pages() { + this.get_pages().then(pages => { + this.all_pages = pages.pages; + this.has_access = pages.has_access; - if (localStorage.current_workspace) { - default_page = localStorage.current_workspace; - } else if (this.workspaces) { - default_page = this.workspaces["Modules"][0].name; - } else if (frappe.boot.allowed_workspaces) { - default_page = frappe.boot.allowed_workspaces[0].name; - } else { - default_page = "Build"; - } + this.all_pages.forEach(page => { + page.is_editable = !page.public || pages.has_access; + }); - let page = frappe.get_route()[1] || default_page; - return page; + this.public_pages = this.all_pages.filter(page => page.public); + this.private_pages = this.all_pages.filter(page => !page.public); + + if (this.all_pages) { + frappe.workspaces = {}; + for (let page of this.all_pages) { + frappe.workspaces[frappe.router.slug(page.title)] = {title: page.title}; + } + if (this.new_page && this.new_page.name) { + if (!frappe.workspaces[frappe.router.slug(this.new_page.name)]) { + this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public }; + } + if (this.new_page.public) { + frappe.set_route(`${frappe.router.slug(this.new_page.name)}`); + } else { + frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`); + } + this.new_page = null; + } + this.make_sidebar(); + frappe.router.route(); + } + }); + } + + get_pages() { + return frappe.xcall("frappe.desk.desktop.get_wspace_sidebar_items"); + } + + sidebar_item_container(item) { + return $(` + + `); } make_sidebar() { + if (this.sidebar.find('.standard-sidebar-section')[0]) { + this.sidebar.find('.standard-sidebar-section').remove(); + } + this.sidebar_categories.forEach(category => { - if (this.workspaces[category]) { - this.build_sidebar_section(category, this.workspaces[category]); + let root_pages = this.public_pages.filter(page => page.parent_page == '' || page.parent_page == null); + if (category != 'Public') { + root_pages = this.private_pages.filter(page => page.parent_page == '' || page.parent_page == null); } - }); - } - - build_sidebar_section(title, items) { - let sidebar_section = $(`
`); - - // DO NOT REMOVE: Comment to load translation - // __("Modules") __("Domains") __("Places") __("Administration") - $(`
${__(title)}
`) - .appendTo(sidebar_section); - - const get_sidebar_item = function (item) { - return $(` - - ${frappe.utils.icon(item.icon || "folder-normal", "md")} - ${item.label || item.name} - - `); - }; - - const make_sidebar_category_item = item => { - if (item.name == this.get_page_to_show()) { - item.selected = true; - this.current_page_name = item.name; - } - - let $item = get_sidebar_item(item); - - $item.appendTo(sidebar_section); - this.sidebar_items[item.name] = $item; - }; - - items.forEach(item => make_sidebar_category_item(item)); - - sidebar_section.appendTo(this.sidebar); - } - - show_page(page) { - if (this.current_page_name && this.pages[this.current_page_name]) { - this.pages[this.current_page_name].hide(); - } - - if (this.sidebar_items && this.sidebar_items[this.current_page_name]) { - this.sidebar_items[this.current_page_name].removeClass("selected"); - this.sidebar_items[page].addClass("selected"); - } - this.current_page_name = page; - localStorage.current_workspace = page; - - this.pages[page] ? this.pages[page].show() : this.make_page(page); - this.current_page = this.pages[page]; - this.setup_dropdown(); - } - - make_page(page) { - const $page = new DesktopPage({ - container: this.body, - page_name: page + this.build_sidebar_section(category, root_pages); }); - this.pages[page] = $page; - return $page; + // Scroll sidebar to selected page if it is not in viewport. + !frappe.dom.is_element_in_viewport(this.sidebar.find('.selected')) + && this.sidebar.find('.selected')[0].scrollIntoView(); } - customize() { - if (this.current_page && this.current_page.allow_customization) { - this.page.clear_menu(); - this.current_page.customize(); + build_sidebar_section(title, root_pages) { + let sidebar_section = $(`
`); - this.page.set_primary_action( - __("Save Customizations"), - () => { - this.current_page.save_customization(); - this.page.clear_primary_action(); - this.page.clear_secondary_action(); - this.setup_dropdown(); - }, - null, - __("Saving") - ); + let $title = $(`
+ ${frappe.utils.icon("small-down", "xs")} + ${__(title)} +
`).appendTo(sidebar_section); + this.prepare_sidebar(root_pages, sidebar_section, this.sidebar); - this.page.set_secondary_action( - __("Discard"), - () => { - this.current_page.reload(); - frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" }); - this.page.clear_primary_action(); - this.page.clear_secondary_action(); - this.setup_dropdown(); - } - ); + $title.on('click', (e) => { + let icon = $(e.target).find("span use").attr("href")==="#icon-small-down" ? "#icon-right" : "#icon-small-down"; + $(e.target).find("span use").attr("href", icon); + $(e.target).parent().find('.sidebar-item-container').toggleClass('hidden'); + }); + + if (!this.current_page.name) { + $title.trigger("click"); + } + + if (Object.keys(root_pages).length === 0) { + sidebar_section.addClass('hidden'); } } - setup_dropdown() { - this.page.clear_menu(); - - this.page.set_secondary_action(__('Customize'), () => { - this.customize(); - }); - - this.page.add_menu_item(__('Reset Customizations'), () => { - this.current_page.reset_customization(); - }, 1); - - this.page.add_menu_item(__('Toggle Sidebar'), () => { - this.toggle_side_bar(); - }, 1); + prepare_sidebar(items, child_container, item_container) { + items.forEach(item => this.append_item(item, child_container)); + child_container.appendTo(item_container); } - toggle_side_bar() { - let show_workspace_sidebar = JSON.parse(localStorage.show_workspace_sidebar || "true"); - show_workspace_sidebar = !show_workspace_sidebar; - localStorage.show_workspace_sidebar = show_workspace_sidebar; - this.show_or_hide_sidebar(); - $(document.body).trigger("toggleDeskSidebar"); + append_item(item, container) { + let is_current_page = frappe.router.slug(item.title) == frappe.router.slug(this.get_page_to_show().name) + && item.public == this.get_page_to_show().public; + if (is_current_page) { + item.selected = true; + this.current_page = { name: item.title, public: item.public }; + } + + let $item_container = this.sidebar_item_container(item); + let sidebar_control = $item_container.find('.sidebar-item-control'); + + this.add_sidebar_actions(item, sidebar_control); + let pages = item.public ? this.public_pages : this.private_pages; + + let child_items = pages.filter(page => page.parent_page == item.title); + if (child_items.length > 0) { + let child_container = $(``); + this.prepare_sidebar(child_items, child_container, $item_container); + } + + $item_container.appendTo(container); + this.sidebar_items[item.public ? 'public' : 'private'][item.title] = $item_container; + + if ($item_container.parent().hasClass('hidden') && is_current_page) { + $item_container.parent().toggleClass('hidden'); + } + + this.add_drop_icon(item, sidebar_control, $item_container); } - show_or_hide_sidebar() { - let show_workspace_sidebar = JSON.parse(localStorage.show_workspace_sidebar || "true"); - $('#page-workspace .layout-side-section').toggleClass('hidden', !show_workspace_sidebar); - } -}; - -class DesktopPage { - constructor({ container, page_name }) { - frappe.desk_page = this; - this.container = container; - this.page_name = page_name; - this.sections = {}; - this.allow_customization = false; - this.reload(); - } - - show() { - frappe.desk_page = this; - this.page.show(); - if (this.sections.shortcuts) { - this.sections.shortcuts.widgets_list.forEach(wid => { - wid.set_actions(); + add_drop_icon(item, sidebar_control, item_container) { + let $child_item_section = item_container.find('.sidebar-child-item'); + let $drop_icon = $(``) + .appendTo(sidebar_control); + let pages = item.public ? this.public_pages : this.private_pages; + if (pages.some(e => e.parent_page == item.title)) { + $drop_icon.removeClass('hidden'); + $drop_icon.on('click', () => { + let icon = $drop_icon.find("use").attr("href")==="#icon-small-down" ? "#icon-small-up" : "#icon-small-down"; + $drop_icon.find("use").attr("href", icon); + $child_item_section.toggleClass("hidden"); }); } } - hide() { - this.page.hide(); - } - - reload() { - this.in_customize_mode = false; - this.page && this.page.remove(); - this.make(); - } - - make() { - this.page = $(`
`); - this.page.append(frappe.render_template('workspace_loading_skeleton')); - this.page.appendTo(this.container); - - this.get_data().then(() => { - if (Object.keys(this.data).length == 0) { - delete localStorage.current_workspace; - frappe.set_route("workspace"); - return; - } - this.refresh(); - }).finally(() => this.page.find('.workspace_loading_skeleton').remove()); - } - - refresh() { - this.page.empty(); - this.allow_customization = this.data.allow_customization || false; - - if (frappe.is_mobile()) { - this.allow_customization = false; + show() { + if (!this.all_pages) { + // pages not yet loaded, call again after a bit + setTimeout(() => this.show(), 100); + return; } - this.data.onboarding && this.data.onboarding.items.length && this.make_onboarding(); - this.make_charts(); - this.make_shortcuts(); - this.make_cards(); + let page = { + name: this.get_page_to_show().name, + public: this.get_page_to_show().public + }; + this.page.set_title(`${__(page.name)}`); + + this.show_page(page); } - get_data() { + get_data(page) { return frappe.xcall("frappe.desk.desktop.get_desktop_page", { - page: this.page_name + page: page }).then(data => { - this.data = data; - if (Object.keys(this.data).length == 0) return; + this.page_data = data; + if (!this.page_data || Object.keys(this.page_data).length === 0) return; return frappe.dashboard_utils.get_dashboard_settings().then(settings => { let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {}; - if (this.data.charts.items) { - this.data.charts.items.map(chart => { + if (this.page_data.charts && this.page_data.charts.items) { + this.page_data.charts.items.map(chart => { chart.chart_settings = chart_config[chart.chart_name] || {}; }); } @@ -287,128 +267,489 @@ class DesktopPage { }); } - customize() { - if (this.in_customize_mode) { + get_page_to_show() { + let default_page; + + if (localStorage.current_page && this.all_pages.filter(page => page.title == localStorage.current_page).length != 0) { + default_page = { name: localStorage.current_page, public: localStorage.is_current_page_public == 'true' }; + } else if (Object.keys(this.all_pages).length !== 0) { + default_page = { name: this.all_pages[0].title, public: true }; + } else { + default_page = { name: "Build", public: true }; + } + + let page = (frappe.get_route()[1] == 'private' ? frappe.get_route()[2] : frappe.get_route()[1]) || default_page.name; + let is_public = frappe.get_route()[1] ? frappe.get_route()[1] != 'private' : default_page.public; + return { name: page, public: is_public }; + } + + show_page(page) { + let section = this.current_page.public ? 'public' : 'private'; + if (this.sidebar_items && this.sidebar_items[section] && this.sidebar_items[section][this.current_page.name]) { + this.sidebar_items[section][this.current_page.name][0].firstElementChild.classList.remove("selected"); + this.sidebar_items[page.public ? 'public':'private'][page.name][0].firstElementChild.classList.add("selected"); + + if (this.sidebar_items[page.public ? 'public':'private'][page.name].parents('.sidebar-item-container')[0]) { + this.sidebar_items[page.public ? 'public':'private'][page.name] + .parents('.sidebar-item-container') + .find('.drop-icon use') + .attr("href", "#icon-small-up"); + } + } + + this.current_page = { name: page.name, public: page.public }; + localStorage.current_page = page.name; + localStorage.is_current_page_public = page.public; + + if (!this.body.find('#editorjs')[0]) { + this.$page = $(` +
+ `).appendTo(this.body); + } + this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); + this.$page.find('.codex-editor').addClass('hidden'); + + if (this.all_pages) { + let pages = page.public ? this.public_pages : this.private_pages; + let this_page = pages.filter(p => p.title == page.name)[0]; + this.setup_actions(page); + this.content = this_page && JSON.parse(this_page.content); + + this.add_custom_cards_in_content(); + + $('.item-anchor').addClass('disable-click'); + this.get_data(this_page).then(() => { + this.prepare_editorjs(); + $('.item-anchor').removeClass('disable-click'); + this.$page.find('.codex-editor').removeClass('hidden'); + this.$page.find('.workspace-skeleton').remove(); + }); + } + } + + add_custom_cards_in_content() { + let index = -1; + this.content.find((item, i) => { + if (item.type == 'card') index = i; + }); + if (index !== -1) { + this.content.splice(index+1, 0, {"type": "card", "data": {"card_name": "Custom Documents", "col": 4}}); + this.content.splice(index+2, 0, {"type": "card", "data": {"card_name": "Custom Reports", "col": 4}}); + } + } + + prepare_editorjs() { + if (this.editor) { + this.editor.isReady.then(() => { + this.editor.configuration.tools.chart.config.page_data = this.page_data; + this.editor.configuration.tools.shortcut.config.page_data = this.page_data; + this.editor.configuration.tools.card.config.page_data = this.page_data; + this.editor.configuration.tools.onboarding.config.page_data = this.page_data; + this.editor.render({ blocks: this.content || [] }); + }); + } else { + this.initialize_editorjs(this.content); + } + } + + setup_actions(page) { + let pages = page.public ? this.public_pages : this.private_pages; + let current_page = pages.filter(p => p.title == page.name)[0]; + + if (!this.is_read_only) { + this.setup_customization_buttons(current_page.is_editable); return; } - // We need to remove this as the chart group will be visible during customization - $('.widget.onboarding-widget-box').hide(); + this.page.clear_primary_action(); + this.page.clear_secondary_action(); + this.page.clear_inner_toolbar(); - Object.keys(this.sections).forEach(section => { - this.sections[section].customize(); + current_page.is_editable && this.page.set_secondary_action(__("Customize"), () => { + if (!this.editor || !this.editor.readOnly) return; + this.is_read_only = false; + this.editor.readOnly.toggle(); + this.editor.isReady.then(() => { + this.initialize_editorjs_undo(); + this.setup_customization_buttons(true); + this.show_sidebar_actions(); + this.make_sidebar_sortable(); + this.make_blocks_sortable(); + }); }); - this.in_customize_mode = true; + this.page.add_inner_button(__("Create Page"), () => { + this.initialize_new_page(); + }); } - save_customization() { + initialize_editorjs_undo() { + this.undo = new Undo({ editor: this.editor }); + this.undo.initialize({ blocks: this.content || [] }); + this.undo.readOnly = false; + } + + setup_customization_buttons(is_editable) { + let me = this; + this.page.clear_primary_action(); + this.page.clear_secondary_action(); + this.page.clear_inner_toolbar(); + + is_editable && this.page.set_primary_action( + __("Save Customizations"), + () => { + this.page.clear_primary_action(); + this.page.clear_secondary_action(); + this.page.clear_inner_toolbar(); + this.undo.readOnly = true; + this.save_page(); + this.editor.readOnly.toggle(); + this.is_read_only = true; + }, + null, + __("Saving") + ); + + this.page.set_secondary_action( + __("Discard"), + () => { + this.page.clear_primary_action(); + this.page.clear_secondary_action(); + this.page.clear_inner_toolbar(); + this.editor.readOnly.toggle(); + this.is_read_only = true; + this.deleted_sidebar_items = []; + this.reload(); + frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" }); + } + ); + + Object.keys(this.blocks).forEach(key => { + this.page.add_inner_button(` + ${this.blocks[key].toolbox.icon} + ${__(this.blocks[key].toolbox.title)} + `, function() { + const index = me.editor.blocks.getBlocksCount() + 1; + me.editor.blocks.insert(key, {}, {}, index, true); + me.editor.caret.setToLastBlock('start', 0); + $('.ce-block:last-child')[0].scrollIntoView(); + }, __('Add Block')); + }); + } + + show_sidebar_actions() { + this.sidebar.find('.standard-sidebar-section').addClass('show-control'); + } + + add_sidebar_actions(item, sidebar_control) { + if (!item.is_editable) { + $(`${frappe.utils.icon("lock", "sm")}`) + .appendTo(sidebar_control); + sidebar_control.parent().click(() => { + frappe.show_alert({ + message: __("Only Workspace Manager can sort or edit this page"), + indicator: 'info' + }, 5); + }); + } else { + frappe.utils.add_custom_button( + frappe.utils.icon('drag', 'xs'), + null, + "drag-handle", + `${__('Drag')}`, + null, + sidebar_control + ); + frappe.utils.add_custom_button( + frappe.utils.icon('delete', 'xs'), + () => this.delete_page(item), + "delete-page", + `${__('Delete')}`, + null, + sidebar_control + ); + } + } + + delete_page(item) { + frappe.confirm(__("Are you sure you want to delete page {0}?", [item.title]), () => { + this.deleted_sidebar_items.push(item); + this.sidebar.find(`.standard-sidebar-section [item-name="${item.title}"][item-public="${item.public}"]`).addClass('hidden'); + }); + } + + make_sidebar_sortable() { + let me = this; + $('.nested-container').each( function() { + new Sortable(this, { + handle: ".drag-handle", + draggable: ".sidebar-item-container.is-draggable", + group: 'nested', + animation: 150, + fallbackOnBody: true, + swapThreshold: 0.65, + onEnd: function (evt) { + let is_public = $(evt.item).attr('item-public') == '1'; + me.prepare_sorted_sidebar(is_public); + } + }); + }); + } + + prepare_sorted_sidebar(is_public) { + if (is_public) { + this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first()); + } else { + this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last()); + } + } + + sort_sidebar($sidebar_section) { + let sorted_items = []; + for (let page of $sidebar_section.find('.sidebar-item-container')) { + let parent_page = ""; + if (page.closest('.nested-container').classList.contains('sidebar-child-item')) { + parent_page = page.parentElement.parentElement.attributes["item-name"].value; + } + sorted_items.push({ + title: page.attributes['item-name'].value, + parent_page: parent_page, + public: page.attributes['item-public'].value + }); + } + return sorted_items; + } + + make_blocks_sortable() { + let me = this; + this.page_sortable = Sortable.create(this.page.main.find(".codex-editor__redactor").get(0), { + handle: ".drag-handle", + draggable: ".ce-block", + animation: 150, + onEnd: function (evt) { + me.editor.blocks.move(evt.newIndex, evt.oldIndex); + }, + setData: function () { + //Do Nothing + } + }); + } + + initialize_new_page() { + this.public_parent_pages = ['', ...this.public_pages.filter(page => !page.parent_page).map(page => page.title)]; + this.private_parent_pages = ['', ...this.private_pages.filter(page => !page.parent_page).map(page => page.title)]; + var me = this; + const d = new frappe.ui.Dialog({ + title: __('Set Title'), + fields: [ + { + label: __('Title'), + fieldtype: 'Data', + fieldname: 'title', + reqd: 1 + }, + { + label: __('Parent'), + fieldtype: 'Select', + fieldname: 'parent', + options: this.private_parent_pages + }, + { + label: __('Public'), + fieldtype: 'Check', + fieldname: 'is_public', + depends_on: `eval:${this.has_access}`, + onchange: function() { + d.set_df_property('parent', 'options', + this.get_value() ? me.public_parent_pages : me.private_parent_pages); + } + }, + { + fieldtype: 'Column Break' + }, + { + label: __('Icon'), + fieldtype: 'Icon', + fieldname: 'icon' + }, + ], + primary_action_label: __('Create'), + primary_action: (values) => { + if (!this.validate_page(values)) return; + d.hide(); + this.initialize_editorjs_undo(); + this.setup_customization_buttons(true); + this.title = values.title; + this.icon = values.icon; + this.parent = values.parent; + this.public = values.is_public; + this.editor.render({ + blocks: [ + { + type: "header", + data: { + text: this.title, + level: 4 + } + } + ] + }).then(() => { + if (this.editor.configuration.readOnly) { + this.is_read_only = false; + this.editor.readOnly.toggle(); + } + this.add_page_to_sidebar(values); + this.show_sidebar_actions(); + this.make_sidebar_sortable(); + this.make_blocks_sortable(); + this.prepare_sorted_sidebar(values.is_public); + }); + } + }); + d.show(); + } + + validate_page(values) { + let message = ""; + let pages = values.is_public ? this.public_pages : this.private_pages; + + if (pages && pages.filter(p => p.title == values.title)[0]) { + message = "Page with title '{0}' already exist."; + } else if (frappe.router.doctype_route_exist(frappe.router.slug(values.title))) { + message = "Doctype with same route already exist. Please choose different title."; + } + + if (message) { + frappe.throw(__(message, [__(values.title)])); + return false; + } + return true; + } + + add_page_to_sidebar({title, icon, parent, is_public}) { + let $sidebar = $('.standard-sidebar-section'); + let item = { + title: title, + icon: icon, + parent_page: parent, + public: is_public + }; + let $sidebar_item = this.sidebar_item_container(item); + $sidebar_item.addClass('is-draggable'); + + frappe.utils.add_custom_button( + frappe.utils.icon('drag', 'xs'), + null, + "drag-handle", + `${__('Drag')}`, + null, + $sidebar_item.find('.sidebar-item-control') + ); + $sidebar_item.find('.sidebar-item-control .drag-handle').css('margin-right', '8px'); + + let $sidebar_section = is_public ? $sidebar[0] : $sidebar[1]; + + if (!parent) { + !is_public && $sidebar.last().removeClass('hidden'); + $sidebar_item.appendTo($sidebar_section); + } else { + let $item_container = $($sidebar_section).find(`[item-name="${parent}"]`); + let $child_section = $item_container.find('.sidebar-child-item'); + let $drop_icon = $item_container.find('.drop-icon'); + if (!$child_section[0]) { + $child_section = $(``) + .appendTo($item_container); + $drop_icon.toggleClass('hidden'); + } + $sidebar_item.appendTo($child_section); + $child_section.removeClass('hidden'); + $item_container.find('.drop-icon use').attr("href", "#icon-small-up"); + } + } + + initialize_editorjs(blocks) { + this.editor = new EditorJS({ + data: { + blocks: blocks || [] + }, + tools: this.tools, + autofocus: false, + tunes: ['spacingTune'], + readOnly: true, + logLevel: 'ERROR' + }); + } + + save_page() { frappe.dom.freeze(); - const config = {}; + let save = true; + if (!this.title && this.current_page) { + let pages = this.current_page.public ? this.public_pages : this.private_pages; + this.title = this.current_page.name; + this.public = pages.filter(p => p.title == this.title)[0].public; + save = false; + } else { + this.current_page = { name: this.title, public: this.public }; + } + let me = this; + this.editor.save().then((outputData) => { + let new_widgets = {}; + outputData.blocks.forEach(item => { + if (item.data.new) { + if (!new_widgets[item.type]) { + new_widgets[item.type] = []; + } + new_widgets[item.type].push(item.data.new); + delete item.data['new']; + } + }); - if (this.sections.charts) config.charts = this.sections.charts.get_widget_config(); - if (this.sections.shortcuts) config.shortcuts = this.sections.shortcuts.get_widget_config(); - if (this.sections.cards) config.cards = this.sections.cards.get_widget_config(); + let blocks = outputData.blocks.filter( + item => item.type != 'card' || + (item.data.card_name !== 'Custom Documents' && + item.data.card_name !== 'Custom Reports') + ); - frappe.call('frappe.desk.desktop.save_customization', { - page: this.page_name, - config: config - }).then(res => { - frappe.dom.unfreeze(); - if (res.message) { - frappe.show_alert({ message: __("Customizations Saved Successfully"), indicator: "green" }); - this.reload(); - } else { - frappe.throw({ message: __("Something went wrong while saving customizations"), indicator: "red" }); - this.reload(); - } + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.save_page", + args: { + title: me.title, + icon: me.icon || '', + parent: me.parent || '', + public: me.public || 0, + sb_public_items: me.sorted_public_items, + sb_private_items: me.sorted_private_items, + deleted_pages: me.deleted_sidebar_items, + new_widgets: new_widgets, + blocks: JSON.stringify(blocks), + save: save + }, + callback: function(res) { + frappe.dom.unfreeze(); + if (res.message) { + me.new_page = res.message; + me.title = ''; + me.icon = ''; + me.parent = ''; + me.public = false; + me.sorted_public_items = []; + me.sorted_private_items = []; + me.deleted_sidebar_items = []; + me.reload(); + frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); + } + } + }); + }).catch((error) => { + error; + // console.log('Saving failed: ', error); }); } - reset_customization() { - frappe.call('frappe.desk.desktop.reset_customization', { - page: this.page_name - }).then(() => { - frappe.show_alert({ message: __("Removed page customizations"), indicator: "green" }); - this.reload(); - }); + reload() { + this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); + this.$page.find('.codex-editor').addClass('hidden'); + this.setup_pages(); + this.undo.readOnly = true; } - - make_onboarding() { - this.onboarding_widget = frappe.widget.make_widget({ - label: this.data.onboarding.label || __("Let's Get Started"), - subtitle: this.data.onboarding.subtitle, - steps: this.data.onboarding.items, - success: this.data.onboarding.success, - docs_url: this.data.onboarding.docs_url, - user_can_dismiss: this.data.onboarding.user_can_dismiss, - widget_type: 'onboarding', - container: this.page, - options: { - allow_sorting: false, - allow_create: false, - allow_delete: false, - allow_hiding: false, - allow_edit: false, - max_widget_count: 2, - } - }); - } - - make_charts() { - this.sections["charts"] = new frappe.widget.WidgetGroup({ - container: this.page, - type: "chart", - columns: 1, - class_name: "widget-charts", - hidden: Boolean(this.onboarding_widget), - options: { - allow_sorting: this.allow_customization, - allow_create: this.allow_customization, - allow_delete: this.allow_customization, - allow_hiding: false, - allow_edit: true, - max_widget_count: 2, - }, - widgets: this.data.charts.items - }); - } - - make_shortcuts() { - this.sections["shortcuts"] = new frappe.widget.WidgetGroup({ - title: this.data.shortcuts.label || __('Your Shortcuts'), - container: this.page, - type: "shortcut", - columns: 3, - options: { - allow_sorting: this.allow_customization, - allow_create: this.allow_customization, - allow_delete: this.allow_customization, - allow_hiding: false, - allow_edit: true, - }, - widgets: this.data.shortcuts.items - }); - } - - make_cards() { - let cards = new frappe.widget.WidgetGroup({ - title: this.data.cards.label || __("Reports & Masters"), - container: this.page, - type: "links", - columns: 3, - options: { - allow_sorting: this.allow_customization, - allow_create: false, - allow_delete: false, - allow_hiding: this.allow_customization, - allow_edit: false, - }, - widgets: this.data.cards.items - }); - - this.sections["cards"] = cards; - } -} - - +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index 9bbfb916e5..e6ae64d9dc 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -25,18 +25,23 @@ export default class Widget { this.action_area.empty(); options.allow_sorting && - this.add_custom_button( + frappe.utils.add_custom_button( frappe.utils.icon('drag', 'xs'), null, "drag-handle", + `${__('Drag')}`, + null, + this.action_area ); options.allow_delete && - this.add_custom_button( + frappe.utils.add_custom_button( frappe.utils.icon('delete', 'xs'), () => this.delete(), "", - `${__('Delete')}` + `${__('Delete')}`, + null, + this.action_area ); if (options.allow_hiding) { @@ -48,11 +53,13 @@ export default class Widget { } const classname = this.hidden ? 'fa fa-eye' : 'fa fa-eye-slash'; const title = this.hidden ? `${__('Show')}` : `${__('Hide')}`; - this.add_custom_button( + frappe.utils.add_custom_button( ``, () => this.hide_or_show(), "show-or-hide-button", - title + title, + null, + this.action_area ); this.show_or_hide_button = this.action_area.find( @@ -61,18 +68,24 @@ export default class Widget { } options.allow_edit && - this.add_custom_button( + frappe.utils.add_custom_button( frappe.utils.icon("edit", "xs"), - () => this.edit() + () => this.edit(), + null, + `${__('Edit')}`, + null, + this.action_area ); if (options.allow_resize) { const title = this.width == 'Full'? `${__('Collapse')}` : `${__('Expand')}`; - this.add_custom_button( + frappe.utils.add_custom_button( '', () => this.toggle_width(), "resize-button", - title + title, + null, + this.action_area ); this.resize_button = this.action_area.find( @@ -88,12 +101,11 @@ export default class Widget { make_widget() { this.widget = $(`
-
-
+
+
@@ -114,37 +126,25 @@ export default class Widget { } set_title(max_chars) { - let base = this.label || this.name; + let base = this.title || this.label || this.name; let title = max_chars ? frappe.ellipsis(base, max_chars) : base; if (this.icon) { let icon = frappe.utils.icon(this.icon); - this.title_field[0].innerHTML = `${icon} ${title}`; + this.title_field[0].innerHTML = `${icon} ${title}`; } else { - this.title_field[0].innerHTML = title; + this.title_field[0].innerHTML = `${title}`; if (max_chars) { - this.title_field[0].setAttribute('title', this.label); + this.title_field[0].setAttribute('title', this.title || this.label); } } this.subtitle && this.subtitle_field.html(this.subtitle); } - add_custom_button(html, action, class_name = "", title="", btn_type) { - if (!btn_type) btn_type = 'btn-secondary'; - let button = $( - `` - ); - button.click(event => { - event.stopPropagation(); - action && action(); - }); - button.appendTo(this.action_area); - } - - delete(animate=true) { + delete(animate=true, dismissed=false) { let remove_widget = (setup_new) => { this.widget.remove(); - this.options.on_delete && this.options.on_delete(this.name, setup_new); + !dismissed && this.options.on_delete && this.options.on_delete(this.name, setup_new); }; if (animate) { @@ -168,8 +168,9 @@ export default class Widget { primary_action: (data) => { Object.assign(this, data); data.name = this.name; - + this.new = true; this.refresh(); + this.options.on_edit && this.options.on_edit(data); }, primary_action_label: __("Save") }); diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 0c36f013ec..44f3edc038 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -349,6 +349,8 @@ export default class ChartWidget extends Widget { } setup_filter_button() { + if (this.in_customize_mode) return; + this.is_document_type = this.chart_doc.chart_type !== "Report" && this.chart_doc.chart_type !== "Custom"; diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index 9d0cadc1d0..cc771b96b5 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -12,14 +12,18 @@ export default class LinksWidget extends Widget { return { name: this.name, links: JSON.stringify(this.links), + link_count: this.links.length, label: this.label, hidden: this.hidden, }; } set_body() { - this.options = {}; - this.options.links = this.links; + + if (!this.options) { + this.options = {}; + this.options.links = this.links; + } this.widget.addClass("links-widget-box"); const is_link_disabled = item => { return item.dependencies && item.incomplete_dependencies; @@ -81,7 +85,9 @@ export default class LinksWidget extends Widget { ${get_link_for_item(item)} `); }); - + if (this.in_customize_mode) { + this.body.empty(); + } this.link_list.forEach(link => link.appendTo(this.body)); } diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index b487c0134f..7237de2fb6 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -3,7 +3,23 @@ import Widget from "./base_widget.js"; frappe.provide("frappe.utils"); export default class OnboardingWidget extends Widget { + + async refresh() { + this.new && await this.get_onboarding_data(); + this.set_title(); + this.set_actions(); + this.set_body(); + this.setup_events(); + } + + get_config() { + return { + label: this.onboarding_name + }; + } + make_body() { + this.body.empty(); this.steps_wrapper = $(`
`).appendTo(this.body); this.step_preview = $(`
@@ -477,11 +493,13 @@ export default class OnboardingWidget extends Widget { } is_dismissed() { + if (this.in_customize_mode) return false; + let dismissed = JSON.parse( localStorage.getItem("dismissed-onboarding") || "{}" ); - if (Object.keys(dismissed).includes(this.label)) { - let last_hidden = new Date(dismissed[this.label]); + if (Object.keys(dismissed).includes(this.title)) { + let last_hidden = new Date(dismissed[this.title]); let today = new Date(); let diff = frappe.datetime.get_hour_diff(today, last_hidden); return diff < 24; @@ -490,6 +508,8 @@ export default class OnboardingWidget extends Widget { } set_actions() { + if (this.in_customize_mode) return; + this.action_area.empty(); const dismiss = $( `
${__('Dismiss', null, 'Stop showing the onboarding widget.')}
` @@ -498,14 +518,38 @@ export default class OnboardingWidget extends Widget { let dismissed = JSON.parse( localStorage.getItem("dismissed-onboarding") || "{}" ); - dismissed[this.label] = frappe.datetime.now_datetime(); + dismissed[this.title] = frappe.datetime.now_datetime(); localStorage.setItem( "dismissed-onboarding", JSON.stringify(dismissed) ); - this.delete(); + this.delete(true, true); + this.widget.closest('.ce-block').hide(); }); dismiss.appendTo(this.action_area); } + + get_onboarding_data() { + return frappe.model + .with_doc("Module Onboarding", this.onboarding_name) + .then(onboarding_doc => { + if (onboarding_doc) { + this.onboarding_doc = onboarding_doc; + this.label = onboarding_doc.label; + this.title = onboarding_doc.title || __("Let's Get Started"); + this.subtitle = onboarding_doc.subtitle; + this.success = onboarding_doc.success; + this.docs_url = onboarding_doc.docs_url; + this.user_can_dismiss = onboarding_doc.user_can_dismiss; + const method = + "frappe.desk.doctype.onboarding_step.onboarding_step.get_onboarding_steps"; + return frappe + .xcall(method, { ob_steps: onboarding_doc.steps }) + .then(steps => { + this.steps = steps; + }); + } + }); + } } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 3f5a4acd73..9262627f02 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -124,6 +124,116 @@ class ChartDialog extends WidgetDialog { } } +class OnboardingDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + return [ + { + fieldtype: "Link", + fieldname: "onboarding_name", + label: "Onboarding Name", + options: "Module Onboarding", + reqd: 1, + } + ]; + } +} + +class CardDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + let me = this; + return [ + { + fieldtype: "Data", + fieldname: "label", + label: "Label", + }, + { + fieldname: 'links', + fieldtype: 'Table', + label: __('Card Links'), + editable_grid: 1, + data: me.values ? JSON.parse(me.values.links) : [], + get_data: () => { + return me.values ? JSON.parse(me.values.links) : []; + }, + fields: [ + { + fieldname: "label", + fieldtype: "Data", + in_list_view: 1, + label: "Label" + }, + { + fieldname: "icon", + fieldtype: "Data", + label: "Icon" + }, + { + fieldname: "link_type", + fieldtype: "Select", + in_list_view: 1, + label: "Link Type", + options: ["DocType", "Page", "Report"], + onchange: (e) => { + me.link_to = e.currentTarget.value; + } + }, + { + fieldname: "link_to", + fieldtype: "Dynamic Link", + in_list_view: 1, + label: "Link To", + options: "link_type", + get_options: () => { + return me.link_to; + } + }, + { + fieldname: "column_break_7", + fieldtype: "Column Break" + }, + { + fieldname: "dependencies", + fieldtype: "Data", + label: "Dependencies" + }, + { + fieldname: "only_for", + fieldtype: "Link", + label: "Only for ", + options: "Country" + }, + { + default: "0", + fieldname: "onboard", + fieldtype: "Check", + label: "Onboard" + }, + { + default: "0", + fieldname: "is_query_report", + fieldtype: "Check", + label: "Is Query Report" + } + ], + }, + ]; + } + + process_data(data) { + data.label = data.label ? data.label : data.chart_name; + return data; + } +} + class ShortcutDialog extends WidgetDialog { constructor(opts) { super(opts); @@ -438,6 +548,8 @@ export default function get_dialog_constructor(type) { chart: ChartDialog, shortcut: ShortcutDialog, number_card: NumberCardDialog, + links: CardDialog, + onboarding: OnboardingDialog }; return widget_map[type] || WidgetDialog; diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 20a8e14540..d8f92edc5d 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -186,4 +186,51 @@ export default class WidgetGroup { } } +export class SingleWidgetGroup { + constructor(opts) { + Object.assign(this, opts); + this.widgets_list = []; + this.widgets_dict = {}; + this.widget_order = []; + this.make(); + } + + make() { + this.add_widget(this.widgets); + } + + add_widget(widget) { + let widget_object = frappe.widget.make_widget({ + ...widget, + widget_type: this.type, + container: this.container, + height: this.height || null, + options: { + ...this.options, + on_delete: () => this.on_delete(), + on_edit: () => this.on_edit(widget_object) + } + }); + this.widgets_list.push(widget_object); + this.widgets_dict[widget.name] = widget_object; + + return widget_object; + } + + on_delete() { + this.api.blocks.delete(); + } + + on_edit(widget_object) { + this.block.call("on_edit", widget_object); + } + + customize() { + this.widgets_list.forEach((wid) => { + wid.customize(this.options); + }); + } +} + frappe.widget.WidgetGroup = WidgetGroup; +frappe.widget.SingleWidgetGroup = SingleWidgetGroup; diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index ac3b1a4f7c..0db526978f 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -9,6 +9,11 @@ body { .standard-sidebar-label { font-size: var(--text-xs); text-transform: uppercase; + cursor: pointer; + + span { + pointer-events: none; + } } .standard-sidebar-section { @@ -128,19 +133,25 @@ body { .widget-head { @include flex(flex, space-between, center, null); - .widget-title { - @include flex(flex, null, center, null); - font-size: var(--text-lg); - font-family: inherit; - font-weight: 500; - line-height: 1.3em; - color: var(--heading-color); + .widget-label { + min-width: 0px; - svg { - margin-right: 6px; - box-shadow: none; + .widget-title { + @include flex(flex, null, center, null); + font-size: var(--text-lg); + font-family: inherit; + font-weight: 500; + line-height: 1.3em; + color: var(--heading-color); + + svg { + flex: none; + margin-right: 6px; + box-shadow: none; + } } } + .widget-control { @include flex(flex, null, center, row-reverse); @@ -781,4 +792,263 @@ body { } } } + + .block-menu-item-icon svg{ + width: 12px; + height: 12px; + margin-right: 5px; + } + + .standard-sidebar-item { + justify-content: space-between; + padding: 0px; + + .sidebar-item-control { + + > * { + align-self: center; + margin-left: 3px; + box-shadow: none; + } + + .drag-handle { + cursor: all-scroll; + cursor: -webkit-grabbing; + display: none; + } + + .delete-page { + display: none; + } + + .drop-icon { + padding: 10px 12px 10px 2px; + } + + .sidebar-info { + display: none; + } + + svg { + margin-right: 0; + } + } + + .sidebar-item-label { + flex: 1; + } + + .item-anchor { + display: flex; + overflow: hidden; + padding: 8px 0px 8px 12px; + flex: 1; + } + } + + .sidebar-item-container { + .sidebar-item-container{ + margin-left: 10px; + + .standard-sidebar-item { + justify-content: start; + } + } + } + + .standard-sidebar-section.show-control { + .desk-sidebar-item.standard-sidebar-item { + + &:hover, &.selected { + .drag-handle { + display: inline-block; + } + + .delete-page { + display: inline-block; + margin-right: 8px; + } + + .sidebar-info { + display: inline-block; + margin-right: 8px; + } + + .drop-icon { + padding: 10px 8px 10px 2px; + margin-left: -4px; + } + } + + .block-click { + pointer-events:none; + } + } + } + + .codex-editor { + min-height: 630px; + + .codex-editor__redactor{ + display: flex; + flex-wrap: wrap; + flex-direction: row; + margin: 0px -7px; + padding-bottom: 20px !important; + + .ce-block{ + width: 100%; + padding-left: 0; + padding-right: 0; + + &.ce-block--selected { + .ce-block__content { + background-color: inherit; + } + } + + .ce-block__content { + max-width: 100%; + height: 100%; + padding: 7px; + + &> div { + height: 100%; + } + + .tune-btn > * { + pointer-events: none; + } + + .ce-header { + padding: 0 !important; + margin-bottom: 0 !important; + flex: 1; + } + + .widget{ + &.header { + display: flex; + justify-content: center; + flex: 1; + padding-left: 15px !important; + padding-right: 15px !important; + min-height: 50px; + box-shadow: none; + background-color: var(--control-bg); + color: var(--text-muted); + } + + &:focus { + outline: none; + } + + &.new-widget { + align-items: inherit; + } + + .paragraph-control { + display: flex; + flex-direction: row-reverse; + position: absolute; + right: 20px; + gap: 5px; + background-color: var(--card-bg); + padding-left: 5px; + + .drag-handle { + cursor: all-scroll; + cursor: -webkit-grabbing; + } + } + } + } + } + } + + svg { + fill: none; + } + + .ce-toolbar { + svg { + fill: currentColor; + } + + .icon { + stroke: none; + width: fit-content; + height: fit-content; + } + + .ce-settings { + width: fit-content; + + .ce-settings__button, .cdx-settings-button { + color: #707684; + } + + .cdx-settings-button--active { + color: #388ae5; + } + + .cdx-settings-button.disabled{ + pointer-events: none; + opacity: .5 + } + .cdx-settings-sidebar{ + position: absolute; + right: 100%; + top:0; + background: #fff; + width: 108px; + height: 145px; + box-shadow: 0 3px 15px -3px rgba(13,20,33,.13); + border-radius: 0 4px 4px 0;z-index: 0; + } + } + + .ce-toolbar__settings-btn { + display: none; + } + } + + .ce-inline-tool, .ce-inline-toolbar__dropdown { + .icon { + fill: currentColor; + } + } + + @media (min-width: 1199px) { + .ce-toolbar__content { + max-width: 930px; + } + } + @media (max-width: 995px) { + .ce-toolbar__content { + max-width: 760px; + } + } + + @media (max-width: 1199px) { + .ce-block.col-4 { + flex: 0 0 50%; + max-width: 50%; + } + } + + @media (max-width: 750px) { + .ce-block.col-4 { + flex: 0 0 100%; + max-width: 100%; + } + } + @media (max-width: 750px) { + .ce-block.col-6 { + flex: 0 0 100%; + max-width: 100%; + } + } + + } } diff --git a/frappe/website/workspace/website/website.json b/frappe/website/workspace/website/website.json index a2a4a299c4..8d22f84b5e 100644 --- a/frappe/website/workspace/website/website.json +++ b/frappe/website/workspace/website/website.json @@ -1,20 +1,20 @@ { - "category": "Modules", - "charts": [ - { - "chart_name": "Website Analytics" - } - ], + "category": "", + "charts": [], + "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Website\", \"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Blog Post\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Blogger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Web Page\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Web Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Website Settings\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Setup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Blog\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Web Site\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Portal\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Knowledge Base\", \"col\": 4}}]", "creation": "2020-03-02 14:13:51.089373", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends": "", "extends_another_page": 0, + "for_user": "", "hide_custom": 0, "icon": "website", "idx": 0, - "is_standard": 1, + "is_default": 0, + "is_standard": 0, "label": "Website", "links": [ { @@ -22,6 +22,7 @@ "icon": "setting", "is_query_report": 0, "label": "Setup", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -30,6 +31,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Settings", + "link_count": 0, "link_to": "Website Settings", "link_type": "DocType", "onboard": 1, @@ -40,6 +42,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Theme", + "link_count": 0, "link_to": "Website Theme", "link_type": "DocType", "onboard": 1, @@ -50,6 +53,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Script", + "link_count": 0, "link_to": "Website Script", "link_type": "DocType", "onboard": 0, @@ -60,6 +64,7 @@ "hidden": 0, "is_query_report": 0, "label": "About Us Settings", + "link_count": 0, "link_to": "About Us Settings", "link_type": "DocType", "onboard": 0, @@ -70,6 +75,7 @@ "hidden": 0, "is_query_report": 0, "label": "Contact Us Settings", + "link_count": 0, "link_to": "Contact Us Settings", "link_type": "DocType", "onboard": 0, @@ -80,6 +86,7 @@ "icon": "", "is_query_report": 0, "label": "Blog", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -88,6 +95,7 @@ "hidden": 0, "is_query_report": 0, "label": "Blog Post", + "link_count": 0, "link_to": "Blog Post", "link_type": "DocType", "onboard": 1, @@ -98,6 +106,7 @@ "hidden": 0, "is_query_report": 0, "label": "Blogger", + "link_count": 0, "link_to": "Blogger", "link_type": "DocType", "onboard": 0, @@ -108,6 +117,7 @@ "hidden": 0, "is_query_report": 0, "label": "Blog Category", + "link_count": 0, "link_to": "Blog Category", "link_type": "DocType", "onboard": 0, @@ -118,6 +128,7 @@ "icon": "website", "is_query_report": 0, "label": "Web Site", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -126,6 +137,7 @@ "hidden": 0, "is_query_report": 0, "label": "Web Page", + "link_count": 0, "link_to": "Web Page", "link_type": "DocType", "onboard": 1, @@ -136,6 +148,7 @@ "hidden": 0, "is_query_report": 0, "label": "Web Form", + "link_count": 0, "link_to": "Web Form", "link_type": "DocType", "onboard": 1, @@ -146,6 +159,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Sidebar", + "link_count": 0, "link_to": "Website Sidebar", "link_type": "DocType", "onboard": 0, @@ -156,6 +170,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Slideshow", + "link_count": 0, "link_to": "Website Slideshow", "link_type": "DocType", "onboard": 0, @@ -166,6 +181,7 @@ "hidden": 0, "is_query_report": 0, "label": "Website Route Meta", + "link_count": 0, "link_to": "Website Route Meta", "link_type": "DocType", "onboard": 0, @@ -176,6 +192,7 @@ "icon": "website", "is_query_report": 0, "label": "Portal", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -184,6 +201,7 @@ "hidden": 0, "is_query_report": 0, "label": "Portal Settings", + "link_count": 0, "link_to": "Portal Settings", "link_type": "DocType", "onboard": 1, @@ -194,6 +212,7 @@ "icon": "project", "is_query_report": 0, "label": "Knowledge Base", + "link_count": 0, "onboard": 0, "type": "Card Break" }, @@ -202,6 +221,7 @@ "hidden": 0, "is_query_report": 0, "label": "Help Category", + "link_count": 0, "link_to": "Help Category", "link_type": "DocType", "onboard": 0, @@ -212,20 +232,26 @@ "hidden": 0, "is_query_report": 0, "label": "Help Article", + "link_count": 0, "link_to": "Help Article", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2020-12-01 13:38:39.556588", + "modified": "2021-08-05 12:16:03.154032", "modified_by": "Administrator", "module": "Website", "name": "Website", "onboarding": "Website", "owner": "Administrator", + "parent_page": "", "pin_to_bottom": 0, "pin_to_top": 0, + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 28, "shortcuts": [ { "color": "Green", @@ -261,5 +287,6 @@ "link_to": "Website Settings", "type": "DocType" } - ] + ], + "title": "Website" } \ No newline at end of file diff --git a/package.json b/package.json index 5b9504e142..1ddbec178e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "homepage": "https://frappeframework.com", "dependencies": { + "@editorjs/editorjs": "2.20.0", "ace-builds": "^1.4.8", "air-datepicker": "github:frappe/air-datepicker", "autoprefixer": "^9.8.6", @@ -30,6 +31,7 @@ "cropperjs": "^1.5.12", "cssnano": "^5.0.0", "driver.js": "^0.9.8", + "editorjs-undo": "0.1.6", "express": "^4.17.1", "fast-deep-equal": "^2.0.1", "frappe-charts": "^2.0.0-rc13", diff --git a/yarn.lock b/yarn.lock index 387d63672d..e8f527b7f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,14 @@ resolved "https://registry.yarnpkg.com/@deepcode/dcignore/-/dcignore-1.0.2.tgz#39e4a3df7dde8811925330506e4bb3fbf3c288d8" integrity sha512-DPgxtHuJwBORpqRkPXzzOT+uoPRVJmaN7LR+pmeL6DQM90kj6G6GFUH1i/YpRH8NbML8ZGEDwB9f9u4UwD2pzg== +"@editorjs/editorjs@2.20.0": + version "2.20.0" + resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.20.0.tgz#1e0dc7e6c1433c34c9d2d3e42153313c2dbb3514" + integrity sha512-e6DWi8bMypFhovq9R6cefaDWVfrlVU++Q7ABp79+MxZIuC/SKAW5EtxBbKPL22H/Mc3bJIhZCxOqEl70HBh2yw== + dependencies: + codex-notifier "^1.1.2" + codex-tooltip "^1.0.1" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" @@ -1510,6 +1518,16 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codex-notifier@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895" + integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg== + +codex-tooltip@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.2.tgz#81a9d3e2937658c6e5312106b47b9f094ff7be63" + integrity sha512-oC+Bu5X/zyhbPydgMSLWKoM/+vkJMqaLWu3Dt/jZgXS3MWK23INwC5DMBrVXZSufAFk0i0SUni38k9rLMyZn/w== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2168,6 +2186,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +editorjs-undo@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/editorjs-undo/-/editorjs-undo-0.1.6.tgz#823349a1e9a78d8bc68ba8570a2b854063bc804a" + integrity sha512-zVHPnBf2mcI8hWT9Eu8H3bGDEcMj4gppXbQjJW11Aa8Kdy2SVBGhM6fS59OUlBsm8iHWLxuoG2NUIfy9Rd30sw== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"