diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index cb502f68a7..3eefd1ce82 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -137,7 +137,7 @@ jobs: - name: UI Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT env: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 5f1ab86d41..03ab61fac4 100644 --- a/cypress/integration/control_barcode.js +++ b/cypress/integration/control_barcode.js @@ -21,7 +21,6 @@ context('Control Barcode', () => { get_dialog_with_barcode().as('dialog'); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') - .focus() .type('123456789') .blur(); cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') @@ -38,7 +37,6 @@ context('Control Barcode', () => { get_dialog_with_barcode().as('dialog'); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') - .focus() .type('123456789') .blur(); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') 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/datetime.js b/cypress/integration/datetime.js index ef1952dc94..4a24faf40b 100644 --- a/cypress/integration/datetime.js +++ b/cypress/integration/datetime.js @@ -103,6 +103,7 @@ context('Control Date, Time and DateTime', () => { input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata) } ]; + datetime_formats.forEach(d => { it(`test datetime format ${d.date_format} ${d.time_format}`, () => { cy.set_value('System Settings', 'System Settings', { diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js index 7752ad0f0b..607db506c7 100644 --- a/cypress/integration/multi_select_dialog.js +++ b/cypress/integration/multi_select_dialog.js @@ -77,11 +77,11 @@ context('MultiSelectDialog', () => { it('tests more button', () => { cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="more_btn"]`) + .get(`.frappe-control[data-fieldname="more_child_btn"]`) .should('exist') .as('more-btn'); - cy.get_open_dialog().get('.list-item-container').should(($rows) => { + cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => { expect($rows).to.have.length(20); }); @@ -89,7 +89,7 @@ context('MultiSelectDialog', () => { cy.get('@more-btn').find('button').click({force: true}); cy.wait('@get-more-records'); - cy.get_open_dialog().get('.list-item-container').should(($rows) => { + cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => { if ($rows.length <= 20) { throw new Error("More button doesn't work"); } diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 629ae72eb8..0253e8fd43 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -7,8 +7,6 @@ context('Report View', () => { cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); - }); - it('Field with enabled allow_on_submit should be editable.', () => { cy.insert_doc(doctype_name, { 'title': 'Doc 1', 'description': 'Random Text', @@ -16,6 +14,8 @@ context('Report View', () => { // submit document 'docstatus': 1 }, true).as('doc'); + }); + it('Field with enabled allow_on_submit should be editable.', () => { cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); // check status column added from docstatus diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index 1b7634d211..5808bd52ef 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -14,7 +14,7 @@ context('Timeline Email', () => { cy.wait(700); }); - it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { + it('Adding email and verifying timeline content for email attachment', () => { cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject').eq(0).click(); @@ -43,7 +43,9 @@ context('Timeline Email', () => { cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click(); cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click(); cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); + }); + it('Deleting attachment and ToDo', () => { cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); 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/__init__.py b/frappe/__init__.py index 20fc41b83a..c6cbfead43 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -446,7 +446,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list) def emit_js(js, user=False, **kwargs): - if user == False: + if user is False: user = session.user publish_realtime('eval_js', js, user=user, **kwargs) diff --git a/frappe/auth.py b/frappe/auth.py index a87edb6460..d4778eb0c1 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -111,7 +111,8 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if self.login()==False: return + if self.login() is False: + return self.resume = False # run login triggers 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/cache_manager.py b/frappe/cache_manager.py index 0df8878da4..94a845639b 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -148,7 +148,7 @@ def build_table_count_cache(): data = ( frappe.qb.from_(information_schema.tables).select(table_name, table_rows) ).run(as_dict=True) - counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data} + counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} _cache.set_value("information_schema:counts", counts) return counts diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 677325e02d..62488525b0 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup): doctype_tables = frappe.get_all("DocType", pluck="name") for x in database_tables: - doctype = x.lstrip("tab") + doctype = x.replace("tab", "", 1) if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): TABLES_TO_DROP.append(x) @@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup): odb = scheduled_backup( ignore_conf=False, - include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP), + include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP), ignore_files=True, force=True, ) diff --git a/frappe/core/doctype/payment_gateway/payment_gateway.json b/frappe/core/doctype/payment_gateway/payment_gateway.json index b97d72c771..7195b3949e 100644 --- a/frappe/core/doctype/payment_gateway/payment_gateway.json +++ b/frappe/core/doctype/payment_gateway/payment_gateway.json @@ -1,154 +1,55 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:gateway", - "beta": 0, - "creation": "2015-12-15 22:26:45.221162", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "autoname": "field:gateway", + "creation": "2022-01-24 21:09:47.229371", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gateway", + "gateway_settings", + "gateway_controller" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Gateway", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Gateway", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_settings", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Settings", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway_settings", + "fieldtype": "Link", + "label": "Gateway Settings", + "options": "DocType" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_controller", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Controller", - "length": 0, - "no_copy": 0, - "options": "gateway_settings", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "gateway_controller", + "fieldtype": "Dynamic Link", + "label": "Gateway Controller", + "options": "gateway_settings" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-05 14:24:33.526645", - "modified_by": "Administrator", - "module": "Core", - "name": "Payment Gateway", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2022-01-24 21:17:03.864719", + "modified_by": "Administrator", + "module": "Core", + "name": "Payment Gateway", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index 661ac932e7..626ab772b8 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -121,7 +121,7 @@ class UserType(Document): for child_table in doc.get_table_fields(): child_doc = frappe.get_meta(child_table.options) - if not child_doc.istable: + if child_doc: self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes) if select_doctypes: 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/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 488c468025..1593ed49a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -107,20 +107,26 @@ class CustomizeForm(Document): def set_name_translation(self): '''Create, update custom translation for this doctype''' current = self.get_name_translation() - if current: - if self.label and current.translated_text != self.label: - frappe.db.set_value('Translation', current.name, 'translated_text', self.label) - frappe.translate.clear_cache() - else: + if not self.label: + if current: # clear translation frappe.delete_doc('Translation', current.name) + return - else: - if self.label: - frappe.get_doc(dict(doctype='Translation', - source_text=self.doc_type, - translated_text=self.label, - language_code=frappe.local.lang or 'en')).insert() + if not current: + frappe.get_doc( + { + "doctype": 'Translation', + "source_text": self.doc_type, + "translated_text": self.label, + "language_code": frappe.local.lang or 'en' + } + ).insert() + return + + if self.label != current.translated_text: + frappe.db.set_value('Translation', current.name, 'translated_text', self.label) + frappe.translate.clear_cache() def clear_existing_doc(self): doc_type = self.doc_type diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 8a287b17e8..0fe39e0008 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -304,3 +304,25 @@ class TestCustomizeForm(unittest.TestCase): action = [d for d in event.actions if d.label=='Test Action'] self.assertEqual(len(action), 0) + + def test_custom_label(self): + d = self.get_customize_form("Event") + + # add label + d.label = "Test Rename" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename") + + # change label + d.label = "Test Rename 2" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # saving again to make sure existing label persists + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # clear label + d.label = "" + d.run_method("save_customization") + self.assertEqual(d.label, "") 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/database/mariadb/database.py b/frappe/database/mariadb/database.py index 6b827a4e89..de28dad900 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -245,9 +245,16 @@ class MariaDBDatabase(Database): column_name as 'name', column_type as 'type', column_default as 'default', - column_key = 'MUL' as 'index', + COALESCE( + (select 1 + from information_schema.statistics + where table_name="{table_name}" + and column_name=columns.column_name + and NON_UNIQUE=1 + limit 1 + ), 0) as 'index', column_key = 'UNI' as 'unique' - from information_schema.columns + from information_schema.columns as columns where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1) def has_index(self, table_name, index_name): diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 5768a2f23d..07bb4d5d7c 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -58,18 +58,34 @@ class MariaDBTable(DBTable): modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition())) for col in self.add_index: - # if index key not exists - if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" % - (self.table_name, '%s'), col.fieldname): - add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname)) + # if index key does not exists + if not frappe.db.has_index(self.table_name, col.fieldname + '_index'): + add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname)) - for col in self.drop_index: + for col in self.drop_index + self.drop_unique: if col.fieldname != 'name': # primary key + current_column = self.current_columns.get(col.fieldname.lower()) + unique_constraint_changed = current_column.unique != col.unique + if unique_constraint_changed and not col.unique: + # nosemgrep + unique_index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=0 + """.format(self.table_name), (col.fieldname), as_dict=1) + if unique_index_record: + drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name)) + index_constraint_changed = current_column.index != col.set_index # if index key exists - if frappe.db.sql("""SHOW INDEX FROM `{0}` - WHERE key_name=%s - AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)): - drop_index_query.append("drop index `{}`".format(col.fieldname)) + if index_constraint_changed and not col.set_index: + # nosemgrep + index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=1 + """.format(self.table_name), (col.fieldname + '_index'), as_dict=1) + if index_record: + drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name)) try: for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 33f07990af..d5495c6879 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -77,11 +77,11 @@ class PostgresDatabase(Database): """Escape quotes and percent in given string.""" if isinstance(s, bytes): s = s.decode('utf-8') - + # MariaDB's driver treats None as an empty string # So Postgres should do the same - if s is None: + if s is None: s = '' if percent: @@ -308,18 +308,20 @@ class PostgresDatabase(Database): WHEN 'timestamp without time zone' THEN 'timestamp' ELSE a.data_type END AS type, - COUNT(b.indexdef) AS Index, + BOOL_OR(b.index) AS index, SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, BOOL_OR(b.unique) AS unique FROM information_schema.columns a LEFT JOIN - (SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique + (SELECT indexdef, tablename, + indexdef LIKE '%UNIQUE INDEX%' AS unique, + indexdef NOT LIKE '%UNIQUE INDEX%' AS index FROM pg_indexes WHERE tablename='{table_name}') b - ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%') + ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') WHERE a.table_name = '{table_name}' - GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;''' - .format(table_name=table_name), as_dict=1) + GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; + '''.format(table_name=table_name), as_dict=1) def get_database_list(self, target): return [d[0] for d in self.sql("SELECT datname FROM pg_database;")] diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 58153ca6ce..a2d5be0b70 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -11,8 +11,6 @@ class PostgresTable(DBTable): column_defs = self.get_column_definitions() if column_defs: add_text += ',\n'.join(column_defs) - # index - # index_defs = self.get_index_definitions() # TODO: set docstatus length # create table frappe.db.sql("""create table `%s` ( @@ -28,8 +26,25 @@ class PostgresTable(DBTable): idx bigint not null default '0', %s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text)) + self.create_indexes() frappe.db.commit() + def create_indexes(self): + create_index_query = "" + for key, col in self.columns.items(): + if (col.set_index + and col.fieldtype in frappe.db.type_map + and frappe.db.type_map.get(col.fieldtype)[0] + not in ('text', 'longtext')): + create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + if create_index_query: + # nosemgrep + frappe.db.sql(create_index_query) + def alter(self): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) @@ -52,8 +67,8 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format( col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length), - using_clause) - ) + using_clause + )) for col in self.set_default: if col.fieldname=="name": @@ -73,37 +88,54 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default)) - create_index_query = "" + create_contraint_query = "" for col in self.add_index: # if index key not exists - create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( index_name=col.fieldname, table_name=self.table_name, field=col.fieldname) - drop_index_query = "" + for col in self.add_unique: + # if index key not exists + create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + + drop_contraint_query = "" for col in self.drop_index: # primary key if col.fieldname != 'name': # if index key exists - if not frappe.db.has_index(self.table_name, col.fieldname): - drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) + drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) - if query: - try: + for col in self.drop_unique: + # primary key + if col.fieldname != 'name': + # if index key exists + drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname) + try: + if query: final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query)) - if final_alter_query: frappe.db.sql(final_alter_query) - if create_index_query: frappe.db.sql(create_index_query) - if drop_index_query: frappe.db.sql(drop_index_query) - except Exception as e: - # sanitize - if frappe.db.is_duplicate_fieldname(e): - frappe.throw(str(e)) - elif frappe.db.is_duplicate_entry(e): - fieldname = str(e).split("'")[-2] - frappe.throw(_("""{0} field cannot be set as unique in {1}, - as there are non-unique existing values""".format( - fieldname, self.table_name))) - raise e - else: - raise e + # nosemgrep + frappe.db.sql(final_alter_query) + if create_contraint_query: + # nosemgrep + frappe.db.sql(create_contraint_query) + if drop_contraint_query: + # nosemgrep + frappe.db.sql(drop_contraint_query) + except Exception as e: + # sanitize + if frappe.db.is_duplicate_fieldname(e): + frappe.throw(str(e)) + elif frappe.db.is_duplicate_entry(e): + fieldname = str(e).split("'")[-2] + frappe.throw( + _("{0} field cannot be set as unique in {1}, as there are non-unique existing values") + .format(fieldname, self.table_name) + ) + else: + raise e diff --git a/frappe/database/query.py b/frappe/database/query.py index 6d2be5fa25..587378b32f 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -308,7 +308,7 @@ class Permission: doctype = [doctype] for dt in doctype: - dt = re.sub("tab", "", dt) + dt = re.sub("^tab", "", dt) if not frappe.has_permission( dt, "select", diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 10582eff8f..9a6dd502dc 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -21,6 +21,7 @@ class DBTable: self.change_name = [] self.add_unique = [] self.add_index = [] + self.drop_unique = [] self.drop_index = [] self.set_default = [] @@ -219,8 +220,10 @@ class DbColumn: self.table.change_type.append(self) # unique - if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): + if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): self.table.add_unique.append(self) + elif (current_def['unique'] and not self.unique) and column_type not in ('text', 'longtext'): + self.table.drop_unique.append(self) # default if (self.default_changed(current_def) @@ -230,9 +233,7 @@ class DbColumn: self.table.set_default.append(self) # index should be applied or dropped irrespective of type change - if ((current_def['index'] and not self.set_index and not self.unique) - or (current_def['unique'] and not self.unique)): - # to drop unique you have to drop index + if (current_def['index'] and not self.set_index) and column_type not in ('text', 'longtext'): self.table.drop_index.append(self) elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')): 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/installer.py b/frappe/installer.py old mode 100755 new mode 100644 index b50fa4a3b5..d892ff4ddc --- a/frappe/installer.py +++ b/frappe/installer.py @@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True): for before_install in app_hooks.before_install or []: out = frappe.get_attr(before_install)() - if out==False: + if out is False: return if name != "frappe": @@ -346,14 +346,15 @@ def post_install(rebuild_website=False): def set_all_patches_as_completed(app): - patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt") - if os.path.exists(patch_path): - for patch in frappe.get_file_items(patch_path): - frappe.get_doc({ - "doctype": "Patch Log", - "patch": patch - }).insert(ignore_permissions=True) - frappe.db.commit() + from frappe.modules.patch_handler import get_patches_from_app + + patches = get_patches_from_app(app) + for patch in patches: + frappe.get_doc({ + "doctype": "Patch Log", + "patch": patch + }).insert(ignore_permissions=True) + frappe.db.commit() def init_singles(): 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/migrate.py b/frappe/migrate.py index 6abc38796f..d13fe858f7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -19,6 +19,8 @@ from frappe.modules.utils import sync_customizations from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.search.website_search import build_index_for_all_routes from frappe.database.schema import add_column +from frappe.modules.patch_handler import PatchType + def migrate(verbose=True, skip_failing=False, skip_search_index=False): @@ -59,16 +61,13 @@ Otherwise, check the server logs and ensure that all the required services are r clear_global_cache() - #run before_migrate hooks for app in frappe.get_installed_apps(): for fn in frappe.get_hooks('before_migrate', app_name=app): frappe.get_attr(fn)() - # run patches - frappe.modules.patch_handler.run_all(skip_failing) - - # sync + frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync) frappe.model.sync.sync_all() + frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync) frappe.translate.clear_cache() sync_jobs() sync_fixtures() @@ -78,18 +77,16 @@ Otherwise, check the server logs and ensure that all the required services are r frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() - # syncs statics + # syncs static files clear_website_cache() # updating installed applications data frappe.get_single('Installed Applications').update_versions() - #run after_migrate hooks for app in frappe.get_installed_apps(): for fn in frappe.get_hooks('after_migrate', app_name=app): frappe.get_attr(fn)() - # build web_routes index if not skip_search_index: # Run this last as it updates the current session print('Building search index for {}'.format(frappe.local.site)) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 2cc5818414..6ffaadc5eb 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -80,6 +80,7 @@ def rename_doc( if doctype=='DocType': rename_doctype(doctype, old, new, force) + update_customizations(old, new) update_attachments(doctype, old, new) @@ -174,6 +175,8 @@ def update_user_settings(old, new, link_fields): else: continue +def update_customizations(old: str, new: str) -> None: + frappe.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False) def update_attachments(doctype, old, new): try: diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 8dfb27c0b8..75d4972152 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -1,37 +1,76 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -""" - Execute Patch Files +""" Patch Handler. + +This file manages execution of manaully written patches. Patches are script +that apply changes in database schema or data to accomodate for changes in the +code. + +Ways to specify patches: + +1. patches.txt file specifies patches that run before doctype schema +migration. Each line represents one patch (old format). +2. patches.txt can alternatively also separate pre and post model sync +patches by using INI like file format: + ```patches.txt + [pre_model_sync] + app.module.patch1 + app.module.patch2 - To run directly - python lib/wnf.py patch patch1, patch2 etc - python lib/wnf.py patch -f patch1, patch2 etc + [post_model_sync] + app.module.patch3 + ``` - where patch1, patch2 is module name + When different sections are specified patches are executed in this order: + 1. Run pre_model_sync patches + 2. Reload/resync all doctype schema + 3. Run post_model_sync patches + + Hence any patch that just needs to modify data but doesn't depend on + old schema should be added to post_model_sync section of file. + +3. simple python commands can be added by starting line with `execute:` +`execute:` example: `execute:print("hello world")` """ -import frappe, frappe.permissions, time -class PatchError(Exception): pass +import configparser +import time +from enum import Enum +from typing import List, Optional + +import frappe -def run_all(skip_failing=False): + +class PatchError(Exception): + pass + + +class PatchType(Enum): + pre_model_sync = "pre_model_sync" + post_model_sync = "post_model_sync" + + +def run_all(skip_failing: bool = False, patch_type: Optional[PatchType] = None) -> None: """run all pending patches""" - executed = [p[0] for p in frappe.db.sql("""select patch from `tabPatch Log`""")] + executed = set(frappe.get_all("Patch Log", fields="patch", pluck="patch")) frappe.flags.final_patches = [] def run_patch(patch): try: if not run_single(patchmodule = patch): - log(patch + ': failed: STOPPED') + print(patch + ': failed: STOPPED') raise PatchError(patch) except Exception: if not skip_failing: raise else: - log('Failed to execute patch') + print('Failed to execute patch') + + patches = get_all_patches(patch_type=patch_type) - for patch in get_all_patches(): + for patch in patches: if patch and (patch not in executed): run_patch(patch) @@ -40,18 +79,54 @@ def run_all(skip_failing=False): patch = patch.replace('finally:', '') run_patch(patch) -def get_all_patches(): +def get_all_patches(patch_type: Optional[PatchType] = None) -> List[str]: + + if patch_type and not isinstance(patch_type, PatchType): + frappe.throw(f"Unsupported patch type specified: {patch_type}") + patches = [] for app in frappe.get_installed_apps(): - if app == "shopping_cart": - continue - # 3-to-4 fix - if app=="webnotes": - app="frappe" - patches.extend(frappe.get_file_items(frappe.get_pymodule_path(app, "patches.txt"))) + patches.extend(get_patches_from_app(app, patch_type=patch_type)) return patches +def get_patches_from_app(app: str, patch_type: Optional[PatchType] = None) -> List[str]: + """ Get patches from an app's patches.txt + + patches.txt can be: + 1. ini like file with section for different patch_type + 2. plain text file with each line representing a patch. + """ + + patches_txt = frappe.get_pymodule_path(app, "patches.txt") + + try: + # Attempt to parse as ini file with pre/post patches + # allow_no_value: patches are not key value pairs + # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter + parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n") + # preserve case + parser.optionxform = str + parser.read(patches_txt) + + + if not patch_type: + return [patch for patch in parser[PatchType.pre_model_sync.value]] + \ + [patch for patch in parser[PatchType.post_model_sync.value]] + + if patch_type.value in parser.sections(): + return [patch for patch in parser[patch_type.value]] + else: + frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type)) + + except configparser.MissingSectionHeaderError: + # treat as old format with each line representing a single patch + # backward compatbility with old patches.txt format + if not patch_type or patch_type == PatchType.pre_model_sync: + return frappe.get_file_items(patches_txt) + + return [] + def reload_doc(args): import frappe.modules run_single(method = frappe.modules.reload_doc, methodargs = args) @@ -73,7 +148,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.db.begin() start_time = time.time() try: - log('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), + print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), site=frappe.local.site, db=frappe.db.cur_db_name)) if patchmodule: if patchmodule.startswith("finally:"): @@ -96,7 +171,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.db.commit() end_time = time.time() block_user(False) - log('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) + print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) return True @@ -109,10 +184,7 @@ def executed(patchmodule): if patchmodule.startswith('finally:'): # patches are saved without the finally: tag patchmodule = patchmodule.replace('finally:', '') - done = frappe.db.get_value("Patch Log", {"patch": patchmodule}) - # if done: - # print "Patch %s already executed in %s" % (patchmodule, frappe.db.cur_db_name) - return done + return frappe.db.get_value("Patch Log", {"patch": patchmodule}) def block_user(block, msg=None): """stop/start execution till patch is run""" @@ -128,6 +200,3 @@ def check_session_stopped(): if frappe.db.get_global("__session_status")=='stop': frappe.msgprint(frappe.db.get_global("__session_status_message")) raise frappe.SessionStopped('Session Stopped') - -def log(msg): - print (msg) diff --git a/frappe/patches.txt b/frappe/patches.txt index 16ae349941..c393b456e3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,3 +1,4 @@ +[pre_model_sync] frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 execute:frappe.utils.global_search.setup_global_search_table() execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 @@ -87,7 +88,6 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.set_primary_key_in_series execute:frappe.delete_doc("Page", "modules", ignore_missing=True) -frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.setup_comments_from_communications frappe.patches.v12_0.replace_null_values_in_tables frappe.patches.v12_0.reset_home_settings @@ -123,7 +123,7 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.set_correct_url_in_files -execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) +execute:frappe.reload_doc('core', 'doctype', 'doctype') execute:frappe.reload_doc('custom', 'doctype', 'property_setter') frappe.patches.v13_0.remove_invalid_options_for_data_fields frappe.patches.v13_0.website_theme_custom_scss @@ -184,12 +184,14 @@ frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.set_first_day_of_the_week -frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents -frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_workspace2 # 20.09.2021 +frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 +frappe.patches.v14_0.transform_todo_schema + +[post_model_sync] +frappe.patches.v14_0.drop_data_import_legacy +frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_github_endpoints #08-11-2021 frappe.patches.v14_0.remove_db_aggregation -frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.update_color_names_in_kanban_board_column -frappe.patches.v14_0.transform_todo_schema diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py index d3a5c59209..8ef9cfaf1f 100644 --- a/frappe/patches/v14_0/copy_mail_data.py +++ b/frappe/patches/v14_0/copy_mail_data.py @@ -3,9 +3,6 @@ import frappe def execute(): - frappe.reload_doc("email", "doctype", "imap_folder") - frappe.reload_doc("email", "doctype", "email_account") - # patch for all Email Account with the flag use_imap for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}): # get all data from Email Account diff --git a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py index ea8a10e43a..ff03604754 100644 --- a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py +++ b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("desk", "doctype", "kanban_board_column") indicator_map = { 'blue': 'Blue', 'orange': 'Orange', 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/permissions.py b/frappe/permissions.py index 59ca9c2cb2..af17faba01 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -23,7 +23,7 @@ def print_has_permission_check_logs(func): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user - raise_exception = False if kwargs.get('raise_exception') == False else True + raise_exception = False if kwargs.get('raise_exception') is False else True # print only if access denied # and if user is checking his own permission 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/desk.js b/frappe/public/js/frappe/desk.js index 202cee645a..51ada70948 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -214,19 +214,20 @@ frappe.Application = class Application { email_password_prompt(email_account,user,i) { var me = this; + const email_id = email_account[i]["email_id"]; let d = new frappe.ui.Dialog({ title: __('Password missing in Email Account'), fields: [ { 'fieldname': 'password', 'fieldtype': 'Password', - 'label': __('Please enter the password for: {0}', [email_account[i]["email_id"]]), + 'label': __('Please enter the password for: {0}', [email_id], "Email Account"), 'reqd': 1 }, { "fieldname": "submit", "fieldtype": "Button", - "label": __("Submit") + "label": __("Submit", null, "Submit password for Email Account") } ] }); diff --git a/frappe/public/js/frappe/form/controls/date_range.js b/frappe/public/js/frappe/form/controls/date_range.js index 727e9d55c2..170404f575 100644 --- a/frappe/public/js/frappe/form/controls/date_range.js +++ b/frappe/public/js/frappe/form/controls/date_range.js @@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form. language: "en", range: true, autoClose: true, - toggleSelected: false + toggleSelected: false, + firstDay: frappe.datetime.get_first_day_of_the_week_index() }; this.datepicker_options.dateFormat = (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); 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/form/form.js b/frappe/public/js/frappe/form/form.js index bbc6ecda28..5c0b6b1399 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -943,7 +943,10 @@ frappe.ui.form.Form = class FrappeForm { // re-enable buttons resolve(); } - frappe.throw (__("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)])); + + frappe.throw( + __("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)], "{0} = verb, {1} = object") + ); } } diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 65d84e2202..934c90f017 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -7,12 +7,12 @@ frappe.ui.form.save = function (frm, action, callback, btn) { $(btn).prop("disabled", true); // specified here because there are keyboard shortcuts to save - var working_label = { - "Save": __("Saving"), - "Submit": __("Submitting"), - "Update": __("Updating"), - "Amend": __("Amending"), - "Cancel": __("Cancelling") + const working_label = { + "Save": __("Saving", null, "Freeze message while saving a document"), + "Submit": __("Submitting", null, "Freeze message while submitting a document"), + "Update": __("Updating", null, "Freeze message while updating a document"), + "Amend": __("Amending", null, "Freeze message while amending a document"), + "Cancel": __("Cancelling", null, "Freeze message while cancelling a document"), }[toTitle(action)]; var freeze_message = working_label ? __(working_label) : ""; @@ -154,8 +154,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { if (error_fields.length) { let meta = frappe.get_meta(doc.doctype); if (meta.istable) { - var message = __('Mandatory fields required in table {0}, Row {1}', - [__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]); + const table_label = __(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(); + var message = __('Mandatory fields required in table {0}, Row {1}', [table_label, doc.idx]); } else { var message = __('Mandatory fields required in {0}', [__(doc.doctype)]); } @@ -276,4 +276,3 @@ frappe.ui.form.update_calling_link = (newdoc) => { frappe._from_link = null; } } - diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 22f8377a57..3cde04313f 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -200,7 +200,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { frappe.render_template("list_view_permission_restrictions", { condition_list: match_rules_list, }), - __("Restrictions") + __("Restrictions", null, "Title of message showing restrictions in list view") ); } @@ -255,8 +255,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { set_primary_action() { if (this.can_create) { + const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype); + + // Better style would be __("Add {0}", [doctype_name], "Primary action in list view") + // Keeping it like this to not disrupt existing translations + const label = `${__("Add", null, "Primary action in list view")} ${doctype_name}`; this.page.set_primary_action( - `${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`, + label, () => { if (this.settings.primary_action) { this.settings.primary_action(); @@ -320,9 +325,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { setup_freeze_area() { this.$freeze = $( - `
${__( - "Loading" - )}...
` + `
+ ${__("Loading")}... +
` ).hide(); this.$result.append(this.$freeze); } @@ -460,8 +465,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ? __("No {0} found", [__(this.doctype)]) : __("You haven't created a {0} yet", [__(this.doctype)]); let new_button_label = filters && filters.length - ? __("Create a new {0}", [__(this.doctype)]) - : __("Create your first {0}", [__(this.doctype)]); + ? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view") + : __("Create your first {0}", [__(this.doctype)], "Create a new document from list view"); let empty_state_image = this.settings.empty_state_image || "/assets/frappe/images/ui-states/list-empty-state.svg"; @@ -469,7 +474,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const new_button = this.can_create ? `

` +

` : ""; return `
@@ -486,7 +493,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (this.list_view_settings && !this.list_view_settings.disable_count) { this.$result .find(".list-count") - .html(`${__("Refreshing")}...`); + .html(`${__("Refreshing", null, "Document count in list view")}...`); } } @@ -1081,14 +1088,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { frappe.ui.keys.add_shortcut({ shortcut: "down", action: () => handle_navigation("down"), - description: __("Navigate list down"), + description: __("Navigate list down", null, "Description of a list view shortcut"), page: this.page, }); frappe.ui.keys.add_shortcut({ shortcut: "up", action: () => handle_navigation("up"), - description: __("Navigate list up"), + description: __("Navigate list up", null, "Description of a list view shortcut"), page: this.page, }); @@ -1100,7 +1107,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { check_row($list_row); focus_next(); }, - description: __("Select multiple list items"), + description: __("Select multiple list items", null, "Description of a list view shortcut"), page: this.page, }); @@ -1112,7 +1119,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { check_row($list_row); focus_prev(); }, - description: __("Select multiple list items"), + description: __("Select multiple list items", null, "Description of a list view shortcut"), page: this.page, }); @@ -1126,7 +1133,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } return false; }, - description: __("Open list item"), + description: __("Open list item", null, "Description of a list view shortcut"), page: this.page, }); @@ -1140,7 +1147,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } return false; }, - description: __("Select list item"), + description: __("Select list item", null, "Description of a list view shortcut"), page: this.page, }); } @@ -1515,7 +1522,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.model.can_import(doctype, null, this.meta)) { items.push({ - label: __("Import"), + label: __("Import", null, "Button in list view menu"), action: () => frappe.set_route("list", "data-import", { reference_doctype: doctype, @@ -1526,7 +1533,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.model.can_set_user_permissions(doctype)) { items.push({ - label: __("User Permissions"), + label: __("User Permissions", null, "Button in list view menu"), action: () => frappe.set_route("list", "user-permission", { allow: doctype, @@ -1537,7 +1544,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.user_roles.includes("System Manager")) { items.push({ - label: __("Role Permissions Manager"), + label: __("Role Permissions Manager", null, "Button in list view menu"), action: () => frappe.set_route("permission-manager", { doctype, @@ -1546,7 +1553,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); items.push({ - label: __("Customize"), + label: __("Customize", null, "Button in list view menu"), action: () => { if (!this.meta) return; if (this.meta.custom) { @@ -1563,7 +1570,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } items.push({ - label: __("Toggle Sidebar"), + label: __("Toggle Sidebar", null, "Button in list view menu"), action: () => this.toggle_side_bar(), condition: () => !this.hide_sidebar, standard: true, @@ -1571,7 +1578,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); items.push({ - label: __("Share URL"), + label: __("Share URL", null, "Button in list view menu"), action: () => this.share_url(), standard: true, shortcut: "Ctrl+L", @@ -1583,7 +1590,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ) { // edit doctype items.push({ - label: __("Edit DocType"), + label: __("Edit DocType", null, "Button in list view menu"), action: () => frappe.set_route("form", "doctype", doctype), standard: true, }); @@ -1591,7 +1598,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.user.has_role("System Manager")) { items.push({ - label: __("List Settings"), + label: __("List Settings", null, "Button in list view menu"), action: () => this.show_list_settings(), standard: true, }); @@ -1682,7 +1689,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { // utility const bulk_assignment = () => { return { - label: __("Assign To"), + label: __("Assign To", null, "Button in list view actions menu"), action: () => { this.disable_list_update = true; bulk_operations.assign( @@ -1700,7 +1707,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_assignment_rule = () => { return { - label: __("Apply Assignment Rule"), + label: __("Apply Assignment Rule", null, "Button in list view actions menu"), action: () => { this.disable_list_update = true; bulk_operations.apply_assignment_rule( @@ -1718,7 +1725,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_add_tags = () => { return { - label: __("Add Tags"), + label: __("Add Tags", null, "Button in list view actions menu"), action: () => { this.disable_list_update = true; bulk_operations.add_tags( @@ -1736,7 +1743,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_printing = () => { return { - label: __("Print"), + label: __("Print", null, "Button in list view actions menu"), action: () => bulk_operations.print(this.get_checked_items()), standard: true, }; @@ -1744,13 +1751,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_delete = () => { return { - label: __("Delete"), + label: __("Delete", null, "Button in list view actions menu"), action: () => { const docnames = this.get_checked_items(true).map( (docname) => docname.toString() ); frappe.confirm( - __("Delete {0} items permanently?", [docnames.length]), + __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"), () => { this.disable_list_update = true; bulk_operations.delete(docnames, () => { @@ -1767,12 +1774,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_cancel = () => { return { - label: __("Cancel"), + label: __("Cancel", null, "Button in list view actions menu"), action: () => { const docnames = this.get_checked_items(true); if (docnames.length > 0) { frappe.confirm( - __("Cancel {0} documents?", [docnames.length]), + __("Cancel {0} documents?", [docnames.length], "Title of confirmation dialog"), () => { this.disable_list_update = true; bulk_operations.submit_or_cancel( @@ -1793,12 +1800,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_submit = () => { return { - label: __("Submit"), + label: __("Submit", null, "Button in list view actions menu"), action: () => { const docnames = this.get_checked_items(true); if (docnames.length > 0) { frappe.confirm( - __("Submit {0} documents?", [docnames.length]), + __("Submit {0} documents?", [docnames.length], "Title of confirmation dialog"), () => { this.disable_list_update = true; bulk_operations.submit_or_cancel( @@ -1820,7 +1827,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_edit = () => { return { - label: __("Edit"), + label: __("Edit", null, "Button in list view actions menu"), action: () => { let field_mappings = {}; @@ -1850,7 +1857,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const bulk_export = () => { return { - label: __("Export"), + label: __("Export", null, "Button in list view actions menu"), action: () => { const docnames = this.get_checked_items(true); diff --git a/frappe/public/js/frappe/model/sync.js b/frappe/public/js/frappe/model/sync.js index 2cb7ca1a32..48f886364c 100644 --- a/frappe/public/js/frappe/model/sync.js +++ b/frappe/public/js/frappe/model/sync.js @@ -39,7 +39,7 @@ Object.assign(frappe.model, { } frappe.model.sync_docinfo(r); - + return r.docs; }, rename_after_save: (d, i) => { 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/dialog.js b/frappe/public/js/frappe/ui/dialog.js index e2e51ce501..1618db9939 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -57,8 +57,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { // show footer this.action = this.action || { primary: { }, secondary: { } }; if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) { - this.set_primary_action(this.primary_action_label || this.action.primary.label || __("Submit"), - this.primary_action || this.action.primary.onsubmit); + this.set_primary_action( + this.primary_action_label || this.action.primary.label || __("Submit", null, "Primary action in dialog"), + this.primary_action || this.action.primary.onsubmit + ); } if (this.secondary_action) { diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 0e0b6527e6..ac0c01c406 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -63,7 +63,7 @@ frappe.warn = function(title, message_html, proceed_action, primary_label, is_mi if (proceed_action) proceed_action(); d.hide(); }, - secondary_action_label: __("Cancel"), + secondary_action_label: __("Cancel", null, "Secondary button in warning dialog"), secondary_action: () => d.hide(), minimizable: is_minimizable }); diff --git a/frappe/public/js/frappe/ui/sort_selector.js b/frappe/public/js/frappe/ui/sort_selector.js index 544467fb14..879466e8f7 100644 --- a/frappe/public/js/frappe/ui/sort_selector.js +++ b/frappe/public/js/frappe/ui/sort_selector.js @@ -113,42 +113,44 @@ frappe.ui.SortSelector = class SortSelector { if(!this.args.options) { // default options var _options = [ - {'fieldname': 'modified'} + {'fieldname': 'modified'}, + {'fieldname': 'name'}, + {'fieldname': 'creation'}, + {'fieldname': 'idx'}, ] // title field - if(meta.title_field) { - _options.push({'fieldname': meta.title_field}); + if (meta.title_field) { + _options.splice(1, 0, {'fieldname': meta.title_field}); + } + + // sort field - set via DocType schema or Customize Form + if (meta_sort_field) { + _options.splice(1, 0, { 'fieldname': meta_sort_field }); } - // bold or mandatory + // bold, mandatory and fields that are available in list view meta.fields.forEach(function(df) { - if(df.mandatory || df.bold) { + if ( + (df.mandatory || df.bold || df.in_list_view) + && frappe.model.is_value_type(df.fieldtype) + && frappe.perm.has_perm(me.doctype, df.permlevel, "read") + ) { _options.push({fieldname: df.fieldname, label: df.label}); } }); - // meta sort field - if(meta_sort_field) _options.push({ 'fieldname': meta_sort_field }); - - // more default options - _options.push( - {'fieldname': 'name'}, - {'fieldname': 'creation'}, - {'fieldname': 'idx'} - ) + // add missing labels + _options.forEach(option => { + if (!option.label) { + option.label = me.get_label(option.fieldname); + } + }); // de-duplicate - this.args.options = _options.uniqBy(function(obj) { + this.args.options = _options.uniqBy(obj => { return obj.fieldname; }); - - // add missing labels - this.args.options.forEach(function(o) { - if(!o.label) { - o.label = me.get_label(o.fieldname); - } - }); } // set default 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/diffview.js b/frappe/public/js/frappe/utils/diffview.js index ebface7f05..a898a318a1 100644 --- a/frappe/public/js/frappe/utils/diffview.js +++ b/frappe/public/js/frappe/utils/diffview.js @@ -54,7 +54,7 @@ frappe.ui.DiffView = class DiffView { fieldname: "diff", }, ], - size: "large", + size: "extra-large", }); return dialog; } 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/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 6d8e281793..f5d9f3e110 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -340,7 +340,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { options: columns_in_picker }, { - label: __('Insert Column Before {0}', [datatabe_col.docfield.label.bold()]), + label: __('Insert Column Before {0}', [__(datatabe_col.docfield.label).bold()]), fieldname: 'insert_before', fieldtype: 'Check' } @@ -789,7 +789,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { } else { this.fields.splice(col_index, 0, field); } - frappe.show_alert(__('Also adding the dependent currency field {0}', [field[0].bold()])); + const field_label = frappe.meta.get_label(doctype, field[0]); + frappe.show_alert( + __('Also adding the dependent currency field {0}', [__(field_label).bold()]) + ); } } @@ -799,7 +802,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { const field = [col, doctype]; this.fields.push(field); this.refresh(); - frappe.show_alert(__('Also adding the status dependency field {0}', [field[0].bold()])); + const field_label = frappe.meta.get_label(doctype, field[0]); + frappe.show_alert( + __('Also adding the status dependency field {0}', [__(field_label).bold()]) + ); } } 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/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 1f540958df..a45fc941d3 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -160,17 +160,17 @@ export default class WebForm extends frappe.ui.FieldGroup { } setup_primary_action() { - this.add_button_to_header(this.button_label || "Save", "primary", () => + this.add_button_to_header(this.button_label || __("Save", null, "Button in web form"), "primary", () => this.save() ); - this.add_button_to_footer(this.button_label || "Save", "primary", () => + this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () => this.save() ); } setup_cancel_button() { - this.add_button_to_header(__("Cancel"), "light", () => this.cancel()); + this.add_button_to_header(__("Cancel", null, "Button in web form"), "light", () => this.cancel()); } setup_delete_button() { @@ -216,16 +216,18 @@ export default class WebForm extends frappe.ui.FieldGroup { let message = ''; if (invalid_values.length) { - message += __('Invalid values for fields:') + '

'; + message += __('Invalid values for fields:', null, 'Error message in web form'); + message += '

'; } if (errors.length) { - message += __('Mandatory fields required:') + '

'; + message += __('Mandatory fields required:', null, 'Error message in web form'); + message += '

'; } if (invalid_values.length || errors.length) { frappe.msgprint({ - title: __('Error'), + title: __('Error', null, 'Title of error message in web form'), message: message, indicator: 'orange' }); 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/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 970e9cab88..dcd11a6c76 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -31,12 +31,6 @@ @import 'my_account'; -body { - @include media-breakpoint-up(sm) { - background-color: var(--bg-color); - } -} - .ql-editor.read-mode { padding: 0; line-height: 1.6; diff --git a/frappe/public/scss/website/my_account.scss b/frappe/public/scss/website/my_account.scss index bdc52588aa..22b29cc3ec 100644 --- a/frappe/public/scss/website/my_account.scss +++ b/frappe/public/scss/website/my_account.scss @@ -1,7 +1,8 @@ //styles for my account and edit-profile page @include media-breakpoint-up(sm) { body[data-path="me"], - body[data-path="list"] { + body[data-path="list"], + body[data-path="update-profile"] { background-color: var(--bg-color); } } diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss index cb79f88266..8f55bf8104 100644 --- a/frappe/public/scss/website/web_form.scss +++ b/frappe/public/scss/website/web_form.scss @@ -6,7 +6,7 @@ .breadcrumb-container.container { @include media-breakpoint-up(sm) { - padding-left: var(--padding-sm); + padding-left: 0; } } diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index 4ae33a2fab..d2c54ef18c 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -34,6 +34,58 @@ class TestDBUpdate(unittest.TestCase): self.assertEqual(fieldtype, table_column.type) self.assertIn(cstr(table_column.default) or 'NULL', [cstr(default), "'{}'".format(default)]) + def test_index_and_unique_constraints(self): + doctype = "User" + frappe.reload_doctype('User', force=True) + frappe.model.meta.trim_tables('User') + + make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertTrue(restrict_ip_in_table.unique) + + make_property_setter(doctype, 'restrict_ip', 'unique', '0', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertFalse(restrict_ip_in_table.unique) + + make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertTrue(restrict_ip_in_table.index) + + make_property_setter(doctype, 'restrict_ip', 'search_index', '0', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertFalse(restrict_ip_in_table.index) + + make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') + make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertTrue(restrict_ip_in_table.index) + self.assertTrue(restrict_ip_in_table.unique) + + make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') + make_property_setter(doctype, 'restrict_ip', 'unique', '0', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertTrue(restrict_ip_in_table.index) + self.assertFalse(restrict_ip_in_table.unique) + + make_property_setter(doctype, 'restrict_ip', 'search_index', '0', 'Int') + make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') + frappe.db.updatedb(doctype) + restrict_ip_in_table = get_table_column("User", "restrict_ip") + self.assertFalse(restrict_ip_in_table.index) + self.assertTrue(restrict_ip_in_table.unique) + + # explicitly make a text index + frappe.db.add_index(doctype, ["email_signature(200)"]) + frappe.db.updatedb(doctype) + email_sig_column = get_table_column("User", "email_signature") + self.assertEqual(email_sig_column.index, 1) + def get_fieldtype_from_def(field_def): fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0)) fieldtype = fieldtuple[0] @@ -69,4 +121,8 @@ def get_other_fields_meta(meta): fields = dict(default_fields_map, **optional_fields_map) field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()] - return field_map \ No newline at end of file + return field_map + +def get_table_column(doctype, fieldname): + table_columns = frappe.db.get_table_columns_description('tab{}'.format(doctype)) + return find(table_columns, lambda d: d.get('name') == fieldname) diff --git a/frappe/tests/test_patches.py b/frappe/tests/test_patches.py index 7f4efc700c..8e6d97bd0d 100644 --- a/frappe/tests/test_patches.py +++ b/frappe/tests/test_patches.py @@ -15,3 +15,18 @@ class TestPatches(unittest.TestCase): self.assertTrue(frappe.get_attr(patchmodule.split()[0] + ".execute")) frappe.flags.in_install = False + + def test_get_patch_list(self): + pre = patch_handler.get_patches_from_app("frappe", patch_handler.PatchType.pre_model_sync) + post = patch_handler.get_patches_from_app("frappe", patch_handler.PatchType.post_model_sync) + all_patches = patch_handler.get_patches_from_app("frappe") + self.assertGreater(len(pre), 0) + self.assertGreater(len(post), 0) + + self.assertEqual(len(all_patches), len(pre) + len(post)) + + def test_all_patches_are_marked_completed(self): + all_patches = patch_handler.get_patches_from_app("frappe") + finished_patches = frappe.db.count("Patch Log") + + self.assertGreaterEqual(finished_patches, len(all_patches)) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index a1a2bf9b9c..fadc61a551 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -222,9 +222,10 @@ def disable_2fa(): def toggle_2fa_all_role(state=None): '''Enable or disable 2fa for 'all' role on the system.''' all_role = frappe.get_doc('Role','All') - if state is None: - state = False if all_role.two_factor_auth == True else False - if state not in [True, False]: return + state = state if state is not None else False + if type(state) != bool: + return + all_role.two_factor_auth = cint(state) all_role.save(ignore_permissions=True) frappe.db.commit() diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 79868b0b76..b299df522c 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -248,10 +248,11 @@ def create_topic_and_reply(web_page): @frappe.whitelist() def update_webform_to_multistep(): - doc = frappe.get_doc("Web Form", "edit-profile") - _doc = frappe.copy_doc(doc) - _doc.is_multi_step_form = 1 - _doc.title = "update-profile-duplicate" - _doc.route = "update-profile-duplicate" - _doc.is_standard = False - _doc.save() + if not frappe.db.exists("Web Form", "update-profile-duplicate"): + doc = frappe.get_doc("Web Form", "edit-profile") + _doc = frappe.copy_doc(doc) + _doc.is_multi_step_form = 1 + _doc.title = "update-profile-duplicate" + _doc.route = "update-profile-duplicate" + _doc.is_standard = False + _doc.save() diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 1dc542f55d..f17ceb63cb 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -1393,6 +1393,7 @@ Is Spam,ist Spam, Is Standard,Ist Standard, Is Submittable,Ist übertragbar, Is Table,ist eine Tabelle, +Is Template, Ist Vorlage, Is Your Company Address,Ist Ihre Unternehmensadresse, It is risky to delete this file: {0}. Please contact your System Manager.,"Es ist riskant, diese Datei zu löschen: {0}. Bitte kontaktieren Sie Ihren System-Manager.", Item cannot be added to its own descendents,Artikel kann nicht zu seinen eigenen Abkömmlingen hinzugefügt werden, diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 351c0413ce..9deec0a77c 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -901,10 +901,11 @@ def dictify(arg): def add_user_info(user, user_info): if user not in user_info: info = frappe.db.get_value("User", - user, ["full_name", "user_image", "name", 'email'], as_dict=True) or frappe._dict() + user, ["full_name", "user_image", "name", 'email', 'time_zone'], as_dict=True) or frappe._dict() user_info[user] = frappe._dict( fullname = info.full_name or user, image = info.user_image, name = user, - email = info.email + email = info.email, + time_zone = info.time_zone ) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index b2592e9e8f..2c8b7f5fd3 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -20,11 +20,17 @@ from frappe.utils.redis_queue import RedisQueue from frappe.utils.commands import log +common_site_config = frappe.get_file_json("common_site_config.json") +custom_workers_config = common_site_config.get("workers", {}) default_timeout = 300 queue_timeout = { - 'long': 1500, - 'default': 300, - 'short': 300 + "default": default_timeout, + "short": default_timeout, + "long": 1500, + **{ + worker: config.get("timeout", default_timeout) + for worker, config in custom_workers_config.items() + } } redis_connection = None diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 6c405ce467..6fa2aee0d4 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -1,6 +1,11 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import frappe, os, re, git + +import git +import os +import re + +import frappe from frappe.utils import touch_file, cstr def make_boilerplate(dest, app_name, no_git=False): diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index dac9ab7a6d..9ca5bbfd4f 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -22,7 +22,7 @@ class RedisWrapper(redis.Redis): if shared: return key if user: - if user == True: + if user is True: user = frappe.session.user key = "user:{0}:{1}".format(user, key) diff --git a/frappe/website/doctype/web_form_field/web_form_field.json b/frappe/website/doctype/web_form_field/web_form_field.json index 2770f03e80..94a8851739 100644 --- a/frappe/website/doctype/web_form_field/web_form_field.json +++ b/frappe/website/doctype/web_form_field/web_form_field.json @@ -39,7 +39,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break" + "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nTime\nSection Break\nColumn Break" }, { "fieldname": "label", @@ -146,7 +146,7 @@ ], "istable": 1, "links": [], - "modified": "2021-04-30 12:02:25.422345", + "modified": "2022-01-24 20:43:25.422345", "modified_by": "Administrator", "module": "Website", "name": "Web Form Field", 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", diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 569ebe27d6..bea1300764 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -2,9 +2,8 @@ # License: MIT. See LICENSE import frappe, os, copy, json, re -from frappe import _ +from frappe import _, get_module_path -from frappe.modules import get_doc_path from frappe.core.doctype.access_log.access_log import make_access_log from frappe.utils import cint, sanitize_html, strip_html from frappe.utils.jinja_globals import is_rtl @@ -251,8 +250,9 @@ def get_print_format(doctype, print_format): frappe.DoesNotExistError) # server, find template - path = os.path.join(get_doc_path(frappe.db.get_value("DocType", doctype, "module"), - "Print Format", print_format.name), frappe.scrub(print_format.name) + ".html") + module = print_format.module or frappe.db.get_value("DocType", doctype, "module") + path = os.path.join(get_module_path(module, "Print Format", print_format.name), + frappe.scrub(print_format.name) + ".html") if os.path.exists(path): with open(path, "r") as pffile: diff --git a/yarn.lock b/yarn.lock index a2fb689c7d..18a8610a2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2774,15 +2774,10 @@ nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== -nanoid@^3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" - integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== - -nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== +nanoid@^3.1.22, nanoid@^3.1.23: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== native-request@^1.0.5: version "1.0.8"