diff --git a/cypress/integration/control_icon.js b/cypress/integration/control_icon.js index 5c531a0823..d89eba8840 100644 --- a/cypress/integration/control_icon.js +++ b/cypress/integration/control_icon.js @@ -19,18 +19,18 @@ context('Control Icon', () => { get_dialog_with_icon().as('dialog'); cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click(); - cy.get('.icon-picker .icon-wrapper[id=active]').first().click(); - cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active'); + cy.get('.icon-picker .icon-wrapper[id=heart-active]').first().click(); + cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart-active'); cy.get('@dialog').then(dialog => { let value = dialog.get_value('icon'); - expect(value).to.equal('active'); + expect(value).to.equal('heart-active'); }); - cy.get('.icon-picker .icon-wrapper[id=resting]').first().click(); - cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting'); + cy.get('.icon-picker .icon-wrapper[id=heart]').first().click(); + cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart'); cy.get('@dialog').then(dialog => { let value = dialog.get_value('icon'); - expect(value).to.equal('resting'); + expect(value).to.equal('heart'); }); }); diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index 65586366e6..9d6eeaff64 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -33,44 +33,39 @@ context('Workspace 2.0', () => { }); 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').click().type('{enter}'); + cy.get('.block-list-container .block-list-item').contains('Heading').click(); cy.get(":focus").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').click().type('{enter}'); + cy.get('.block-list-container .block-list-item').contains('Text').click(); cy.get(":focus").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(":focus").click(); + cy.get('.paragraph-control .setting-btn').click(); + cy.get('.paragraph-control .dropdown-item').contains('Delete').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(":focus").click(); + cy.get('.ce-block:last .setting-btn').click(); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-11'); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-10'); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-9'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-10'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-11'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-12'); cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); }); @@ -79,7 +74,10 @@ context('Workspace 2.0', () => { cy.get('.codex-editor__redactor .ce-block'); cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); - cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click(); + cy.get('.sidebar-item-container[item-name="Test Private Page"]') + .find('.sidebar-item-control .setting-btn').click(); + cy.get('.sidebar-item-container[item-name="Test Private Page"]') + .find('.dropdown-item[title="Delete Workspace"]').click({force: true}); 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(); diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json index fa2606dc43..40b265b34f 100644 --- a/frappe/automation/workspace/tools/tools.json +++ b/frappe/automation/workspace/tools/tools.json @@ -1,6 +1,6 @@ { "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}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -208,7 +208,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:02.839181", + "modified": "2022-01-13 17:48:48.456763", "modified_by": "Administrator", "module": "Automation", "name": "Tools", @@ -217,7 +217,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 26, + "sequence_id": 26.0, "shortcuts": [ { "label": "ToDo", diff --git a/frappe/boot.py b/frappe/boot.py index d5d992343a..f1fd0f6a3b 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -107,8 +107,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_wspace_sidebar_items - bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages') + from frappe.desk.desktop import get_workspace_sidebar_items + bootinfo.allowed_workspaces = get_workspace_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 aabb4f9d1c..c1c506ae3a 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -1,6 +1,6 @@ { "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}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"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}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]", "creation": "2021-01-02 10:51:16.579957", "docstatus": 0, "doctype": "Workspace", @@ -222,7 +222,7 @@ "type": "Link" } ], - "modified": "2021-09-05 21:14:52.384816", + "modified": "2022-01-13 17:26:02.736366", "modified_by": "Administrator", "module": "Core", "name": "Build", @@ -231,7 +231,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 5, + "sequence_id": 5.0, "shortcuts": [ { "doc_view": "", diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json index 917ce2cbdc..5aadbc42d5 100644 --- a/frappe/core/workspace/settings/settings.json +++ b/frappe/core/workspace/settings/settings.json @@ -1,6 +1,6 @@ { "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}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -367,7 +367,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.456174", + "modified": "2022-01-13 17:49:59.586909", "modified_by": "Administrator", "module": "Core", "name": "Settings", @@ -376,7 +376,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 29, + "sequence_id": 29.0, "shortcuts": [ { "icon": "setting", diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json index 85c110151b..5741c54eeb 100644 --- a/frappe/core/workspace/users/users.json +++ b/frappe/core/workspace/users/users.json @@ -1,6 +1,6 @@ { "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}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -145,7 +145,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.010205", + "modified": "2022-01-13 17:49:08.912772", "modified_by": "Administrator", "module": "Core", "name": "Users", @@ -154,7 +154,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 27, + "sequence_id": 27.0, "shortcuts": [ { "label": "User", diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json index 8938bdec9c..1756abcb1d 100644 --- a/frappe/custom/workspace/customization/customization.json +++ b/frappe/custom/workspace/customization/customization.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"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\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"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}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -123,7 +123,7 @@ "type": "Link" } ], - "modified": "2021-11-24 16:20:03.500885", + "modified": "2022-01-13 17:28:08.345794", "modified_by": "Administrator", "module": "Custom", "name": "Customization", @@ -132,7 +132,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 8, + "sequence_id": 8.0, "shortcuts": [ { "label": "Customize Form", diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index e1789852f1..4164db679d 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -56,31 +56,6 @@ class Workspace: 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) - shortcuts = self.doc.shortcuts - - for section in cards: - links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links') - for item in links: - if self.is_item_allowed(item.get('link_to'), item.get('link_type')): - return True - - def _in_active_domains(item): - if not item.restrict_to_domain: - return True - else: - return item.restrict_to_domain in frappe.get_active_domains() - - for item in shortcuts: - 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 @@ -346,20 +321,20 @@ def get_desktop_page(page): dict: dictionary of cards, charts and shortcuts to be displayed on website """ try: - wspace = Workspace(loads(page)) - wspace.build_workspace() + workspace = Workspace(loads(page)) + workspace.build_workspace() return { - 'charts': wspace.charts, - 'shortcuts': wspace.shortcuts, - 'cards': wspace.cards, - 'onboardings': wspace.onboardings + 'charts': workspace.charts, + 'shortcuts': workspace.shortcuts, + 'cards': workspace.cards, + 'onboardings': workspace.onboardings } except DoesNotExistError: frappe.log_error(frappe.get_traceback()) return {} @frappe.whitelist() -def get_wspace_sidebar_items(): +def get_workspace_sidebar_items(): """Get list of sidebar items for desk""" has_access = "Workspace Manager" in frappe.get_roles() @@ -385,8 +360,8 @@ def get_wspace_sidebar_items(): # Filter Page based on Permission for page in all_pages: try: - wspace = Workspace(page, True) - if wspace.is_permitted() and wspace.is_page_allowed() or has_access: + workspace = Workspace(page, True) + if has_access or workspace.is_permitted(): if page.public: pages.append(page) elif page.for_user == frappe.session.user: @@ -453,25 +428,24 @@ def get_custom_report_list(module): return out def save_new_widget(doc, page, blocks, new_widgets): + if loads(new_widgets): + widgets = _dict(loads(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) + 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) + 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) + json_config = widgets and dumps(widgets, sort_keys=True, indent=4) # Error log body log = \ diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 04975c69e3..211029dfcf 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "field:label", "beta": 1, "creation": "2020-01-23 13:45:59.470592", @@ -141,7 +142,7 @@ }, { "fieldname": "sequence_id", - "fieldtype": "Int", + "fieldtype": "Float", "label": "Sequence Id" }, { @@ -158,7 +159,7 @@ ], "in_create": 1, "links": [], - "modified": "2021-09-16 12:01:06.450622", + "modified": "2021-12-15 19:33:00.805265", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 94114e3918..b40f517350 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.model.rename_doc import rename_doc from frappe.desk.desktop import save_new_widget from frappe.desk.utils import validate_route_conflict @@ -121,77 +122,157 @@ 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 new_page(new_page): + if not loads(new_page): + return + + page = loads(new_page) + + if page.get("public") and not is_workspace_manager(): + return + + doc = frappe.new_doc('Workspace') + doc.title = page.get('title') + doc.icon = page.get('icon') + doc.content = page.get('content') + doc.parent_page = page.get('parent_page') + doc.label = page.get('label') + doc.for_user = page.get('for_user') + doc.public = page.get('public') + doc.sequence_id = last_sequence_id(doc) + 1 + doc.save(ignore_permissions=True) + + return doc @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) +def save_page(title, public, new_widgets, blocks): public = frappe.parse_json(public) - if save: - doc = frappe.new_doc('Workspace') + + filters = { + 'public': public, + 'label': title + } + + if not public: + 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) + + save_new_widget(doc, title, blocks, new_widgets) + + return {"name": title, "public": public, "label": doc.label} + +@frappe.whitelist() +def update_page(name, title, icon, parent, public): + public = frappe.parse_json(public) + + doc = frappe.get_doc("Workspace", name) + + filters = { + 'parent_page': doc.title, + 'public': doc.public + } + child_docs = frappe.get_list("Workspace", filters=filters) + + if doc: 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 + if doc.public != public: + doc.sequence_id = frappe.db.count('Workspace', {'public':public}, cache=True) + doc.public = public + doc.for_user = '' if public else doc.for_user or frappe.session.user + doc.label = '{0}-{1}'.format(title, doc.for_user) if doc.for_user else title doc.save(ignore_permissions=True) - if loads(new_widgets): - save_new_widget(doc, title, blocks, new_widgets) + if name != doc.label: + rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True) - if loads(sb_public_items) or loads(sb_private_items): - sort_pages(loads(sb_public_items), loads(sb_private_items)) + # update new name and public in child pages + if child_docs: + for child in child_docs: + child_doc = frappe.get_doc("Workspace", child.name) + child_doc.parent_page = doc.title + child_doc.public = doc.public + child_doc.save(ignore_permissions=True) - if loads(deleted_pages): - return delete_pages(loads(deleted_pages)) + return {"name": doc.title, "public": doc.public, "label": doc.label} - return {"name": title, "public": public, "label": doc.label} +@frappe.whitelist() +def duplicate_page(page_name, new_page): + if not loads(new_page): + return + + new_page = loads(new_page) + + if new_page.get("is_public") and not is_workspace_manager(): + return + + old_doc = frappe.get_doc("Workspace", page_name) + doc = frappe.copy_doc(old_doc) + doc.title = new_page.get('title') + doc.icon = new_page.get('icon') + doc.parent_page = new_page.get('parent') or '' + doc.public = new_page.get('is_public') + doc.for_user = '' + doc.label = doc.title + if not doc.public: + doc.for_user = doc.for_user or frappe.session.user + doc.label = '{0}-{1}'.format(doc.title, doc.for_user) + doc.name = doc.label + if old_doc.public == doc.public: + doc.sequence_id += 0.1 + else: + doc.sequence_id = last_sequence_id(doc) + 1 + doc.insert(ignore_permissions=True) -def delete_pages(deleted_pages): - for page in deleted_pages: - if page.get("public") and not is_workspace_manager(): - return {"name": page.get("title"), "public": 1, "label": page.get("label")} + return doc - if frappe.db.exists("Workspace", page.get("name")): - frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) +@frappe.whitelist() +def delete_page(page): + if not loads(page): + return + + page = loads(page) + + if page.get("public") and not is_workspace_manager(): + return + + if frappe.db.exists("Workspace", page.get("name")): + frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) - return {"name": "Home", "public": 1, "label": "Home"} + return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")} +@frappe.whitelist() 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 not loads(sb_public_items) and not loads(sb_private_items): + return + + sb_public_items = loads(sb_public_items) + sb_private_items = loads(sb_private_items) + + workspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) + workspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user}) if sb_private_items: - sort_page(wspace_private_pages, sb_private_items) + return sort_page(workspace_private_pages, sb_private_items) if sb_public_items and is_workspace_manager(): - sort_page(wspace_public_pages, sb_public_items) + return sort_page(workspace_public_pages, sb_public_items) -def sort_page(wspace_pages, pages): + return False + +def sort_page(workspace_pages, pages): for seq, d in enumerate(pages): - for page in wspace_pages: + for page in workspace_pages: if page.title == d.get('title'): doc = frappe.get_doc('Workspace', page.name) doc.sequence_id = seq + 1 @@ -199,6 +280,27 @@ def sort_page(wspace_pages, pages): doc.save(ignore_permissions=True) break + return True + +def last_sequence_id(doc): + doc_exists = frappe.db.exists({ + 'doctype': 'Workspace', + 'public': doc.public, + 'for_user': doc.for_user + }) + + if not doc_exists: + return 0 + + return frappe.db.get_list('Workspace', + fields=['sequence_id'], + filters={ + 'public': doc.public, + 'for_user': doc.for_user + }, + order_by="sequence_id desc" + )[0].sequence_id + def get_page_list(fields, filters): return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc') diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json index b85056e3ef..bbd2e1199f 100644 --- a/frappe/integrations/workspace/integrations/integrations.json +++ b/frappe/integrations/workspace/integrations/integrations.json @@ -1,6 +1,6 @@ { "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}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -260,7 +260,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:00.355268", + "modified": "2022-01-13 17:39:01.292154", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", @@ -269,7 +269,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 15, + "sequence_id": 15.0, "shortcuts": [], "title": "Integrations" } \ No newline at end of file diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py index 82076c4328..a4b057b989 100644 --- a/frappe/patches/v14_0/update_workspace2.py +++ b/frappe/patches/v14_0/update_workspace2.py @@ -5,10 +5,10 @@ from frappe import _ def execute(): frappe.reload_doc('desk', 'doctype', 'workspace', force=True) - for seq, wspace in enumerate(frappe.get_all('Workspace', order_by='name asc')): - doc = frappe.get_doc('Workspace', wspace.name) + for seq, workspace in enumerate(frappe.get_all('Workspace', order_by='name asc')): + doc = frappe.get_doc('Workspace', workspace.name) content = create_content(doc) - update_wspace(doc, seq, content) + update_workspace(doc, seq, content) frappe.db.commit() def create_content(doc): @@ -49,7 +49,7 @@ def create_content(doc): del doc.links[doc.links.index(l)] return content -def update_wspace(doc, seq, content): +def update_workspace(doc, seq, content): if not doc.title and not doc.content and not doc.is_standard and not doc.public: doc.sequence_id = seq + 1 doc.content = json.dumps(content) diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index b878f713e9..f2977e3016 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -1,217 +1,262 @@ diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index cac02c7a68..e056a34be2 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -96,6 +96,7 @@ import "./frappe/ui/sort_selector.js"; import "./frappe/change_log.html"; import "./frappe/ui/workspace_loading_skeleton.html"; +import "./frappe/ui/workspace_sidebar_loading_skeleton.html"; import "./frappe/desk.js"; import "./frappe/query_string.js"; diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 2c5661ca87..ea9ceb35f3 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f get_options() { let options = ''; if (this.df.get_options) { - options = this.df.get_options(); + options = this.df.get_options(this); } else if (this.docname==null && cur_dialog) { //for dialog box options = cur_dialog.get_value(this.df.options); diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 4fcf0dbd14..7bea7f0584 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -133,14 +133,14 @@ frappe.router = { // /app/user/user-001 = ["Form", "User", "user-001"] // /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] - let private_wspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`; + let private_workspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`; if (frappe.workspaces[route[0]]) { // public workspace route = ['Workspaces', frappe.workspaces[route[0]].title]; - } else if (route[0] == 'private' && frappe.workspaces[private_wspace]) { + } else if (route[0] == 'private' && frappe.workspaces[private_workspace]) { // private workspace - route = ['Workspaces', 'private', frappe.workspaces[private_wspace].title]; + route = ['Workspaces', 'private', frappe.workspaces[private_workspace].title]; } else if (this.routes[route[0]]) { // route route = this.set_doctype_route(route); diff --git a/frappe/public/js/frappe/ui/workspace_sidebar_loading_skeleton.html b/frappe/public/js/frappe/ui/workspace_sidebar_loading_skeleton.html new file mode 100644 index 0000000000..4f20e3c21c --- /dev/null +++ b/frappe/public/js/frappe/ui/workspace_sidebar_loading_skeleton.html @@ -0,0 +1,22 @@ +
+
+ + + + +
+
+ + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index a3a8f96b11..725296a121 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -243,9 +243,28 @@ Object.assign(frappe.utils, { '=': '=' }; - return String(txt).replace(/[&<>"'`=/]/g, function(char) { - return escape_html_mapping[char]; - }); + return String(txt).replace( + /[&<>"'`=/]/g, + char => escape_html_mapping[char] || char + ); + }, + + unescape_html: function(txt) { + let unescape_html_mapping = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '/': '/', + '`': '`', + '=': '=' + }; + + return String(txt).replace( + /&|<|>|"|'|/|`|=/g, + char => unescape_html_mapping[char] || char + ); }, html2text: function(html) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/block.js b/frappe/public/js/frappe/views/workspace/blocks/block.js index aed3c2f727..f4d4eca6cc 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/block.js +++ b/frappe/public/js/frappe/views/workspace/blocks/block.js @@ -7,7 +7,7 @@ export default class Block { make(block, block_name, widget_type = block) { let block_data = this.config.page_data[block+'s'].items.find(obj => { - return obj.label == block_name; + return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name); }); if (!block_data) return false; this.wrapper.innerHTML = ''; @@ -28,12 +28,64 @@ export default class Block { return true; } - rendered() { - var e = this.wrapper.closest('.ce-block'); - e.classList.add("col-" + this.get_col()); + rendered(wrapper) { + if (wrapper) this.wrapper = wrapper; + !this.readOnly && this.resizer(); + let block = this.wrapper.closest('.ce-block'); + this.set_col_class(block, this.get_col()); + } + + resizer() { + this.wrapper.className = this.wrapper.className + ' resizable'; + var resizer = document.createElement('div'); + resizer.className = 'resizer'; + this.wrapper.parentElement.appendChild(resizer); + resizer.addEventListener('mousedown', init_drag, false); + let me = this; + var startX, startWidth; + + function init_drag(e) { + startX = e.clientX; + startWidth = this.parentElement.offsetWidth; + document.documentElement.addEventListener('mousemove', do_drag, false); + document.documentElement.addEventListener('mouseup', stop_drag, false); + } + + function do_drag(e) { + $(this).css("cursor", "col-resize"); + $('.widget').css("pointer-events", "none"); + $(me.wrapper.parentElement).find('.resizer').css("border-right", "3px solid var(--gray-400)"); + un_focus(); + if ((startWidth + e.clientX - startX) - startWidth > 60) { + startX = e.clientX; + me.increase_width(); + } else if ((startWidth + e.clientX - startX) - startWidth < -60) { + startX = e.clientX; + me.decrease_width(); + } + } + + // disable text selection on mousedown (on drag) + function un_focus() { + if (document.selection) { + document.selection.empty(); + } else { + window.getSelection().removeAllRanges(); + } + } + + function stop_drag() { + $(this).css("cursor", "default"); + $('.widget').css("pointer-events", "auto"); + $(me.wrapper.parentElement).find('.resizer').css("border-right", "0px solid transparent"); + + document.documentElement.removeEventListener('mousemove', do_drag, false); + document.documentElement.removeEventListener('mouseup', stop_drag, false); + } } new(block, widget_type = block) { + let me = this; const dialog_class = get_dialog_constructor(widget_type); let block_name = block+'_name'; this.dialog = new dialog_class({ @@ -53,13 +105,18 @@ export default class Block { }); this.block_widget.customize(this.options); this.wrapper.setAttribute(block_name, this.block_widget.label); + $(this.wrapper).find('.widget').addClass(`${widget_type} edit-mode`); this.new_block_widget = this.block_widget.get_config(); - this.add_tune_button(); + this.add_settings_button(); }, }); if (!this.readOnly && this.data && !this.data[block_name]) { this.dialog.make(); + + this.dialog.dialog.get_close_btn().click(() => { + me.wrapper.closest('.ce-block').remove(); + }); } } @@ -74,42 +131,203 @@ export default class Block { 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); + add_new_block_button() { + let $new_button = $(` +
${frappe.utils.icon('add-round', 'lg')}
+ `); + + $new_button.appendTo(this.wrapper); + + $new_button.click(event => { + event.stopPropagation(); + let index = this.api.blocks.getCurrentBlockIndex() + 1; + this.api.blocks.insert('paragraph', {}, {}, index); + this.api.caret.setToBlock(index); + }); + } + + add_settings_button() { + let me = this; + this.dropdown_list = [ + { + label: 'Delete', + title: 'Delete Block', + icon: frappe.utils.icon('delete-active', 'sm'), + action: () => this.api.blocks.delete() + }, + { + label: 'Expand', + title: 'Expand Block', + icon: frappe.utils.icon('expand-alt', 'sm'), + action: () => this.increase_width() + }, + { + label: 'Shrink', + title: 'Shrink Block', + icon: frappe.utils.icon('shrink', 'sm'), + action: () => this.decrease_width() + }, + { + label: 'Move Up', + title: 'Move Up', + icon: frappe.utils.icon('up-arrow', 'sm'), + action: () => this.move_block('up') }, - "tune-btn", - `${__('Tune')}`, - null, - $widget_control, - true - ); + { + label: 'Move Down', + title: 'Move Down', + icon: frappe.utils.icon('down-arrow', 'sm'), + action: () => this.move_block('down') + } + ]; + + let $widget_control = $(this.wrapper).find('.widget-control'); + + let $button = $(` + + `); + + + let dropdown_item = function(label, title, icon, action) { + let html = $(` + + `); + + html.click(event => { + event.stopPropagation(); + action && action(); + }); + + return html; + }; + + $button.click(event => { + event.stopPropagation(); + $button.find('.dropdown-list').toggleClass('hidden'); + }); + + $(document).click(() => { + $button.find('.dropdown-list').addClass('hidden'); + }); + + $widget_control.prepend($button); + + this.dropdown_list.forEach((item) => { + if ((item.label == 'Expand' || item.label == 'Shrink') && + me.options && !me.options.allow_resize) { + return; + } + $button.find('.dropdown-list').append(dropdown_item(item.label, item.title, item.icon, item.action)); + }); } get_col() { let col = this.col || 12; - let class_name = "col-12"; + let class_name = "col-xs-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); + if (cn.match(col_class)) { + class_name = cn; + } }); let parts = class_name.split("-"); - col = parseInt(parts[1]); + col = parseInt(parts[2]); } return col; } + + decrease_width() { + this.update_width('decrease'); + } + + increase_width() { + this.update_width('increase'); + } + + update_width(action) { + let min_width = this.options && this.options.min_width || 3; + const current_block_index = this.api.blocks.getCurrentBlockIndex(); + if (current_block_index < 0) { + return; + } + + let current_block = this.api.blocks.getBlockByIndex(current_block_index); + if (!current_block) { + return; + } + + const current_block_element = current_block.holder; + + let className = 'col-xs-12'; + const colClass = new RegExp(/\bcol-.+?\b/, 'g'); + if (current_block_element.className.match(colClass)) { + current_block_element.classList.forEach( cn => { + if (cn.match(colClass)) { + className = cn; + } + }); + let parts = className.split('-'); + let width = parseInt(parts[2]); + + let condition = true; + + if (action == 'increase') { + condition = width <= 11; + width = width + 1; + } else if (action == 'decrease') { + condition = width > min_width; + width = width - 1; + } + + if (condition) { + this.set_col_class(current_block_element, width); + } + } + } + + set_col_class(node, width) { + let classes = $.grep(node.classList, function (item) { + return item.indexOf("col-") !== 0; + }); + + node.classList = ''; + + classes.forEach(cl => { + node.classList.add(cl); + }); + + let col = 'col-xs-12'; + if (width <= 12 && width >= 7) { + col = 'col-xs-' + width; + } else if (width == 6 || width == 5) { + node.classList.add('col-xs-12'); + col = 'col-sm-' + width; + } else if (width == 4) { + node.classList.add('col-xs-12'); + node.classList.add('col-sm-6'); + col = 'col-md-' + width; + } else if (width == 3) { + node.classList.add('col-xs-12'); + node.classList.add('col-sm-6'); + node.classList.add('col-md-4'); + col = 'col-lg-' + width; + } + node.classList.add(col); + } + + move_block(direction) { + let current_index = this.api.blocks.getCurrentBlockIndex(); + let new_index = current_index + (direction == 'down' ? 1 : -1); + this.api.blocks.move(new_index, current_index); + } } \ 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 index 9b4a2ed14f..9ce6ce8b4d 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -3,7 +3,7 @@ export default class Card extends Block { static get toolbox() { return { title: 'Card', - icon: '' + icon: frappe.utils.icon('card', 'sm') }; } @@ -22,6 +22,7 @@ export default class Card extends Block { allow_delete: this.allow_customization, allow_hiding: false, allow_edit: true, + allow_resize: true }; } @@ -35,7 +36,9 @@ export default class Card extends Block { } if (!this.readOnly) { - this.add_tune_button(); + $(this.wrapper).find('.widget').addClass('links edit-mode'); + this.add_settings_button(); + this.add_new_block_button(); } return this.wrapper; @@ -49,9 +52,9 @@ export default class Card extends Block { return true; } - save(blockContent) { + save() { return { - card_name: blockContent.getAttribute('card_name'), + card_name: this.wrapper.getAttribute('card_name'), col: this.get_col(), new: this.new_block_widget }; diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js index 02e6a66e6f..ccef1fa15f 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/chart.js +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -3,7 +3,7 @@ export default class Chart extends Block { static get toolbox() { return { title: 'Chart', - icon: '' + icon: frappe.utils.icon('chart', 'sm') }; } @@ -21,7 +21,9 @@ export default class Chart extends Block { allow_delete: this.allow_customization, allow_hiding: false, allow_edit: true, - max_widget_count: 2, + allow_resize: true, + min_width: 6, + max_widget_count: 2 }; } @@ -35,7 +37,9 @@ export default class Chart extends Block { } if (!this.readOnly) { - this.add_tune_button(); + $(this.wrapper).find('.widget').addClass('chart edit-mode'); + this.add_settings_button(); + this.add_new_block_button(); } return this.wrapper; @@ -49,9 +53,9 @@ export default class Chart extends Block { return true; } - save(blockContent) { + save() { return { - chart_name: blockContent.getAttribute('chart_name'), + chart_name: this.wrapper.getAttribute('chart_name'), col: this.get_col(), new: this.new_block_widget }; diff --git a/frappe/public/js/frappe/views/workspace/blocks/header.js b/frappe/public/js/frappe/views/workspace/blocks/header.js index d88bc42af9..29ffb0a828 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/header.js +++ b/frappe/public/js/frappe/views/workspace/blocks/header.js @@ -4,16 +4,8 @@ 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; @@ -27,8 +19,7 @@ export default class Header extends Block { data = {}; } - newData.text = (data.text && __(data.text.replace(/(\n|\t)/gm, ""))) || ''; - newData.level = parseInt(data.level) || this.defaultLevel.number; + newData.text = data.text || ''; newData.col = parseInt(data.col) || 12; return newData; @@ -36,7 +27,6 @@ export default class Header extends Block { render() { this.wrapper = document.createElement('div'); - this.wrapper.contentEditable = this.readOnly ? 'false' : 'true'; if (!this.readOnly) { let $widget_head = $(`
`); let $widget_control = $(`
`); @@ -45,27 +35,10 @@ export default class Header extends Block { $widget_control.appendTo($widget_head); $widget_head.appendTo(this.wrapper); - this.wrapper.classList.add('widget', 'header'); + this.wrapper.classList.add('widget', 'header', 'edit-mode'); - 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 - ); + this.add_settings_button(); + this.add_new_block_button(); frappe.utils.add_custom_button( frappe.utils.icon('drag', 'xs'), @@ -76,67 +49,14 @@ export default class Header extends Block { $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, + text: this.data.text + data.text }; this.data = newData; @@ -146,31 +66,28 @@ export default class Header extends Block { return blockData.text.trim() !== ''; } - save(toolsContent) { + save() { this.wrapper = this._element; return { - text: toolsContent.innerText, - level: this.currentLevel.number, + text: this.wrapper.innerHTML.replace(/ /gi, ''), 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 - }; + super.rendered(this._element); } static get sanitize() { return { level: false, - text: {}, + text: { + br: true, + b: true, + i: true, + a: true, + span: true + }, }; } @@ -180,7 +97,6 @@ export default class Header extends Block { get data() { this._data.text = this._element.innerHTML; - this._data.level = this.currentLevel.number; return this._data; } @@ -188,15 +104,11 @@ export default class Header extends Block { 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 || ''; + let text = this._data.text || ''; + const contains_html_tag = /<[a-z][\s\S]*>/i.test(text); + this._element.innerHTML = contains_html_tag ? + text : `${text}`; } if (!this.readOnly && this.wrapper) { @@ -205,11 +117,12 @@ export default class Header extends Block { } getTag() { - const tag = document.createElement(this.currentLevel.tag); + const tag = document.createElement('DIV'); - tag.innerHTML = this._data.text || ''; + let text = this._data.text || ' '; + tag.innerHTML = `${text}`; - tag.classList.add(this._CSS.wrapper); + tag.classList.add('ce-header'); if (!this.readOnly) { tag.contentEditable = true; @@ -220,120 +133,10 @@ export default class Header extends Block { 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', + icon: frappe.utils.icon('header', 'sm') }; } } \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/blocks/header_size.js b/frappe/public/js/frappe/views/workspace/blocks/header_size.js new file mode 100644 index 0000000000..3694b8799b --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/header_size.js @@ -0,0 +1,117 @@ +export default class HeaderSize { + + static get isInline() { + return true; + } + + get state() { + return this._state; + } + + set state(state) { + this._state = state; + } + + get title() { + return 'Header Size'; + } + + constructor({api}) { + this.api = api; + this.button = null; + this._state = true; + this.selectedText = null; + this.range = null; + this.headerLevels = []; + } + + render() { + this.button = document.createElement('button'); + this.button.type = 'button'; + this.button.innerHTML = `${frappe.utils.icon('header', 'sm')}${frappe.utils.icon('small-down', 'xs')}`; + this.button.classList = 'header-inline-tool'; + + return this.button; + } + + checkState(selection) { + let termWrapper = this.api.selection.findParentTag('SPAN'); + + for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) { + if (termWrapper && termWrapper.classList.contains(h)) { + let num = h.match(/\d+/)[0]; + $('.header-inline-tool svg:first-child').replaceWith(frappe.utils.icon(`header-${num}`, 'md')); + } + } + + const text = selection.anchorNode; + if (!text) return; + } + + change_size(range, size) { + if (!range) return; + + let span = document.createElement('SPAN'); + + span.classList.add(`h${size}`); + span.innerText = range.toString(); + + this.remove_parent_tag(range, range.commonAncestorContainer, span); + + range.extractContents(); + range.insertNode(span); + this.api.inlineToolbar.close(); + } + + remove_parent_tag(range, parent_node, span) { + let diff = range.startContainer.data; + let selected_text = span.innerText; + let parent_tag = parent_node.parentElement; + + if (diff !== selected_text) { + parent_tag = parent_node; + } + + if (parent_tag.innerText == selected_text) { + if (!parent_tag.classList.contains('ce-header') && !parent_tag.classList.contains('ce-paragraph')) { + this.remove_parent_tag(range, parent_node.parentElement, span); + parent_tag.remove(); + } + } + } + + surround(range) { + this.selectedText = range.cloneContents(); + this.actions.hidden = !this.actions.hidden; + this.range = !this.actions.hidden ? range : null; + this.state = !this.actions.hidden; + } + + renderActions() { + this.actions = document.createElement('div'); + this.actions.classList = 'header-level-select'; + + this.headerLevels = new Array(6).fill().map((_, idx) => { + const $header_level = document.createElement('div'); + $header_level.classList.add(`h${idx+1}`, 'header-level'); + $header_level.innerText = `Header ${idx+1}`; + return $header_level; + }); + + for (const [i, headerLevel] of this.headerLevels.entries()) { + this.actions.appendChild(headerLevel); + this.api.listeners.on(headerLevel, 'click', () => { + this.change_size(this.range, i+1); + }); + } + + this.actions.hidden = true; + return this.actions; + } + + destroy() { + for (const headerLevel of this.headerLevels) { + this.api.listeners.off(headerLevel, 'click'); + } + } +} \ 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 index 00a9b8c83a..5fac17bd02 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/index.js +++ b/frappe/public/js/frappe/views/workspace/blocks/index.js @@ -8,11 +8,11 @@ import Spacer from "./spacer"; import Onboarding from "./onboarding"; // import tunes -import SpacingTune from "./spacing_tune"; +import HeaderSize from "./header_size"; -frappe.provide("frappe.wspace_block"); +frappe.provide("frappe.workspace_block"); -frappe.wspace_block.blocks = { +frappe.workspace_block.blocks = { header: Header, paragraph: Paragraph, card: Card, @@ -22,6 +22,6 @@ frappe.wspace_block.blocks = { onboarding: Onboarding, }; -frappe.wspace_block.tunes = { - spacing_tune: SpacingTune +frappe.workspace_block.tunes = { + header_size: HeaderSize, }; \ 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 index 7176b7726d..c0ba529853 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -4,7 +4,7 @@ export default class Onboarding extends Block { static get toolbox() { return { title: 'Onboarding', - icon: '' + icon: frappe.utils.icon('onboarding', 'sm') }; } @@ -21,19 +21,21 @@ export default class Onboarding extends Block { allow_create: this.allow_customization, allow_delete: this.allow_customization, allow_hiding: false, - allow_edit: true + allow_edit: true, + allow_resize: false }; } rendered() { - var e = this.wrapper.closest('.ce-block'); + let block = this.wrapper.closest('.ce-block'); if (this.readOnly && !$(this.wrapper).find('.onboarding-widget-box').is(':visible')) { - $(e).hide(); + $(block).hide(); } - e.classList.add("col-" + this.get_col()); + this.set_col_class(block, this.get_col()); } new(block, widget_type = block) { + let me = this; const dialog_class = get_dialog_constructor(widget_type); let block_name = block+'_name'; this.dialog = new dialog_class({ @@ -54,13 +56,18 @@ export default class Onboarding extends Block { }); this.block_widget.customize(this.options); this.wrapper.setAttribute(block_name, this.block_widget.label || this.block_widget.onboarding_name); + $(this.wrapper).find('.widget').addClass(`${widget_type} edit-mode`); this.new_block_widget = this.block_widget.get_config(); - this.add_tune_button(); + this.add_settings_button(); }, }); if (!this.readOnly && this.data && !this.data[block_name]) { this.dialog.make(); + + this.dialog.dialog.get_close_btn().click(() => { + me.wrapper.closest('.ce-block').remove(); + }); } } @@ -105,7 +112,9 @@ export default class Onboarding extends Block { } if (!this.readOnly) { - this.add_tune_button(); + $(this.wrapper).find('.widget').addClass('onboarding edit-mode'); + this.add_settings_button(); + this.add_new_block_button(); } $(this.wrapper).css("padding-bottom", "20px"); return this.wrapper; @@ -119,9 +128,9 @@ export default class Onboarding extends Block { return true; } - save(blockContent) { + save() { return { - onboarding_name: blockContent.getAttribute('onboarding_name'), + onboarding_name: this.wrapper.getAttribute('onboarding_name'), col: this.get_col(), new: this.new_block_widget }; diff --git a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js index 9e5dfb68ff..70f97c44c1 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js +++ b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js @@ -27,6 +27,8 @@ export default class Paragraph extends Block { } onKeyUp(e) { + if (!this.wrapper) return; + this.show_hide_block_list(true); if (e.code !== 'Backspace' && e.code !== 'Delete') { return; } @@ -34,55 +36,86 @@ export default class Paragraph extends Block { const {textContent} = this._element; if (textContent === '') { + this.show_hide_block_list(); this._element.innerHTML = ''; } } + show_hide_block_list(hide) { + let $wrapper = $(this.wrapper).hasClass('ce-paragraph') ? $(this.wrapper.parentElement) : $(this.wrapper); + let $block_list_container = $wrapper.find('.block-list-container.dropdown-list'); + $block_list_container.removeClass('hidden'); + hide && $block_list_container.addClass('hidden'); + } + 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('focus', () => { + const {textContent} = this._element; + if (textContent !== '') return; + this.show_hide_block_list(); + }); + div.addEventListener('blur', () => { + setTimeout(() => this.show_hide_block_list(true), 10); + }); + div.dataset.placeholder = this.api.i18n.t(this._placeholder); div.addEventListener('keyup', this.onKeyUp); } return div; } + open_block_list() { + let dropdown_title = 'Templates'; + let $block_list_container = $(` + + `); + + let all_blocks = frappe.workspace_block.blocks; + Object.keys(all_blocks).forEach(key => { + let $block_list_item = $(` + + `); + + $block_list_item.click(event => { + event.stopPropagation(); + const index = this.api.blocks.getCurrentBlockIndex(); + this.api.blocks.delete(); + this.api.blocks.insert(key, {}, {}, index); + this.api.caret.setToBlock(index); + }); + + $block_list_container.append($block_list_item); + }); + + $block_list_container.addClass('hidden'); + $block_list_container.appendTo(this.wrapper); + } + render() { this.wrapper = document.createElement('div'); - this.wrapper.contentEditable = this.readOnly ? 'false' : 'true'; if (!this.readOnly) { - let $para_control = $(`
`); + let $para_control = $(`
`); this.wrapper.appendChild(this._element); this._element.classList.remove('widget'); $para_control.appendTo(this.wrapper); - this.wrapper.classList.add('widget'); + this.wrapper.classList.add('widget', 'paragraph', 'edit-mode'); - 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 - ); + this.open_block_list(); + this.add_new_block_button(); + this.add_settings_button(); frappe.utils.add_custom_button( frappe.utils.icon('drag', 'xs'), @@ -93,15 +126,6 @@ export default class Paragraph extends Block { $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; @@ -132,8 +156,7 @@ export default class Paragraph extends Block { } rendered() { - var e = this._element.closest('.ce-block'); - e.classList.add("col-" + this.get_col()); + super.rendered(this._element); } onPaste(event) { @@ -144,20 +167,14 @@ export default class Paragraph extends Block { 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, b: true, i: true, - a: true + a: true, + span: true } }; } @@ -188,8 +205,8 @@ export default class Paragraph extends Block { static get toolbox() { return { - icon: '', - title: 'Text' + title: 'Text', + icon: frappe.utils.icon('text', 'sm') }; } } \ 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 index 96b8f47484..2be5da0d4b 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -3,7 +3,7 @@ export default class Shortcut extends Block { static get toolbox() { return { title: 'Shortcut', - icon: '' + icon: frappe.utils.icon('shortcut', 'sm') }; } @@ -13,17 +13,39 @@ export default class Shortcut extends Block { constructor({ data, api, config, readOnly, block }) { super({ data, api, config, readOnly, block }); - this.col = this.data.col ? this.data.col : "4"; + this.col = this.data.col ? this.data.col : "3"; 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 + allow_edit: true, + allow_resize: true }; } + rendered() { + super.rendered(); + + this.remove_last_divider(); + $(window).resize(() => { + this.remove_last_divider(); + }); + } + + remove_last_divider() { + let block = this.wrapper.closest('.ce-block'); + let container_offset_right = $('.layout-main-section')[0].offsetWidth; + let block_offset_right = block.offsetLeft + block.offsetWidth; + + if (container_offset_right - block_offset_right <= 110) { + $(block).find('.divider').addClass('hidden'); + } else { + $(block).find('.divider').removeClass('hidden'); + } + } + render() { this.wrapper = document.createElement('div'); this.new('shortcut'); @@ -34,7 +56,14 @@ export default class Shortcut extends Block { } if (!this.readOnly) { - this.add_tune_button(); + $(this.wrapper).find('.widget').addClass('shortcut edit-mode'); + this.add_settings_button(); + this.add_new_block_button(); + } else { + let $shortcut_icon = frappe.utils.icon('arrow-up-right', 'xs', '', 'stroke: grey', 'ml-2'); + $(this.wrapper).find('.widget .widget-title').append($shortcut_icon); + + $(this.wrapper).append($(`
`)); } return this.wrapper; } @@ -47,9 +76,9 @@ export default class Shortcut extends Block { return true; } - save(blockContent) { + save() { return { - shortcut_name: blockContent.getAttribute('shortcut_name'), + shortcut_name: this.wrapper.getAttribute('shortcut_name'), col: this.get_col(), new: this.new_block_widget }; diff --git a/frappe/public/js/frappe/views/workspace/blocks/spacer.js b/frappe/public/js/frappe/views/workspace/blocks/spacer.js index 3309cad4a4..bb75cea873 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/spacer.js +++ b/frappe/public/js/frappe/views/workspace/blocks/spacer.js @@ -3,7 +3,7 @@ export default class Spacer extends Block { static get toolbox() { return { title: 'Spacer', - icon: '' + icon: frappe.utils.icon('spacer', 'sm') }; } @@ -18,40 +18,24 @@ export default class Spacer extends Block { render() { this.wrapper = document.createElement('div'); + this.wrapper.classList.add('widget', 'spacer'); if (!this.readOnly) { let $spacer = $(`
-
+
Spacer
`); $spacer.appendTo(this.wrapper); - this.wrapper.classList.add('widget', 'new-widget'); - this.wrapper.style.minHeight = 50 + 'px'; + this.wrapper.classList.add('edit-mode'); + this.wrapper.style.minHeight = 40 + '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 - ); + this.add_settings_button(); + this.add_new_block_button(); frappe.utils.add_custom_button( frappe.utils.icon('drag', 'xs'), @@ -61,15 +45,6 @@ export default class Spacer extends Block { 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; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js b/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js deleted file mode 100644 index 365f7f590e..0000000000 --- a/frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js +++ /dev/null @@ -1,123 +0,0 @@ -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 e6248f66cf..e28225904d 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -20,13 +20,11 @@ frappe.views.Workspace = class Workspace { constructor(wrapper) { this.wrapper = $(wrapper); this.page = wrapper.page; - this.blocks = frappe.wspace_block.blocks; + this.blocks = frappe.workspace_block.blocks; this.is_read_only = true; - this.new_page = null; this.pages = {}; this.sorted_public_items = []; this.sorted_private_items = []; - this.deleted_sidebar_items = []; this.current_page = {}; this.sidebar_items = { 'public': {}, @@ -52,7 +50,10 @@ frappe.views.Workspace = class Workspace { } async setup_pages(reload) { + !this.discard && this.create_page_skeleton(); + !this.discard && this.create_sidebar_skeleton(); this.sidebar_pages = !this.discard ? await this.get_pages() : this.sidebar_pages; + this.cached_pages = $.extend(true, {}, this.sidebar_pages); this.all_pages = this.sidebar_pages.pages; this.has_access = this.sidebar_pages.has_access; @@ -68,24 +69,13 @@ frappe.views.Workspace = class Workspace { for (let page of this.all_pages) { frappe.workspaces[frappe.router.slug(page.name)] = {title: page.title}; } - if (this.new_page && this.new_page.name) { - if (!frappe.workspaces[frappe.router.slug(this.new_page.label)]) { - 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(); reload && this.show(); } } get_pages() { - return frappe.xcall("frappe.desk.desktop.get_wspace_sidebar_items"); + return frappe.xcall("frappe.desk.desktop.get_workspace_sidebar_items"); } sidebar_item_container(item) { @@ -101,6 +91,7 @@ frappe.views.Workspace = class Workspace { + `); } @@ -119,8 +110,10 @@ frappe.views.Workspace = class Workspace { }); // 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').length && !frappe.dom.is_element_in_viewport(this.sidebar.find('.selected')) && this.sidebar.find('.selected')[0].scrollIntoView(); + + this.remove_sidebar_skeleton(); } build_sidebar_section(title, root_pages) { @@ -164,7 +157,8 @@ frappe.views.Workspace = class Workspace { let child_items = pages.filter(page => page.parent_page == item.title); if (child_items.length > 0) { - let child_container = $(``); + let child_container = $item_container.find('.sidebar-child-item'); + child_container.addClass('hidden'); this.prepare_sidebar(child_items, child_container, $item_container); } @@ -179,18 +173,23 @@ frappe.views.Workspace = class Workspace { } add_drop_icon(item, sidebar_control, item_container) { + let drop_icon = 'small-down'; + if (item_container.find(`[item-name="${this.current_page.name}"]`).length) { + drop_icon = 'small-up'; + } + let $child_item_section = item_container.find('.sidebar-child-item'); - let $drop_icon = $(``) + 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"); - }); } + $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"); + }); } show() { @@ -203,21 +202,48 @@ frappe.views.Workspace = class Workspace { let page = this.get_page_to_show(); this.page.set_title(`${__(page.name)}`); + this.update_selected_sidebar(this.current_page, false); //remove selected from old page + this.update_selected_sidebar(page, true); //add selected on new page + this.show_page(page); } + update_selected_sidebar(page, add) { + let section = page.public ? 'public' : 'private'; + if (this.sidebar && this.sidebar_items[section] && this.sidebar_items[section][page.name]) { + let $sidebar = this.sidebar_items[section][page.name]; + let pages = page.public ? this.public_pages : this.private_pages; + let sidebar_page = pages.find(p => p.title == page.name); + + if (add) { + $sidebar[0].firstElementChild.classList.add("selected"); + if (sidebar_page) sidebar_page.selected = true; + + // open child sidebar section if closed + $sidebar.parent().hasClass('hidden') && + $sidebar.parent().removeClass('hidden'); + + this.current_page = { name: page.name, public: page.public }; + localStorage.current_page = page.name; + localStorage.is_current_page_public = page.public; + } else { + $sidebar[0].firstElementChild.classList.remove("selected"); + if (sidebar_page) sidebar_page.selected = false; + } + } + } + get_data(page) { - return frappe.xcall("frappe.desk.desktop.get_desktop_page", { + return frappe.call("frappe.desk.desktop.get_desktop_page", { page: page }).then(data => { - this.page_data = data; + this.page_data = data.message; // caching page data this.pages[page.name] && delete this.pages[page.name]; - this.pages[page.name] = data; + this.pages[page.name] = data.message; if (!this.page_data || Object.keys(this.page_data).length === 0) return; - if (this.page_data.charts && this.page_data.charts.items.length === 0) return; return frappe.dashboard_utils.get_dashboard_settings().then(settings => { @@ -249,49 +275,35 @@ frappe.views.Workspace = class Workspace { } async 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.create_skeleton(); if (this.all_pages) { + this.create_page_skeleton(); + 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); + let current_page = pages.filter(p => p.title == page.name)[0]; + this.content = current_page && JSON.parse(current_page.content); this.add_custom_cards_in_content(); $('.item-anchor').addClass('disable-click'); - if (this.pages && this.pages[this_page.name]) { - this.page_data = this.pages[this_page.name]; + if (this.pages && this.pages[current_page.name]) { + this.page_data = this.pages[current_page.name]; } else { - await this.get_data(this_page); + await frappe.after_ajax(() => this.get_data(current_page)); } + this.setup_actions(page); + this.prepare_editorjs(); $('.item-anchor').removeClass('disable-click'); - this.remove_skeleton(); + + this.remove_page_skeleton(); } } @@ -329,9 +341,7 @@ frappe.views.Workspace = class Workspace { return; } - this.page.clear_primary_action(); - this.page.clear_secondary_action(); - this.page.clear_inner_toolbar(); + this.clear_page_actions(); current_page.is_editable && this.page.set_secondary_action(__("Edit"), async () => { if (!this.editor || !this.editor.readOnly) return; @@ -341,7 +351,6 @@ frappe.views.Workspace = class Workspace { this.initialize_editorjs_undo(); this.setup_customization_buttons(current_page); this.show_sidebar_actions(); - this.make_sidebar_sortable(); this.make_blocks_sortable(); }); }); @@ -357,22 +366,25 @@ frappe.views.Workspace = class Workspace { this.undo.readOnly = false; } - setup_customization_buttons(page) { - let me = this; + clear_page_actions() { this.page.clear_primary_action(); this.page.clear_secondary_action(); this.page.clear_inner_toolbar(); + } + + setup_customization_buttons(page) { + this.clear_page_actions(); page.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; + this.clear_page_actions(); + this.save_page(page).then((saved) => { + if (!saved) return; + this.undo.readOnly = true; + this.editor.readOnly.toggle(); + this.is_read_only = true; + }); }, null, __("Saving") @@ -382,11 +394,10 @@ frappe.views.Workspace = class Workspace { __("Discard"), async () => { this.discard = true; - this.page.clear_primary_action(); - this.page.clear_secondary_action(); - this.page.clear_inner_toolbar(); + this.clear_page_actions(); await this.editor.readOnly.toggle(); this.is_read_only = true; + this.sidebar_pages = this.cached_pages; this.reload(); frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" }); } @@ -395,34 +406,30 @@ frappe.views.Workspace = class Workspace { page.name && this.page.add_inner_button(__("Settings"), () => { frappe.set_route(`workspace/${page.name}`); }); - - 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'); + this.make_sidebar_sortable(); } - add_sidebar_actions(item, sidebar_control) { + add_sidebar_actions(item, sidebar_control, is_new) { if (!item.is_editable) { - $(`${frappe.utils.icon("lock", "sm")}`) - .appendTo(sidebar_control); sidebar_control.parent().click(() => { !this.is_read_only && frappe.show_alert({ message: __("Only Workspace Manager can sort or edit this page"), indicator: 'info' }, 5); }); + + frappe.utils.add_custom_button( + frappe.utils.icon('duplicate', 'sm'), + () => this.duplicate_page(item), + "duplicate-page", + `${__('Duplicate Workspace')}`, + null, + sidebar_control + ); } else { frappe.utils.add_custom_button( frappe.utils.icon('drag', 'xs'), @@ -432,24 +439,380 @@ frappe.views.Workspace = class Workspace { null, sidebar_control ); - frappe.utils.add_custom_button( - frappe.utils.icon('delete', 'xs'), - () => this.delete_page(item), - "delete-page", - `${__('Delete')}`, - null, - sidebar_control - ); + + !is_new && this.add_settings_button(item, sidebar_control); + } + } + + get_parent_pages(page) { + this.public_parent_pages = ['', ...this.public_pages.filter(p => !p.parent_page).map(p => p.title)]; + this.private_parent_pages = ['', ...this.private_pages.filter(p => !p.parent_page).map(p => p.title)]; + + if (page) { + return page.public ? this.public_parent_pages : this.private_parent_pages; } } - 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'); + edit_page(item) { + var me = this; + let old_item = item; + let parent_pages = this.get_parent_pages(item); + let idx = parent_pages.findIndex(x => x == item.title); + if (idx !== -1) parent_pages.splice(idx, 1); + const d = new frappe.ui.Dialog({ + title: __('Update Details'), + fields: [ + { + label: __('Title'), + fieldtype: 'Data', + fieldname: 'title', + reqd: 1, + default: item.title + }, + { + label: __('Parent'), + fieldtype: 'Select', + fieldname: 'parent', + options: parent_pages, + default: item.parent_page + }, + { + label: __('Public'), + fieldtype: 'Check', + fieldname: 'is_public', + depends_on: `eval:${this.has_access}`, + default: item.public, + 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', + default: item.icon + }, + ], + primary_action_label: __('Update'), + primary_action: (values) => { + let is_title_changed = values.title != old_item.title; + let is_section_changed = values.is_public != old_item.public; + if ((is_title_changed || is_section_changed) && !this.validate_page(values, old_item)) return; + d.hide(); + + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.update_page", + args: { + name: old_item.name, + title: values.title, + icon: values.icon || '', + parent: values.parent || '', + public: values.is_public || 0, + }, + callback: function(res) { + if (res.message) { + let message = `Workspace ${old_item.title} Edited Successfully`; + frappe.show_alert({ message: __(message), indicator: "green" }); + } + } + }); + + this.update_sidebar(old_item, values); + + if (this.make_page_selected) { + let pre_url = values.is_public ? '' : 'private/'; + let route = pre_url + frappe.router.slug(values.title); + frappe.set_route(route); + + this.make_page_selected = false; + } + + this.make_sidebar(); + this.show_sidebar_actions(); + } + }); + d.show(); + } + + update_sidebar(old_item, new_item) { + let is_section_changed = old_item.public != (new_item.is_public || 0); + let is_title_changed = old_item.title != new_item.title; + let new_updated_item = {...old_item}; + + let pages = old_item.public ? this.public_pages : this.private_pages; + + let child_items = pages.filter(page => page.parent_page == old_item.title); + + this.make_page_selected = old_item.selected; + + new_updated_item.title = new_item.title; + new_updated_item.icon = new_item.icon; + new_updated_item.parent_page = new_item.parent || ""; + new_updated_item.public = new_item.is_public; + + if (is_title_changed || is_section_changed) { + if (new_item.is_public) { + new_updated_item.name = new_item.title; + new_updated_item.label = new_item.title; + new_updated_item.for_user = ""; + } else { + let user = frappe.session.user; + new_updated_item.name = `${new_item.title}-${user}`; + new_updated_item.label = `${new_item.title}-${user}`; + new_updated_item.for_user = user; + } + } + this.update_cached_values(old_item, new_updated_item); + + if (child_items.length) { + child_items.forEach(child => { + child.parent_page = new_item.title; + is_section_changed && this.update_child_sidebar(child, new_item); + }); + } + } + + update_child_sidebar(child, new_item) { + let old_child = {...child}; + this.make_page_selected = child.selected; + + child.public = new_item.is_public; + if (new_item.is_public) { + child.name = child.title; + child.label = child.title; + child.for_user = ""; + } else { + let user = frappe.session.user; + child.name = `${child.title}-${user}`; + child.label = `${child.title}-${user}`; + child.for_user = user; + } + + this.update_cached_values(old_child, child); + } + + update_cached_values(old_item, new_item, duplicate, new_page) { + let [from_pages, to_pages] = old_item.public ? + [this.public_pages, this.private_pages] : [this.private_pages, this.public_pages]; + + let old_item_index = from_pages.findIndex(page => page.title == old_item.title); + duplicate && old_item_index++; + + // update frappe.workspaces + if (frappe.workspaces[frappe.router.slug(old_item.name)] || new_page) { + !duplicate && delete frappe.workspaces[frappe.router.slug(old_item.name)]; + if (new_item) { + frappe.workspaces[frappe.router.slug(new_item.name)] = {'title': new_item.title}; + } + } + + // update page block data + if (this.pages && this.pages[old_item.name] || new_page) { + if (new_item) { + this.pages[new_item.name] = this.pages[old_item.name] || {}; + } + !duplicate && delete this.pages[old_item.name]; + } + + // update public and private pages + if (new_item) { + let is_section_changed = old_item.public != (new_item.is_public || new_item.public || 0); + + if (is_section_changed) { + !duplicate && from_pages.splice(old_item_index, 1); + to_pages.push(new_item); + } else if (new_page) { + from_pages.push(new_item); + } else { + from_pages.splice(old_item_index, duplicate ? 0 : 1, new_item); + } + } else { + from_pages.splice(old_item_index, 1); + } + + this.sidebar_pages.pages = [...this.public_pages, ...this.private_pages]; + this.cached_pages = this.sidebar_pages; + } + + add_settings_button(item, sidebar_control) { + this.dropdown_list = [ + { + label: 'Edit', + title: 'Edit Workspace', + icon: frappe.utils.icon('edit', 'sm'), + action: () => this.edit_page(item) + }, + { + label: 'Duplicate', + title: 'Duplicate Workspace', + icon: frappe.utils.icon('duplicate', 'sm'), + action: () => this.duplicate_page(item) + }, + { + label: 'Delete', + title: 'Delete Workspace', + icon: frappe.utils.icon('delete-active', 'sm'), + action: () => this.delete_page(item) + } + ]; + + let $button = $(` + + + `); + + let dropdown_item = function(label, title, icon, action) { + let html = $(` + + `); + + html.click(event => { + event.stopPropagation(); + action && action(); + }); + + return html; + }; + + $button.filter('.dropdown-btn').click(event => { + event.stopPropagation(); + if ($button.filter('.dropdown-list.hidden').length) { + $('.dropdown-list:not(.hidden)').addClass('hidden'); + } + $button.filter('.dropdown-list').toggleClass('hidden'); + }); + + $(document).click(event => { + event.stopPropagation(); + $('.dropdown-list:not(.hidden)').addClass('hidden'); + }); + + sidebar_control.append($button); + + this.dropdown_list.forEach((i) => { + $button.filter('.dropdown-list').append(dropdown_item(i.label, i.title, i.icon, i.action)); + }); + } + + delete_page(page) { + frappe.confirm(__("Are you sure you want to delete page {0}?", [page.title]), () => { + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.delete_page", + args: { page: page }, + callback: function(res) { + if (res.message) { + let page = res.message; + let message = `Workspace ${page.title} Deleted Successfully`; + frappe.show_alert({ message: __(message), indicator: "green" }); + } + } + }); + + this.page.clear_primary_action(); + this.update_cached_values(page); + + if (this.current_page.name == page.title && this.current_page.public == page.public) { + frappe.set_route('/'); + } + + this.make_sidebar(); + this.show_sidebar_actions(); }); } + duplicate_page(page) { + var me = this; + let parent_pages = this.get_parent_pages(page); + const d = new frappe.ui.Dialog({ + title: __('Create Duplicate'), + fields: [ + { + label: __('Title'), + fieldtype: 'Data', + fieldname: 'title', + reqd: 1 + }, + { + label: __('Parent'), + fieldtype: 'Select', + fieldname: 'parent', + options: parent_pages, + default: page.parent_page + }, + { + label: __('Public'), + fieldtype: 'Check', + fieldname: 'is_public', + depends_on: `eval:${this.has_access}`, + default: page.public, + 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', + default: page.icon + }, + ], + primary_action_label: __('Duplicate'), + primary_action: (values) => { + if (!this.validate_page(values)) return; + d.hide(); + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.duplicate_page", + args: { + page_name: page.name, + new_page: values + }, + callback: function(res) { + if (res.message) { + let new_page = res.message; + let message = `Duplicate of ${page.title} named as ${new_page.title} is created successfully`; + frappe.show_alert({ message: __(message), indicator: "green" }); + } + } + }); + + let new_page = {...page}; + + new_page.title = values.title; + new_page.public = values.is_public || 0; + new_page.name = values.title + (new_page.public ? '' : '-' + frappe.session.user); + new_page.label = new_page.name; + new_page.icon = values.icon; + new_page.parent_page = values.parent || ''; + new_page.for_user = new_page.public ? '' : frappe.session.user; + new_page.is_editable = !new_page.public; + new_page.selected = true; + + this.update_cached_values(page, new_page, true); + + let pre_url = values.is_public ? '' : 'private/'; + let route = pre_url + frappe.router.slug(values.title); + frappe.set_route(route); + + me.make_sidebar(); + me.show_sidebar_actions(); + } + }); + d.show(); + } + make_sidebar_sortable() { let me = this; $('.nested-container').each( function() { @@ -463,35 +826,75 @@ frappe.views.Workspace = class Workspace { onEnd: function (evt) { let is_public = $(evt.item).attr('item-public') == '1'; me.prepare_sorted_sidebar(is_public); + me.update_sorted_sidebar(); } }); }); } prepare_sorted_sidebar(is_public) { + let pages = is_public ? this.public_pages : this.private_pages; if (is_public) { - this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last()); + this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last(), pages); } else { - this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first()); + this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first(), pages); } + + this.sidebar_pages.pages = [...this.public_pages, ...this.private_pages]; + this.cached_pages = this.sidebar_pages; } - sort_sidebar($sidebar_section) { + sort_sidebar($sidebar_section, pages) { let sorted_items = []; - for (let page of $sidebar_section.find('.sidebar-item-container')) { + Array.from($sidebar_section.find('.sidebar-item-container')).forEach((page, i) => { 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 }); - } + + let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first(); + if ($(page).find('.sidebar-child-item > *').length != 0) { + $drop_icon.removeClass('hidden'); + } else { + $drop_icon.addClass('hidden'); + } + + let from_index = pages.findIndex(p => p.title == page.attributes['item-name'].value); + let element = pages[from_index]; + element.parent_page = parent_page; + if (from_index != i) { + pages.splice(from_index, 1); + pages.splice(i, 0, element); + } + }); return sorted_items; } + update_sorted_sidebar() { + if (this.sorted_public_items || this.sorted_private_items) { + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.sort_pages", + args: { + sb_public_items: this.sorted_public_items, + sb_private_items: this.sorted_private_items, + }, + callback: function(res) { + if (res.message) { + let message = `Sidebar Updated Successfully`; + frappe.show_alert({ message: __(message), indicator: "green" }); + } + } + }); + } + } + make_blocks_sortable() { let me = this; this.page_sortable = Sortable.create(this.page.main.find(".codex-editor__redactor").get(0), { @@ -508,11 +911,10 @@ frappe.views.Workspace = class Workspace { } 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; + this.get_parent_pages(); const d = new frappe.ui.Dialog({ - title: __('Set Title'), + title: __('New Workspace'), fields: [ { label: __('Title'), @@ -551,81 +953,115 @@ frappe.views.Workspace = class Workspace { d.hide(); this.initialize_editorjs_undo(); this.setup_customization_buttons({is_editable: true}); - this.title = values.title; - this.icon = values.icon; - this.parent = values.parent; - this.public = values.is_public; + + let name = values.title + (values.is_public ? '' : '-' + frappe.session.user); + let blocks = [{ + type: "header", + data: { text: values.title } + }]; + + let new_page = { + content: JSON.stringify(blocks), + name: name, + label: name, + title: values.title, + public: values.is_public || 0, + for_user: values.is_public ? '' : frappe.session.user, + icon: values.icon, + parent_page: values.parent || '', + is_editable: true, + selected: true + }; + this.editor.render({ - blocks: [ - { - type: "header", - data: { - text: this.title, - level: 4 - } - } - ] + blocks: blocks }).then(async () => { if (this.editor.configuration.readOnly) { this.is_read_only = false; await this.editor.readOnly.toggle(); } - this.add_page_to_sidebar(values); + + frappe.call({ + method: "frappe.desk.doctype.workspace.workspace.new_page", + args: { + new_page: new_page + }, + callback: function(res) { + if (res.message) { + let message = `Workspace ${new_page.title} Created Successfully`; + frappe.show_alert({ message: __(message), indicator: "green" }); + } + } + }); + + this.update_cached_values(new_page, new_page, true, true); + + let pre_url = new_page.public ? '' : 'private/'; + let route = pre_url + frappe.router.slug(new_page.title); + frappe.set_route(route); + + this.make_sidebar(); this.show_sidebar_actions(); - this.make_sidebar_sortable(); - this.make_blocks_sortable(); - this.prepare_sorted_sidebar(values.is_public); }); } }); d.show(); } - validate_page(values) { + validate_page(new_page, old_page) { let message = ""; - let pages = values.is_public ? this.public_pages : this.private_pages; + let [from_pages, to_pages] = new_page.is_public ? + [this.private_pages, this.public_pages] : [this.public_pages, this.private_pages]; + + let section = this.sidebar_categories[new_page.is_public]; - 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))) { + if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) { + message = `Page with title ${new_page.title} already exist.`; + } + + if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.title))) { message = "Doctype with same route already exist. Please choose different title."; } + let child_pages = old_page && from_pages.filter(p => p.parent_page == old_page.title); + if (child_pages) { + child_pages.every(child_page => { + if (to_pages && to_pages.find(p => p.title == child_page.title)) { + message = `One of the child page with name ${child_page.title} already exist in ${section} Section. Please update the name of the child page first before moving`; + cur_dialog.hide(); + return false; + } + return true; + }); + } + if (message) { - frappe.throw(__(message, [__(values.title)])); + frappe.throw(__(message)); return false; } return true; } - add_page_to_sidebar({title, icon, parent, is_public}) { + add_page_to_sidebar(page) { let $sidebar = $('.standard-sidebar-section'); - let item = { - title: title, - icon: icon, - parent_page: parent, - public: is_public - }; + let item = {...page}; + + item.selected = true; + item.is_editable = true; + 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') - ); + this.add_sidebar_actions(item, $sidebar_item.find('.sidebar-item-control'), true); + $sidebar_item.find('.sidebar-item-control .drag-handle').css('margin-right', '8px'); - let $sidebar_section = is_public ? $sidebar[1] : $sidebar[0]; + let sidebar_section = item.is_public ? $sidebar[1] : $sidebar[0]; - if (!parent) { - !is_public && $sidebar.first().removeClass('hidden'); - $sidebar_item.appendTo($sidebar_section); + if (!item.parent) { + !item.is_public && $sidebar.first().removeClass('hidden'); + $sidebar_item.appendTo(sidebar_section); } else { - let $item_container = $($sidebar_section).find(`[item-name="${parent}"]`); + let $item_container = $(sidebar_section).find(`[item-name="${item.parent}"]`); let $child_section = $item_container.find('.sidebar-child-item'); let $drop_icon = $item_container.find('.drop-icon'); if (!$child_section[0]) { @@ -635,22 +1071,31 @@ frappe.views.Workspace = class Workspace { } $sidebar_item.appendTo($child_section); $child_section.removeClass('hidden'); + $item_container.find('.drop-icon.hidden').removeClass('hidden'); $item_container.find('.drop-icon use').attr("href", "#icon-small-up"); } + + let section = item.is_public ? 'public' : 'private'; + if (this.sidebar_items && this.sidebar_items[section] && !this.sidebar_items[section][item.title]) { + this.sidebar_items[section][item.title] = $sidebar_item; + } } initialize_editorjs(blocks) { this.tools = { header: { class: this.blocks['header'], - inlineToolbar: true, + inlineToolbar: ['HeaderSize', 'bold', 'italic', 'link'], config: { - defaultLevel: 4 + default_size: 4 } }, paragraph: { class: this.blocks['paragraph'], - inlineToolbar: true + inlineToolbar: ['HeaderSize', 'bold', 'italic', 'link'], + config: { + placeholder: 'Choose a block or continue typing' + } }, chart: { class: this.blocks['chart'], @@ -677,7 +1122,7 @@ frappe.views.Workspace = class Workspace { } }, spacer: this.blocks['spacer'], - spacingTune: frappe.wspace_block.tunes['spacing_tune'], + HeaderSize: frappe.workspace_block.tunes['header_size'], }; this.editor = new EditorJS({ data: { @@ -685,27 +1130,18 @@ frappe.views.Workspace = class Workspace { }, tools: this.tools, autofocus: false, - tunes: ['spacingTune'], readOnly: true, logLevel: 'ERROR' }); } - save_page() { - frappe.dom.freeze(); - this.create_skeleton(); - 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 }; - } + save_page(page) { let me = this; - this.editor.save().then((outputData) => { + this.current_page = { name: page.title, public: page.public }; + + return this.editor.save().then((outputData) => { let new_widgets = {}; + outputData.blocks.forEach(item => { if (item.data.new) { if (!new_widgets[item.type]) { @@ -718,34 +1154,36 @@ frappe.views.Workspace = class Workspace { let blocks = outputData.blocks.filter( item => item.type != 'card' || - (item.data.card_name !== 'Custom Documents' && - item.data.card_name !== 'Custom Reports') + (item.data.card_name !== 'Custom Documents' && + item.data.card_name !== 'Custom Reports') ); + if (page.content == JSON.stringify(blocks)) { + this.setup_customization_buttons(page); + frappe.show_alert({ message: __("No changes made on the page"), indicator: "warning" }); + return false; + } + + this.create_page_skeleton(); + page.content = JSON.stringify(blocks); 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, + title: page.title, + public: page.public || 0, new_widgets: new_widgets, - blocks: JSON.stringify(blocks), - save: save + blocks: JSON.stringify(blocks) }, callback: function(res) { - frappe.dom.unfreeze(); if (res.message) { - me.new_page = res.message; - me.pages[res.message.label] && delete me.pages[res.message.label]; + me.discard = true; + me.update_cached_values(page, page); me.reload(); frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); } } }); + return true; }).catch((error) => { error; // console.log('Saving failed: ', error); @@ -753,26 +1191,34 @@ frappe.views.Workspace = class Workspace { } reload() { - this.title = ''; - this.icon = ''; - this.parent = ''; - this.public = false; this.sorted_public_items = []; this.sorted_private_items = []; - this.deleted_sidebar_items = []; - this.create_skeleton(); this.setup_pages(true); this.discard = false; this.undo.readOnly = true; } - create_skeleton() { - this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); - this.$page.find('.codex-editor').addClass('hidden'); + create_page_skeleton() { + if ($('.layout-main-section').find('.workspace-skeleton').length) return; + + $('.layout-main-section').prepend(frappe.render_template('workspace_loading_skeleton')); + $('.layout-main-section').find('.codex-editor').addClass('hidden'); + } + + remove_page_skeleton() { + $('.layout-main-section').find('.codex-editor').removeClass('hidden'); + $('.layout-main-section').find('.workspace-skeleton').remove(); + } + + create_sidebar_skeleton() { + if ($('.list-sidebar').find('.workspace-sidebar-skeleton').length) return; + + $('.list-sidebar').prepend(frappe.render_template('workspace_sidebar_loading_skeleton')); + $('.desk-sidebar').addClass('hidden'); } - remove_skeleton() { - this.$page.find('.codex-editor').removeClass('hidden'); - this.$page.find('.workspace-skeleton').remove(); + remove_sidebar_skeleton() { + $('.desk-sidebar').removeClass('hidden'); + $('.list-sidebar').find('.workspace-sidebar-skeleton').remove(); } }; diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index e6ae64d9dc..aabb3526b0 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -34,16 +34,6 @@ export default class Widget { this.action_area ); - options.allow_delete && - frappe.utils.add_custom_button( - frappe.utils.icon('delete', 'xs'), - () => this.delete(), - "", - `${__('Delete')}`, - null, - this.action_area - ); - if (options.allow_hiding) { if (this.hidden) { this.widget.removeClass("hidden"); @@ -71,27 +61,11 @@ export default class Widget { frappe.utils.add_custom_button( frappe.utils.icon("edit", "xs"), () => this.edit(), - null, + "edit-button", `${__('Edit')}`, null, this.action_area ); - - if (options.allow_resize) { - const title = this.width == 'Full'? `${__('Collapse')}` : `${__('Expand')}`; - frappe.utils.add_custom_button( - '', - () => this.toggle_width(), - "resize-button", - title, - null, - this.action_area - ); - - this.resize_button = this.action_area.find( - ".resize-button" - ); - } } make() { @@ -100,9 +74,7 @@ export default class Widget { } make_widget() { - this.widget = $(`
+ this.widget = $(`
@@ -110,10 +82,8 @@ export default class Widget {
-
-
- +
+
`); this.title_field = this.widget.find(".widget-title"); diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index ec602b8522..6c34fac45a 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -28,7 +28,7 @@ export default class ChartWidget extends Widget { } set_chart_title() { - const max_chars = this.widget.width() < 600 ? 20 : 60; + const max_chars = this.widget.width() < 600 ? 40 : 60; this.set_title(max_chars); } diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index cc771b96b5..3320e88bfb 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -80,7 +80,9 @@ export default class LinksWidget extends Widget { return $(` + } ${disabled_dependent(item)}" type="${item.type}" title="${ + item.label ? item.label : item.name + }"> ${get_link_for_item(item)} `); diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 5676a834fe..01d41a0cf9 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -9,6 +9,7 @@ class WidgetDialog { this.setup_dialog_events(); this.dialog.show(); + window.cur_dialog = this.dialog; this.editing && this.set_default_values(); } @@ -181,19 +182,16 @@ class CardDialog extends WidgetDialog { fieldtype: "Select", in_list_view: 1, label: "Link Type", - options: ["DocType", "Page", "Report"], - onchange: (e) => { - me.link_to = e.currentTarget.value; - } + options: ["DocType", "Page", "Report"] }, { fieldname: "link_to", fieldtype: "Dynamic Link", in_list_view: 1, label: "Link To", - options: "link_type", - get_options: () => { - return me.link_to; + get_options: (df) => { + return df.doc.link_type; + } }, { @@ -506,7 +504,7 @@ class NumberCardDialog extends WidgetDialog { setup_dialog_events() { if (!this.document_type) { - if (this.default_values['doctype']) { + if (this.default_values && this.default_values['doctype']) { this.document_type = this.default_values['doctype']; this.setup_filter(this.default_values['doctype']); this.set_aggregate_function_fields(); @@ -518,7 +516,7 @@ class NumberCardDialog extends WidgetDialog { set_aggregate_function_fields() { let aggregate_function_fields = []; - if (this.document_type) { + if (this.document_type && frappe.get_meta(this.document_type)) { frappe.get_meta(this.document_type).fields.map(df => { if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { if (df.fieldtype == 'Currency') { @@ -537,7 +535,7 @@ class NumberCardDialog extends WidgetDialog { if (data.new_or_existing == 'Existing Card') { data.name = data.card; } - data.stats_filter = JSON.stringify(this.filter_group.get_filters()); + data.stats_filter = this.filter_group && JSON.stringify(this.filter_group.get_filters()); data.document_type = this.document_type; return data; diff --git a/frappe/public/scss/common/buttons.scss b/frappe/public/scss/common/buttons.scss index de3a4cfc20..62479e7a7a 100644 --- a/frappe/public/scss/common/buttons.scss +++ b/frappe/public/scss/common/buttons.scss @@ -62,7 +62,7 @@ background-color: var(--control-bg); color: var(--text-color); &:hover, &:active { - background-color: var(--gray-300); + background-color: var(--gray-400); color: var(--text-color); } } diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index 8cab845d35..6e5ebdb694 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -87,6 +87,8 @@ --highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500); + --shadow-base: 0px 4px 8px rgba(114, 176, 233, 0.06), 0px 0px 4px rgba(112, 172, 228, 0.12); + // input --input-disabled-bg: none; diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 6ab01a744c..549ed6eee9 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -107,13 +107,20 @@ body { } } +.divider { + height: 30%; + position: absolute; + top: 18px; + right: 0; + border-right: 1px solid var(--gray-400); +} + .widget { @include flex(flex, null, null, column); min-height: 1px; - padding: 15px; + padding: 7px; border-radius: var(--border-radius-md); height: 100%; - box-shadow: var(--card-shadow); background-color: var(--card-bg); .btn { @@ -143,6 +150,7 @@ body { font-weight: 500; line-height: 1.3em; color: var(--heading-color); + cursor: default; svg { flex: none; @@ -329,9 +337,28 @@ body { } &.onboarding-widget-box { - margin-top: var(--margin-xs); margin-bottom: var(--margin-2xl); - padding: var(--padding-lg); + padding: var(--padding-lg) !important; + background-color: var(--bg-color); + + &.edit-mode:hover { + background-color: var(--fg-color); + + .onboarding-step { + &.active, + &:hover { + background-color: var(--bg-color); + + .step-index.step-pending { + background-color: var(--fg-color); + } + } + + .step-index { + background-color: var(--bg-color); + } + } + } .widget-head { display: flex; @@ -390,12 +417,6 @@ body { .step-index.step-pending { display: flex; } - - &.active { - .step-index.step-pending { - background-color: var(--fg-color); - } - } } &.complete { @@ -418,7 +439,11 @@ body { &.active, &:hover { - background-color: var(--bg-light-gray); + background-color: var(--fg-color); + + .step-index { + background-color: var(--bg-color); + } .step-skip { visibility: visible; @@ -434,7 +459,7 @@ body { height: 20px; width: 20px; color: var(--text-on-light-gray); - background-color: var(--bg-light-gray); + background-color: var(--fg-color); margin-right: var(--margin-sm); border-radius: var(--border-radius-full); @@ -447,7 +472,7 @@ body { display: none; background-color: var(--primary); .icon use { - stroke: var(--white); + stroke: var(--var(--fg-color)); } } @@ -496,7 +521,7 @@ body { } } - @media (max-width: map-get($grid-breakpoints, "sm")) { + @media (max-width: map-get($grid-breakpoints, "md")) { .widget-body { flex-direction: column; .onboarding-steps-wrapper { @@ -513,9 +538,19 @@ body { &.shortcut-widget-box { cursor: pointer; - .widget-head { - margin-top: var(--margin-xs); - margin-bottom: 5px; + &:hover { + .widget-title { + color: var(--blue-500) !important; + } + + svg.icon-xs { + stroke: var(--blue-500) !important; + } + } + + .widget-title { + cursor: pointer !important; + font-size: var(--text-base) !important; } .indicator-pill { @@ -631,8 +666,8 @@ body { width: 18px; .icon-xs { - width: 8px; - height: 7px; + width: 10px; + height: 10px; } } @@ -757,6 +792,25 @@ body { } } +.workspace-sidebar-skeleton { + transition: ease; + .sidebar-box { + height: 40px; + margin-bottom: 10px; + margin-left: 10px; + background-color: var(--skeleton-bg); + + &.child { + margin-left: 30px; + } + + &.section { + height: 25px; + margin-left: 0px; + } + } +} + [data-page-route="Workspaces"] { @media (min-width: map-get($grid-breakpoints, "lg")) { .layout-main { @@ -764,7 +818,6 @@ body { .layout-side-section, .layout-main-section-wrapper { height: 100%; overflow-y: auto; - padding-right: 25px; scrollbar-color: var(--gray-200) transparent; [data-theme="dark"] & { scrollbar-color: var(--gray-800) transparent; @@ -783,7 +836,12 @@ body { } .layout-side-section { - margin-right: 20px; + padding-right: 15px; + } + + .layout-main-section { + padding: var(--padding-md); + margin-bottom: var(--margin-sm); } .desk-sidebar { @@ -792,9 +850,15 @@ body { } } + .layout-main-section { + background-color: var(--fg-color); + padding: var(--padding-sm); + border-radius: var(--border-radius-lg); + } + .block-menu-item-icon svg{ - width: 12px; - height: 12px; + width: 18px; + height: 18px; margin-right: 5px; } @@ -803,7 +867,6 @@ body { padding: 0px; .sidebar-item-control { - > * { align-self: center; margin-left: 3px; @@ -816,7 +879,7 @@ body { display: none; } - .delete-page { + .setting-btn, .duplicate-page { display: none; } @@ -824,13 +887,13 @@ body { padding: 10px 12px 10px 2px; } - .sidebar-info { - display: none; - } - svg { margin-right: 0; } + + .dropdown-list { + top: 42px; + } } .sidebar-item-label { @@ -846,6 +909,7 @@ body { } .sidebar-item-container { + position: relative; .sidebar-item-container{ margin-left: 10px; @@ -863,19 +927,14 @@ body { display: inline-block; } - .delete-page { - display: inline-block; - margin-right: 8px; - } - - .sidebar-info { + .setting-btn, .duplicate-page { display: inline-block; margin-right: 8px; } .drop-icon { padding: 10px 8px 10px 2px; - margin-left: -4px; + margin-left: -8px; } } @@ -899,11 +958,81 @@ body { margin: 0px -7px; padding-bottom: 20px !important; - .ce-block{ + .ce-block { width: 100%; padding-left: 0; padding-right: 0; + .ce-header b { + font-weight: 600 !important; + } + + .new-block-button { + position: absolute; + top: 14px; + left: -22px; + cursor: pointer; + visibility: hidden; + opacity: 0; + transition: visibility 0s, opacity 0.5s ease-in-out; + } + + .edit-mode { + .widget-control > *, .paragraph-control > * { + width: 0px; + visibility: hidden; + opacity: 0; + transition: visibility 0s, opacity 0.5s ease-in-out; + } + + .link-item { + pointer-events: none; + } + } + + &:hover { + .widget-control > *, .new-block-button { + width: auto; + visibility: visible; + opacity: 1; + } + } + + &.ce-block--focused { + .widget { + box-shadow: var(--shadow-base) !important; + + .widget-control > * { + width: auto; + visibility: visible; + opacity: 1; + } + + &.shortcut, &.header { + background-color: var(--fg-color) !important; + } + + &.onboarding { + background-color: var(--fg-color); + + .onboarding-step { + &.active, + &:hover { + background-color: var(--bg-color); + + .step-index.step-pending { + background-color: var(--fg-color); + } + } + + .step-index { + background-color: var(--bg-color); + } + } + } + } + } + &.ce-block--selected { .ce-block__content { background-color: inherit; @@ -923,50 +1052,125 @@ body { pointer-events: none; } + .resizer { + width: 10px; + height: 100%; + position:absolute; + right: 0; + bottom: 0; + cursor: col-resize; + border-color: transparent; + transition: border-color 0.3s ease-in-out; + + &:hover { + border-right: 3px solid var(--gray-400) !important; + } + } + .ce-header { - padding: 0 !important; + padding-left: 7px !important; margin-bottom: 0 !important; flex: 1; + + &:focus { + outline: none; + } + } + + .block-list-container { + left: 20px; + top: 55px !important; + width: 200px !important; } - .widget{ + .dropdown-title { + padding: 6px 10px; + font-size: smaller; + cursor: default; + } + + .ce-paragraph[data-placeholder]:empty::before { + opacity: 1; + } + + .widget { + &.edit-mode { + padding: 7px 12px; + + &:hover { + box-shadow: var(--shadow-base); + background-color: var(--fg-color); + } + + &.spacer { + align-items: inherit; + color: var(--text-muted); + border: 1px dashed var(--gray-400); + cursor: pointer; + + .widget-control > * { + width: auto; + } + + .spacer-left { + min-width: 74px; + } + } + } + + &.spacer { + height: 18px !important; + } + + &.ce-paragraph { + display: block; + } + + &.paragraph { + cursor: text; + + .ce-paragraph { + padding: 2px; + } + + .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: grabbing; + } + } + } + &.header { display: flex; justify-content: center; flex: 1; - padding-left: 15px !important; - padding-right: 15px !important; - min-height: 50px; + padding-left: 0px !important; + min-height: 40px; box-shadow: none; background-color: var(--control-bg); color: var(--text-muted); - } - - &:focus { - outline: none; - } + cursor: text; - &.new-widget { - align-items: inherit; + .ce-header { + padding-left: 14px !important; + } } - &.ce-paragraph { - display: block; + &.shortcut { + background-color: var(--control-bg); } - .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: grabbing; - } + &:focus { + outline: none; } } } @@ -978,14 +1182,21 @@ body { } .ce-toolbar { + + &.ce-toolbar--opened { + display: none; + } + svg { fill: currentColor; } .icon { stroke: none; - width: fit-content; - height: fit-content; + + &.icon--plus { + width: 14px; + } } .ce-settings { @@ -993,6 +1204,10 @@ body { .ce-settings__button, .cdx-settings-button { color: #707684; + + .icon { + width: 14px; + } } .cdx-settings-button--active { @@ -1024,6 +1239,10 @@ body { .icon { fill: currentColor; } + + svg { + stroke: none; + } } @media (min-width: 1199px) { @@ -1037,25 +1256,63 @@ body { } } - @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%; - } + .cdx-marker { + background: rgba(245,235,111,0.29); + padding: 3px 0; + } + + .header-inline-tool { + border: none; + background-color: transparent; + margin-bottom: 2px; + } + + .header-level-select { + display: flex; + flex-direction: column; + padding: 6px; + } + + .header-level-select .header-level { + border: none; + background-color: transparent; + border-radius: var(--border-radius-sm); + padding: 6px; + margin: 2px 0px; + + &:hover { + background-color: var(--fg-hover-color); } + } + + .dropdown-btn { + position: relative; + } + + .dropdown-list { + position: absolute; + background-color: var(--fg-color); + box-shadow: var(--shadow-base) !important; + border-radius: var(--border-radius-sm); + padding: 6px; + top: 30px; + right: 0; + width: 150px; + z-index: 1; + } + .dropdown-list .dropdown-item { + cursor: pointer; + padding: 6px 10px; + font-size: small; + border-radius: var(--border-radius-sm); + margin: 1px 0px; } + + .dropdown-item-icon { + margin-right: 5px; + } + } diff --git a/frappe/website/workspace/website/website.json b/frappe/website/workspace/website/website.json index bd06f0a131..a0d9a817d4 100644 --- a/frappe/website/workspace/website/website.json +++ b/frappe/website/workspace/website/website.json @@ -1,6 +1,6 @@ { "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}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Website\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Blog Post\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Blogger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Web Page\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Web Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"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", "docstatus": 0, "doctype": "Workspace", @@ -232,7 +232,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.154033", + "modified": "2022-01-13 17:49:41.527194", "modified_by": "Administrator", "module": "Website", "name": "Website", @@ -241,7 +241,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 28, + "sequence_id": 28.0, "shortcuts": [ { "color": "Green",