Ver código fonte

Merge branch 'develop' into remove-email-salutation

version-14
Suraj Shetty 3 anos atrás
committed by GitHub
pai
commit
615c1d00f6
Nenhuma chave conhecida encontrada para esta assinatura no banco de dados ID da chave GPG: 4AEE18F83AFDEB23
94 arquivos alterados com 2993 adições e 1764 exclusões
  1. +1
    -1
      .github/workflows/ui-tests.yml
  2. +0
    -2
      cypress/integration/control_barcode.js
  3. +6
    -6
      cypress/integration/control_icon.js
  4. +1
    -0
      cypress/integration/datetime.js
  5. +3
    -3
      cypress/integration/multi_select_dialog.js
  6. +2
    -2
      cypress/integration/report_view.js
  7. +3
    -1
      cypress/integration/timeline_email.js
  8. +25
    -27
      cypress/integration/workspace.js
  9. +1
    -1
      frappe/__init__.py
  10. +2
    -1
      frappe/auth.py
  11. +3
    -3
      frappe/automation/workspace/tools/tools.json
  12. +2
    -2
      frappe/boot.py
  13. +1
    -1
      frappe/cache_manager.py
  14. +2
    -2
      frappe/commands/site.py
  15. +45
    -144
      frappe/core/doctype/payment_gateway/payment_gateway.json
  16. +1
    -1
      frappe/core/doctype/user_type/user_type.py
  17. +3
    -3
      frappe/core/workspace/build/build.json
  18. +3
    -3
      frappe/core/workspace/settings/settings.json
  19. +3
    -3
      frappe/core/workspace/users/users.json
  20. +17
    -11
      frappe/custom/doctype/customize_form/customize_form.py
  21. +22
    -0
      frappe/custom/doctype/customize_form/test_customize_form.py
  22. +3
    -3
      frappe/custom/workspace/customization/customization.json
  23. +9
    -2
      frappe/database/mariadb/database.py
  24. +25
    -9
      frappe/database/mariadb/schema.py
  25. +9
    -7
      frappe/database/postgres/database.py
  26. +58
    -26
      frappe/database/postgres/schema.py
  27. +1
    -1
      frappe/database/query.py
  28. +5
    -4
      frappe/database/schema.py
  29. +19
    -45
      frappe/desk/desktop.py
  30. +3
    -2
      frappe/desk/doctype/workspace/workspace.json
  31. +151
    -49
      frappe/desk/doctype/workspace/workspace.py
  32. +10
    -9
      frappe/installer.py
  33. +3
    -3
      frappe/integrations/workspace/integrations/integrations.json
  34. +5
    -8
      frappe/migrate.py
  35. +3
    -0
      frappe/model/rename_doc.py
  36. +99
    -30
      frappe/modules/patch_handler.py
  37. +8
    -6
      frappe/patches.txt
  38. +0
    -3
      frappe/patches/v14_0/copy_mail_data.py
  39. +0
    -1
      frappe/patches/v14_0/update_color_names_in_kanban_board_column.py
  40. +4
    -4
      frappe/patches/v14_0/update_workspace2.py
  41. +1
    -1
      frappe/permissions.py
  42. +629
    -400
      frappe/public/icons/timeless/symbol-defs.svg
  43. +1
    -0
      frappe/public/js/desk.bundle.js
  44. +3
    -2
      frappe/public/js/frappe/desk.js
  45. +2
    -1
      frappe/public/js/frappe/form/controls/date_range.js
  46. +1
    -1
      frappe/public/js/frappe/form/controls/dynamic_link.js
  47. +4
    -1
      frappe/public/js/frappe/form/form.js
  48. +8
    -9
      frappe/public/js/frappe/form/save.js
  49. +42
    -35
      frappe/public/js/frappe/list/list_view.js
  50. +1
    -1
      frappe/public/js/frappe/model/sync.js
  51. +3
    -3
      frappe/public/js/frappe/router.js
  52. +4
    -2
      frappe/public/js/frappe/ui/dialog.js
  53. +1
    -1
      frappe/public/js/frappe/ui/messages.js
  54. +24
    -22
      frappe/public/js/frappe/ui/sort_selector.js
  55. +22
    -0
      frappe/public/js/frappe/ui/workspace_sidebar_loading_skeleton.html
  56. +1
    -1
      frappe/public/js/frappe/utils/diffview.js
  57. +22
    -3
      frappe/public/js/frappe/utils/utils.js
  58. +9
    -3
      frappe/public/js/frappe/views/reports/report_view.js
  59. +247
    -29
      frappe/public/js/frappe/views/workspace/blocks/block.js
  60. +7
    -4
      frappe/public/js/frappe/views/workspace/blocks/card.js
  61. +9
    -5
      frappe/public/js/frappe/views/workspace/blocks/chart.js
  62. +24
    -221
      frappe/public/js/frappe/views/workspace/blocks/header.js
  63. +117
    -0
      frappe/public/js/frappe/views/workspace/blocks/header_size.js
  64. +5
    -5
      frappe/public/js/frappe/views/workspace/blocks/index.js
  65. +18
    -9
      frappe/public/js/frappe/views/workspace/blocks/onboarding.js
  66. +61
    -44
      frappe/public/js/frappe/views/workspace/blocks/paragraph.js
  67. +35
    -6
      frappe/public/js/frappe/views/workspace/blocks/shortcut.js
  68. +7
    -32
      frappe/public/js/frappe/views/workspace/blocks/spacer.js
  69. +0
    -123
      frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js
  70. +637
    -191
      frappe/public/js/frappe/views/workspace/workspace.js
  71. +8
    -6
      frappe/public/js/frappe/web_form/web_form.js
  72. +4
    -34
      frappe/public/js/frappe/widgets/base_widget.js
  73. +1
    -1
      frappe/public/js/frappe/widgets/chart_widget.js
  74. +3
    -1
      frappe/public/js/frappe/widgets/links_widget.js
  75. +8
    -10
      frappe/public/js/frappe/widgets/widget_dialog.js
  76. +1
    -1
      frappe/public/scss/common/buttons.scss
  77. +2
    -0
      frappe/public/scss/desk/dark.scss
  78. +340
    -83
      frappe/public/scss/desk/desktop.scss
  79. +0
    -6
      frappe/public/scss/website/index.scss
  80. +2
    -1
      frappe/public/scss/website/my_account.scss
  81. +1
    -1
      frappe/public/scss/website/web_form.scss
  82. +57
    -1
      frappe/tests/test_db_update.py
  83. +15
    -0
      frappe/tests/test_patches.py
  84. +4
    -3
      frappe/tests/test_twofactor.py
  85. +8
    -7
      frappe/tests/ui_test_helpers.py
  86. +1
    -0
      frappe/translations/de.csv
  87. +3
    -2
      frappe/utils/__init__.py
  88. +9
    -3
      frappe/utils/background_jobs.py
  89. +6
    -1
      frappe/utils/boilerplate.py
  90. +1
    -1
      frappe/utils/redis_wrapper.py
  91. +2
    -2
      frappe/website/doctype/web_form_field/web_form_field.json
  92. +3
    -3
      frappe/website/workspace/website/website.json
  93. +4
    -4
      frappe/www/printview.py
  94. +4
    -9
      yarn.lock

+ 1
- 1
.github/workflows/ui-tests.yml Ver arquivo

@@ -137,7 +137,7 @@ jobs:


- name: UI Tests - name: UI Tests
if: ${{ steps.check-build.outputs.build == 'strawberry' }} 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: env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb




+ 0
- 2
cypress/integration/control_barcode.js Ver arquivo

@@ -21,7 +21,6 @@ context('Control Barcode', () => {
get_dialog_with_barcode().as('dialog'); get_dialog_with_barcode().as('dialog');


cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.focus()
.type('123456789') .type('123456789')
.blur(); .blur();
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') 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'); get_dialog_with_barcode().as('dialog');


cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.focus()
.type('123456789') .type('123456789')
.blur(); .blur();
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')


+ 6
- 6
cypress/integration/control_icon.js Ver arquivo

@@ -19,18 +19,18 @@ context('Control Icon', () => {
get_dialog_with_icon().as('dialog'); get_dialog_with_icon().as('dialog');
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click(); 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 => { cy.get('@dialog').then(dialog => {
let value = dialog.get_value('icon'); 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 => { cy.get('@dialog').then(dialog => {
let value = dialog.get_value('icon'); let value = dialog.get_value('icon');
expect(value).to.equal('resting');
expect(value).to.equal('heart');
}); });
}); });




+ 1
- 0
cypress/integration/datetime.js Ver arquivo

@@ -103,6 +103,7 @@ context('Control Date, Time and DateTime', () => {
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata) input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
} }
]; ];

datetime_formats.forEach(d => { datetime_formats.forEach(d => {
it(`test datetime format ${d.date_format} ${d.time_format}`, () => { it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
cy.set_value('System Settings', 'System Settings', { cy.set_value('System Settings', 'System Settings', {


+ 3
- 3
cypress/integration/multi_select_dialog.js Ver arquivo

@@ -77,11 +77,11 @@ context('MultiSelectDialog', () => {


it('tests more button', () => { it('tests more button', () => {
cy.get_open_dialog() cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="more_btn"]`)
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
.should('exist') .should('exist')
.as('more-btn'); .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); expect($rows).to.have.length(20);
}); });


@@ -89,7 +89,7 @@ context('MultiSelectDialog', () => {
cy.get('@more-btn').find('button').click({force: true}); cy.get('@more-btn').find('button').click({force: true});
cy.wait('@get-more-records'); 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) { if ($rows.length <= 20) {
throw new Error("More button doesn't work"); throw new Error("More button doesn't work");
} }


+ 2
- 2
cypress/integration/report_view.js Ver arquivo

@@ -7,8 +7,6 @@ context('Report View', () => {
cy.visit('/app/website'); cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true); cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache(); cy.clear_cache();
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.insert_doc(doctype_name, { cy.insert_doc(doctype_name, {
'title': 'Doc 1', 'title': 'Doc 1',
'description': 'Random Text', 'description': 'Random Text',
@@ -16,6 +14,8 @@ context('Report View', () => {
// submit document // submit document
'docstatus': 1 'docstatus': 1
}, true).as('doc'); }, 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.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`); cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus // check status column added from docstatus


+ 3
- 1
cypress/integration/timeline_email.js Ver arquivo

@@ -14,7 +14,7 @@ context('Timeline Email', () => {
cy.wait(700); 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.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject').eq(0).click(); 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 > .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('#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(); 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.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();




+ 25
- 27
cypress/integration/workspace.js Ver arquivo

@@ -33,44 +33,39 @@ context('Workspace 2.0', () => {
}); });


it('Add New Block', () => { 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(":focus").type('Header');
cy.get(".ce-block:last").find('.ce-header').should('exist'); 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(":focus").type('Paragraph text');
cy.get(".ce-block:last").find('.ce-paragraph').should('exist'); cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
}); });


it('Delete A Block', () => { 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'); cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
}); });


it('Shrink and Expand A Block', () => { 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(); 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('.codex-editor__redactor .ce-block');
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); 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.wait(300);
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click(); cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();


+ 1
- 1
frappe/__init__.py Ver arquivo

@@ -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) 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): def emit_js(js, user=False, **kwargs):
if user == False:
if user is False:
user = session.user user = session.user
publish_realtime('eval_js', js, user=user, **kwargs) publish_realtime('eval_js', js, user=user, **kwargs)




+ 2
- 1
frappe/auth.py Ver arquivo

@@ -111,7 +111,8 @@ class LoginManager:
self.user_type = None self.user_type = None


if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": 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 self.resume = False


# run login triggers # run login triggers


+ 3
- 3
frappe/automation/workspace/tools/tools.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 14:53:24.980279",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -208,7 +208,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-05 12:16:02.839181",
"modified": "2022-01-13 17:48:48.456763",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Automation", "module": "Automation",
"name": "Tools", "name": "Tools",
@@ -217,7 +217,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 26,
"sequence_id": 26.0,
"shortcuts": [ "shortcuts": [
{ {
"label": "ToDo", "label": "ToDo",


+ 2
- 2
frappe/boot.py Ver arquivo

@@ -107,8 +107,8 @@ def load_conf_settings(bootinfo):
if key in conf: bootinfo[key] = conf.get(key) if key in conf: bootinfo[key] = conf.get(key)


def load_desktop_data(bootinfo): 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.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.dashboards = frappe.get_all("Dashboard")




+ 1
- 1
frappe/cache_manager.py Ver arquivo

@@ -148,7 +148,7 @@ def build_table_count_cache():
data = ( data = (
frappe.qb.from_(information_schema.tables).select(table_name, table_rows) frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
).run(as_dict=True) ).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) _cache.set_value("information_schema:counts", counts)


return counts return counts


+ 2
- 2
frappe/commands/site.py Ver arquivo

@@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup):
doctype_tables = frappe.get_all("DocType", pluck="name") doctype_tables = frappe.get_all("DocType", pluck="name")


for x in database_tables: 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): if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
TABLES_TO_DROP.append(x) TABLES_TO_DROP.append(x)


@@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup):


odb = scheduled_backup( odb = scheduled_backup(
ignore_conf=False, 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, ignore_files=True,
force=True, force=True,
) )


+ 45
- 144
frappe/core/doctype/payment_gateway/payment_gateway.json Ver arquivo

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

+ 1
- 1
frappe/core/doctype/user_type/user_type.py Ver arquivo

@@ -121,7 +121,7 @@ class UserType(Document):


for child_table in doc.get_table_fields(): for child_table in doc.get_table_fields():
child_doc = frappe.get_meta(child_table.options) 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) self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)


if select_doctypes: if select_doctypes:


+ 3
- 3
frappe/core/workspace/build/build.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Elements</b></span>\",\"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", "creation": "2021-01-02 10:51:16.579957",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -222,7 +222,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-09-05 21:14:52.384816",
"modified": "2022-01-13 17:26:02.736366",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Build", "name": "Build",
@@ -231,7 +231,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 5,
"sequence_id": 5.0,
"shortcuts": [ "shortcuts": [
{ {
"doc_view": "", "doc_view": "",


+ 3
- 3
frappe/core/workspace/settings/settings.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 15:09:40.527211",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -367,7 +367,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-05 12:16:03.456174",
"modified": "2022-01-13 17:49:59.586909",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Settings", "name": "Settings",
@@ -376,7 +376,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 29,
"sequence_id": 29.0,
"shortcuts": [ "shortcuts": [
{ {
"icon": "setting", "icon": "setting",


+ 3
- 3
frappe/core/workspace/users/users.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 15:12:16.754449",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -145,7 +145,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-05 12:16:03.010205",
"modified": "2022-01-13 17:49:08.912772",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Users", "name": "Users",
@@ -154,7 +154,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 27,
"sequence_id": 27.0,
"shortcuts": [ "shortcuts": [
{ {
"label": "User", "label": "User",


+ 17
- 11
frappe/custom/doctype/customize_form/customize_form.py Ver arquivo

@@ -107,20 +107,26 @@ class CustomizeForm(Document):
def set_name_translation(self): def set_name_translation(self):
'''Create, update custom translation for this doctype''' '''Create, update custom translation for this doctype'''
current = self.get_name_translation() 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 # clear translation
frappe.delete_doc('Translation', current.name) 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): def clear_existing_doc(self):
doc_type = self.doc_type doc_type = self.doc_type


+ 22
- 0
frappe/custom/doctype/customize_form/test_customize_form.py Ver arquivo

@@ -304,3 +304,25 @@ class TestCustomizeForm(unittest.TestCase):


action = [d for d in event.actions if d.label=='Test Action'] action = [d for d in event.actions if d.label=='Test Action']
self.assertEqual(len(action), 0) 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, "")

+ 3
- 3
frappe/custom/workspace/customization/customization.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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 &amp; 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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 15:15:03.839594",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -123,7 +123,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-11-24 16:20:03.500885",
"modified": "2022-01-13 17:28:08.345794",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customization", "name": "Customization",
@@ -132,7 +132,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 8,
"sequence_id": 8.0,
"shortcuts": [ "shortcuts": [
{ {
"label": "Customize Form", "label": "Customize Form",


+ 9
- 2
frappe/database/mariadb/database.py Ver arquivo

@@ -245,9 +245,16 @@ class MariaDBDatabase(Database):
column_name as 'name', column_name as 'name',
column_type as 'type', column_type as 'type',
column_default as 'default', 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' 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) where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1)


def has_index(self, table_name, index_name): def has_index(self, table_name, index_name):


+ 25
- 9
frappe/database/mariadb/schema.py Ver arquivo

@@ -58,18 +58,34 @@ class MariaDBTable(DBTable):
modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition())) modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition()))


for col in self.add_index: 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 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 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: try:
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:


+ 9
- 7
frappe/database/postgres/database.py Ver arquivo

@@ -77,11 +77,11 @@ class PostgresDatabase(Database):
"""Escape quotes and percent in given string.""" """Escape quotes and percent in given string."""
if isinstance(s, bytes): if isinstance(s, bytes):
s = s.decode('utf-8') s = s.decode('utf-8')
# MariaDB's driver treats None as an empty string # MariaDB's driver treats None as an empty string
# So Postgres should do the same # So Postgres should do the same


if s is None:
if s is None:
s = '' s = ''


if percent: if percent:
@@ -308,18 +308,20 @@ class PostgresDatabase(Database):
WHEN 'timestamp without time zone' THEN 'timestamp' WHEN 'timestamp without time zone' THEN 'timestamp'
ELSE a.data_type ELSE a.data_type
END AS 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, SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
BOOL_OR(b.unique) AS unique BOOL_OR(b.unique) AS unique
FROM information_schema.columns a FROM information_schema.columns a
LEFT JOIN 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 FROM pg_indexes
WHERE tablename='{table_name}') b 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}' 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): def get_database_list(self, target):
return [d[0] for d in self.sql("SELECT datname FROM pg_database;")] return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]


+ 58
- 26
frappe/database/postgres/schema.py Ver arquivo

@@ -11,8 +11,6 @@ class PostgresTable(DBTable):
column_defs = self.get_column_definitions() column_defs = self.get_column_definitions()
if column_defs: add_text += ',\n'.join(column_defs) if column_defs: add_text += ',\n'.join(column_defs)


# index
# index_defs = self.get_index_definitions()
# TODO: set docstatus length # TODO: set docstatus length
# create table # create table
frappe.db.sql("""create table `%s` ( frappe.db.sql("""create table `%s` (
@@ -28,8 +26,25 @@ class PostgresTable(DBTable):
idx bigint not null default '0', idx bigint not null default '0',
%s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text)) %s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text))


self.create_indexes()
frappe.db.commit() 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): def alter(self):
for col in self.columns.values(): for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) 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( query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format(
col.fieldname, col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length), get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause)
)
using_clause
))


for col in self.set_default: for col in self.set_default:
if col.fieldname=="name": if col.fieldname=="name":
@@ -73,37 +88,54 @@ class PostgresTable(DBTable):


query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default)) query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default))


create_index_query = ""
create_contraint_query = ""
for col in self.add_index: for col in self.add_index:
# if index key not exists # 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, index_name=col.fieldname,
table_name=self.table_name, table_name=self.table_name,
field=col.fieldname) 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: for col in self.drop_index:
# primary key # primary key
if col.fieldname != 'name': if col.fieldname != 'name':
# if index key exists # 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)) 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

+ 1
- 1
frappe/database/query.py Ver arquivo

@@ -308,7 +308,7 @@ class Permission:
doctype = [doctype] doctype = [doctype]


for dt in doctype: for dt in doctype:
dt = re.sub("tab", "", dt)
dt = re.sub("^tab", "", dt)
if not frappe.has_permission( if not frappe.has_permission(
dt, dt,
"select", "select",


+ 5
- 4
frappe/database/schema.py Ver arquivo

@@ -21,6 +21,7 @@ class DBTable:
self.change_name = [] self.change_name = []
self.add_unique = [] self.add_unique = []
self.add_index = [] self.add_index = []
self.drop_unique = []
self.drop_index = [] self.drop_index = []
self.set_default = [] self.set_default = []


@@ -219,8 +220,10 @@ class DbColumn:
self.table.change_type.append(self) self.table.change_type.append(self)


# unique # 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) 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 # default
if (self.default_changed(current_def) if (self.default_changed(current_def)
@@ -230,9 +233,7 @@ class DbColumn:
self.table.set_default.append(self) self.table.set_default.append(self)


# index should be applied or dropped irrespective of type change # 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) self.table.drop_index.append(self)


elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')): elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):


+ 19
- 45
frappe/desk/desktop.py Ver arquivo

@@ -56,31 +56,6 @@ class Workspace:
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() 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() 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): def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed.""" """Returns true if Has Role is not set or the user is allowed."""
from frappe.utils import has_common 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 dict: dictionary of cards, charts and shortcuts to be displayed on website
""" """
try: try:
wspace = Workspace(loads(page))
wspace.build_workspace()
workspace = Workspace(loads(page))
workspace.build_workspace()
return { 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: except DoesNotExistError:
frappe.log_error(frappe.get_traceback()) frappe.log_error(frappe.get_traceback())
return {} return {}


@frappe.whitelist() @frappe.whitelist()
def get_wspace_sidebar_items():
def get_workspace_sidebar_items():
"""Get list of sidebar items for desk""" """Get list of sidebar items for desk"""
has_access = "Workspace Manager" in frappe.get_roles() has_access = "Workspace Manager" in frappe.get_roles()


@@ -385,8 +360,8 @@ def get_wspace_sidebar_items():
# Filter Page based on Permission # Filter Page based on Permission
for page in all_pages: for page in all_pages:
try: 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: if page.public:
pages.append(page) pages.append(page)
elif page.for_user == frappe.session.user: elif page.for_user == frappe.session.user:
@@ -453,25 +428,24 @@ def get_custom_report_list(module):
return out return out


def save_new_widget(doc, page, blocks, new_widgets): 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 # remove duplicate and unwanted widgets
if widgets:
clean_up(doc, blocks)
clean_up(doc, blocks)


try: try:
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
except (ValidationError, TypeError) as e: except (ValidationError, TypeError) as e:
# Create a json string to log # 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 # Error log body
log = \ log = \


+ 3
- 2
frappe/desk/doctype/workspace/workspace.json Ver arquivo

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1,
"autoname": "field:label", "autoname": "field:label",
"beta": 1, "beta": 1,
"creation": "2020-01-23 13:45:59.470592", "creation": "2020-01-23 13:45:59.470592",
@@ -141,7 +142,7 @@
}, },
{ {
"fieldname": "sequence_id", "fieldname": "sequence_id",
"fieldtype": "Int",
"fieldtype": "Float",
"label": "Sequence Id" "label": "Sequence Id"
}, },
{ {
@@ -158,7 +159,7 @@
], ],
"in_create": 1, "in_create": 1,
"links": [], "links": [],
"modified": "2021-09-16 12:01:06.450622",
"modified": "2021-12-15 19:33:00.805265",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "Workspace", "name": "Workspace",


+ 151
- 49
frappe/desk/doctype/workspace/workspace.py Ver arquivo

@@ -6,6 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.modules.export_file import export_to_files from frappe.modules.export_file import export_to_files
from frappe.model.document import Document 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.desktop import save_new_widget
from frappe.desk.utils import validate_route_conflict 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") report_type = frappe.get_value("Report", report, "report_type")
return report_type in ["Query Report", "Script Report", "Custom Report"] 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() @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) 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.title = title
doc.icon = icon doc.icon = icon
doc.content = blocks
doc.parent_page = parent 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) 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): 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: 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(): 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 seq, d in enumerate(pages):
for page in wspace_pages:
for page in workspace_pages:
if page.title == d.get('title'): if page.title == d.get('title'):
doc = frappe.get_doc('Workspace', page.name) doc = frappe.get_doc('Workspace', page.name)
doc.sequence_id = seq + 1 doc.sequence_id = seq + 1
@@ -199,6 +280,27 @@ def sort_page(wspace_pages, pages):
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
break 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): def get_page_list(fields, filters):
return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc') return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc')




+ 10
- 9
frappe/installer.py Ver arquivo

@@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True):


for before_install in app_hooks.before_install or []: for before_install in app_hooks.before_install or []:
out = frappe.get_attr(before_install)() out = frappe.get_attr(before_install)()
if out==False:
if out is False:
return return


if name != "frappe": if name != "frappe":
@@ -346,14 +346,15 @@ def post_install(rebuild_website=False):




def set_all_patches_as_completed(app): 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(): def init_singles():


+ 3
- 3
frappe/integrations/workspace/integrations/integrations.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 15:16:18.714190",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -260,7 +260,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-05 12:16:00.355268",
"modified": "2022-01-13 17:39:01.292154",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integrations", "module": "Integrations",
"name": "Integrations", "name": "Integrations",
@@ -269,7 +269,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 15,
"sequence_id": 15.0,
"shortcuts": [], "shortcuts": [],
"title": "Integrations" "title": "Integrations"
} }

+ 5
- 8
frappe/migrate.py Ver arquivo

@@ -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.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes from frappe.search.website_search import build_index_for_all_routes
from frappe.database.schema import add_column 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): 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() clear_global_cache()


#run before_migrate hooks
for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('before_migrate', app_name=app): for fn in frappe.get_hooks('before_migrate', app_name=app):
frappe.get_attr(fn)() 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.model.sync.sync_all()
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
frappe.translate.clear_cache() frappe.translate.clear_cache()
sync_jobs() sync_jobs()
sync_fixtures() 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() frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()


# syncs statics
# syncs static files
clear_website_cache() clear_website_cache()


# updating installed applications data # updating installed applications data
frappe.get_single('Installed Applications').update_versions() frappe.get_single('Installed Applications').update_versions()


#run after_migrate hooks
for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app): for fn in frappe.get_hooks('after_migrate', app_name=app):
frappe.get_attr(fn)() frappe.get_attr(fn)()


# build web_routes index
if not skip_search_index: if not skip_search_index:
# Run this last as it updates the current session # Run this last as it updates the current session
print('Building search index for {}'.format(frappe.local.site)) print('Building search index for {}'.format(frappe.local.site))


+ 3
- 0
frappe/model/rename_doc.py Ver arquivo

@@ -80,6 +80,7 @@ def rename_doc(


if doctype=='DocType': if doctype=='DocType':
rename_doctype(doctype, old, new, force) rename_doctype(doctype, old, new, force)
update_customizations(old, new)


update_attachments(doctype, old, new) update_attachments(doctype, old, new)


@@ -174,6 +175,8 @@ def update_user_settings(old, new, link_fields):
else: else:
continue 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): def update_attachments(doctype, old, new):
try: try:


+ 99
- 30
frappe/modules/patch_handler.py Ver arquivo

@@ -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 # 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""" """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 = [] frappe.flags.final_patches = []


def run_patch(patch): def run_patch(patch):
try: try:
if not run_single(patchmodule = patch): if not run_single(patchmodule = patch):
log(patch + ': failed: STOPPED')
print(patch + ': failed: STOPPED')
raise PatchError(patch) raise PatchError(patch)
except Exception: except Exception:
if not skip_failing: if not skip_failing:
raise raise
else: 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): if patch and (patch not in executed):
run_patch(patch) run_patch(patch)


@@ -40,18 +79,54 @@ def run_all(skip_failing=False):
patch = patch.replace('finally:', '') patch = patch.replace('finally:', '')
run_patch(patch) 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 = [] patches = []
for app in frappe.get_installed_apps(): 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 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): def reload_doc(args):
import frappe.modules import frappe.modules
run_single(method = frappe.modules.reload_doc, methodargs = args) run_single(method = frappe.modules.reload_doc, methodargs = args)
@@ -73,7 +148,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
frappe.db.begin() frappe.db.begin()
start_time = time.time() start_time = time.time()
try: 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)) site=frappe.local.site, db=frappe.db.cur_db_name))
if patchmodule: if patchmodule:
if patchmodule.startswith("finally:"): if patchmodule.startswith("finally:"):
@@ -96,7 +171,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
frappe.db.commit() frappe.db.commit()
end_time = time.time() end_time = time.time()
block_user(False) 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 return True


@@ -109,10 +184,7 @@ def executed(patchmodule):
if patchmodule.startswith('finally:'): if patchmodule.startswith('finally:'):
# patches are saved without the finally: tag # patches are saved without the finally: tag
patchmodule = patchmodule.replace('finally:', '') 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): def block_user(block, msg=None):
"""stop/start execution till patch is run""" """stop/start execution till patch is run"""
@@ -128,6 +200,3 @@ def check_session_stopped():
if frappe.db.get_global("__session_status")=='stop': if frappe.db.get_global("__session_status")=='stop':
frappe.msgprint(frappe.db.get_global("__session_status_message")) frappe.msgprint(frappe.db.get_global("__session_status_message"))
raise frappe.SessionStopped('Session Stopped') raise frappe.SessionStopped('Session Stopped')

def log(msg):
print (msg)

+ 8
- 6
frappe/patches.txt Ver arquivo

@@ -1,3 +1,4 @@
[pre_model_sync]
frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3
execute:frappe.utils.global_search.setup_global_search_table() execute:frappe.utils.global_search.setup_global_search_table()
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 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.v11_0.set_default_letter_head_source
frappe.patches.v12_0.set_primary_key_in_series frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.delete_doc("Page", "modules", ignore_missing=True) 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.setup_comments_from_communications
frappe.patches.v12_0.replace_null_values_in_tables frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings 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 frappe.patches.v12_0.remove_example_email_thread_notify
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v12_0.set_correct_url_in_files 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') execute:frappe.reload_doc('custom', 'doctype', 'property_setter')
frappe.patches.v13_0.remove_invalid_options_for_data_fields frappe.patches.v13_0.remove_invalid_options_for_data_fields
frappe.patches.v13_0.website_theme_custom_scss 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.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.set_first_day_of_the_week 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.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.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.update_github_endpoints #08-11-2021
frappe.patches.v14_0.remove_db_aggregation 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.update_color_names_in_kanban_board_column
frappe.patches.v14_0.transform_todo_schema

+ 0
- 3
frappe/patches/v14_0/copy_mail_data.py Ver arquivo

@@ -3,9 +3,6 @@ import frappe




def execute(): 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 # 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}): for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}):
# get all data from Email Account # get all data from Email Account


+ 0
- 1
frappe/patches/v14_0/update_color_names_in_kanban_board_column.py Ver arquivo

@@ -5,7 +5,6 @@ from __future__ import unicode_literals
import frappe import frappe


def execute(): def execute():
frappe.reload_doc("desk", "doctype", "kanban_board_column")
indicator_map = { indicator_map = {
'blue': 'Blue', 'blue': 'Blue',
'orange': 'Orange', 'orange': 'Orange',


+ 4
- 4
frappe/patches/v14_0/update_workspace2.py Ver arquivo

@@ -5,10 +5,10 @@ from frappe import _
def execute(): def execute():
frappe.reload_doc('desk', 'doctype', 'workspace', force=True) 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) content = create_content(doc)
update_wspace(doc, seq, content)
update_workspace(doc, seq, content)
frappe.db.commit() frappe.db.commit()


def create_content(doc): def create_content(doc):
@@ -49,7 +49,7 @@ def create_content(doc):
del doc.links[doc.links.index(l)] del doc.links[doc.links.index(l)]
return content 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: if not doc.title and not doc.content and not doc.is_standard and not doc.public:
doc.sequence_id = seq + 1 doc.sequence_id = seq + 1
doc.content = json.dumps(content) doc.content = json.dumps(content)


+ 1
- 1
frappe/permissions.py Ver arquivo

@@ -23,7 +23,7 @@ def print_has_permission_check_logs(func):
frappe.flags['has_permission_check_logs'] = [] frappe.flags['has_permission_check_logs'] = []
result = func(*args, **kwargs) result = func(*args, **kwargs)
self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user 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 # print only if access denied
# and if user is checking his own permission # and if user is checking his own permission


+ 629
- 400
frappe/public/icons/timeless/symbol-defs.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 0
frappe/public/js/desk.bundle.js Ver arquivo

@@ -96,6 +96,7 @@ import "./frappe/ui/sort_selector.js";


import "./frappe/change_log.html"; import "./frappe/change_log.html";
import "./frappe/ui/workspace_loading_skeleton.html"; import "./frappe/ui/workspace_loading_skeleton.html";
import "./frappe/ui/workspace_sidebar_loading_skeleton.html";
import "./frappe/desk.js"; import "./frappe/desk.js";
import "./frappe/query_string.js"; import "./frappe/query_string.js";




+ 3
- 2
frappe/public/js/frappe/desk.js Ver arquivo

@@ -214,19 +214,20 @@ frappe.Application = class Application {


email_password_prompt(email_account,user,i) { email_password_prompt(email_account,user,i) {
var me = this; var me = this;
const email_id = email_account[i]["email_id"];
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: __('Password missing in Email Account'), title: __('Password missing in Email Account'),
fields: [ fields: [
{ {
'fieldname': 'password', 'fieldname': 'password',
'fieldtype': 'Password', 'fieldtype': 'Password',
'label': __('Please enter the password for: <b>{0}</b>', [email_account[i]["email_id"]]),
'label': __('Please enter the password for: <b>{0}</b>', [email_id], "Email Account"),
'reqd': 1 'reqd': 1
}, },
{ {
"fieldname": "submit", "fieldname": "submit",
"fieldtype": "Button", "fieldtype": "Button",
"label": __("Submit")
"label": __("Submit", null, "Submit password for Email Account")
} }
] ]
}); });


+ 2
- 1
frappe/public/js/frappe/form/controls/date_range.js Ver arquivo

@@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form.
language: "en", language: "en",
range: true, range: true,
autoClose: true, autoClose: true,
toggleSelected: false
toggleSelected: false,
firstDay: frappe.datetime.get_first_day_of_the_week_index()
}; };
this.datepicker_options.dateFormat = this.datepicker_options.dateFormat =
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');


+ 1
- 1
frappe/public/js/frappe/form/controls/dynamic_link.js Ver arquivo

@@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
get_options() { get_options() {
let options = ''; let options = '';
if (this.df.get_options) { if (this.df.get_options) {
options = this.df.get_options();
options = this.df.get_options(this);
} else if (this.docname==null && cur_dialog) { } else if (this.docname==null && cur_dialog) {
//for dialog box //for dialog box
options = cur_dialog.get_value(this.df.options); options = cur_dialog.get_value(this.df.options);


+ 4
- 1
frappe/public/js/frappe/form/form.js Ver arquivo

@@ -943,7 +943,10 @@ frappe.ui.form.Form = class FrappeForm {
// re-enable buttons // re-enable buttons
resolve(); 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")
);
} }
} }




+ 8
- 9
frappe/public/js/frappe/form/save.js Ver arquivo

@@ -7,12 +7,12 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$(btn).prop("disabled", true); $(btn).prop("disabled", true);


// specified here because there are keyboard shortcuts to save // 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)]; }[toTitle(action)];


var freeze_message = working_label ? __(working_label) : ""; var freeze_message = working_label ? __(working_label) : "";
@@ -154,8 +154,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
if (error_fields.length) { if (error_fields.length) {
let meta = frappe.get_meta(doc.doctype); let meta = frappe.get_meta(doc.doctype);
if (meta.istable) { 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 { } else {
var message = __('Mandatory fields required in {0}', [__(doc.doctype)]); var message = __('Mandatory fields required in {0}', [__(doc.doctype)]);
} }
@@ -276,4 +276,3 @@ frappe.ui.form.update_calling_link = (newdoc) => {
frappe._from_link = null; frappe._from_link = null;
} }
} }


+ 42
- 35
frappe/public/js/frappe/list/list_view.js Ver arquivo

@@ -200,7 +200,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.render_template("list_view_permission_restrictions", { frappe.render_template("list_view_permission_restrictions", {
condition_list: match_rules_list, 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() { set_primary_action() {
if (this.can_create) { 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( this.page.set_primary_action(
`${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`,
label,
() => { () => {
if (this.settings.primary_action) { if (this.settings.primary_action) {
this.settings.primary_action(); this.settings.primary_action();
@@ -320,9 +325,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


setup_freeze_area() { setup_freeze_area() {
this.$freeze = $( this.$freeze = $(
`<div class="freeze flex justify-center align-center text-muted">${__(
"Loading"
)}...</div>`
`<div class="freeze flex justify-center align-center text-muted">
${__("Loading")}...
</div>`
).hide(); ).hide();
this.$result.append(this.$freeze); this.$result.append(this.$freeze);
} }
@@ -460,8 +465,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
? __("No {0} found", [__(this.doctype)]) ? __("No {0} found", [__(this.doctype)])
: __("You haven't created a {0} yet", [__(this.doctype)]); : __("You haven't created a {0} yet", [__(this.doctype)]);
let new_button_label = filters && filters.length 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 = let empty_state_image =
this.settings.empty_state_image || this.settings.empty_state_image ||
"/assets/frappe/images/ui-states/list-empty-state.svg"; "/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 const new_button = this.can_create
? `<p><button class="btn btn-primary btn-sm btn-new-doc hidden-xs"> ? `<p><button class="btn btn-primary btn-sm btn-new-doc hidden-xs">
${new_button_label} ${new_button_label}
</button> <button class="btn btn-primary btn-new-doc visible-xs">${__('Create New')}</button></p>`
</button> <button class="btn btn-primary btn-new-doc visible-xs">
${__("Create New", null, "Create a new document from list view")}
</button></p>`
: ""; : "";


return `<div class="msg-box no-border"> return `<div class="msg-box no-border">
@@ -486,7 +493,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (this.list_view_settings && !this.list_view_settings.disable_count) { if (this.list_view_settings && !this.list_view_settings.disable_count) {
this.$result this.$result
.find(".list-count") .find(".list-count")
.html(`<span>${__("Refreshing")}...</span>`);
.html(`<span>${__("Refreshing", null, "Document count in list view")}...</span>`);
} }
} }


@@ -1081,14 +1088,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.ui.keys.add_shortcut({ frappe.ui.keys.add_shortcut({
shortcut: "down", shortcut: "down",
action: () => handle_navigation("down"), action: () => handle_navigation("down"),
description: __("Navigate list down"),
description: __("Navigate list down", null, "Description of a list view shortcut"),
page: this.page, page: this.page,
}); });


frappe.ui.keys.add_shortcut({ frappe.ui.keys.add_shortcut({
shortcut: "up", shortcut: "up",
action: () => handle_navigation("up"), action: () => handle_navigation("up"),
description: __("Navigate list up"),
description: __("Navigate list up", null, "Description of a list view shortcut"),
page: this.page, page: this.page,
}); });


@@ -1100,7 +1107,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
check_row($list_row); check_row($list_row);
focus_next(); focus_next();
}, },
description: __("Select multiple list items"),
description: __("Select multiple list items", null, "Description of a list view shortcut"),
page: this.page, page: this.page,
}); });


@@ -1112,7 +1119,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
check_row($list_row); check_row($list_row);
focus_prev(); focus_prev();
}, },
description: __("Select multiple list items"),
description: __("Select multiple list items", null, "Description of a list view shortcut"),
page: this.page, page: this.page,
}); });


@@ -1126,7 +1133,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
} }
return false; return false;
}, },
description: __("Open list item"),
description: __("Open list item", null, "Description of a list view shortcut"),
page: this.page, page: this.page,
}); });


@@ -1140,7 +1147,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
} }
return false; return false;
}, },
description: __("Select list item"),
description: __("Select list item", null, "Description of a list view shortcut"),
page: this.page, 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)) { if (frappe.model.can_import(doctype, null, this.meta)) {
items.push({ items.push({
label: __("Import"),
label: __("Import", null, "Button in list view menu"),
action: () => action: () =>
frappe.set_route("list", "data-import", { frappe.set_route("list", "data-import", {
reference_doctype: doctype, reference_doctype: doctype,
@@ -1526,7 +1533,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


if (frappe.model.can_set_user_permissions(doctype)) { if (frappe.model.can_set_user_permissions(doctype)) {
items.push({ items.push({
label: __("User Permissions"),
label: __("User Permissions", null, "Button in list view menu"),
action: () => action: () =>
frappe.set_route("list", "user-permission", { frappe.set_route("list", "user-permission", {
allow: doctype, allow: doctype,
@@ -1537,7 +1544,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


if (frappe.user_roles.includes("System Manager")) { if (frappe.user_roles.includes("System Manager")) {
items.push({ items.push({
label: __("Role Permissions Manager"),
label: __("Role Permissions Manager", null, "Button in list view menu"),
action: () => action: () =>
frappe.set_route("permission-manager", { frappe.set_route("permission-manager", {
doctype, doctype,
@@ -1546,7 +1553,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}); });


items.push({ items.push({
label: __("Customize"),
label: __("Customize", null, "Button in list view menu"),
action: () => { action: () => {
if (!this.meta) return; if (!this.meta) return;
if (this.meta.custom) { if (this.meta.custom) {
@@ -1563,7 +1570,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
} }


items.push({ items.push({
label: __("Toggle Sidebar"),
label: __("Toggle Sidebar", null, "Button in list view menu"),
action: () => this.toggle_side_bar(), action: () => this.toggle_side_bar(),
condition: () => !this.hide_sidebar, condition: () => !this.hide_sidebar,
standard: true, standard: true,
@@ -1571,7 +1578,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}); });


items.push({ items.push({
label: __("Share URL"),
label: __("Share URL", null, "Button in list view menu"),
action: () => this.share_url(), action: () => this.share_url(),
standard: true, standard: true,
shortcut: "Ctrl+L", shortcut: "Ctrl+L",
@@ -1583,7 +1590,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
) { ) {
// edit doctype // edit doctype
items.push({ items.push({
label: __("Edit DocType"),
label: __("Edit DocType", null, "Button in list view menu"),
action: () => frappe.set_route("form", "doctype", doctype), action: () => frappe.set_route("form", "doctype", doctype),
standard: true, standard: true,
}); });
@@ -1591,7 +1598,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


if (frappe.user.has_role("System Manager")) { if (frappe.user.has_role("System Manager")) {
items.push({ items.push({
label: __("List Settings"),
label: __("List Settings", null, "Button in list view menu"),
action: () => this.show_list_settings(), action: () => this.show_list_settings(),
standard: true, standard: true,
}); });
@@ -1682,7 +1689,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
// utility // utility
const bulk_assignment = () => { const bulk_assignment = () => {
return { return {
label: __("Assign To"),
label: __("Assign To", null, "Button in list view actions menu"),
action: () => { action: () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.assign( bulk_operations.assign(
@@ -1700,7 +1707,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_assignment_rule = () => { const bulk_assignment_rule = () => {
return { return {
label: __("Apply Assignment Rule"),
label: __("Apply Assignment Rule", null, "Button in list view actions menu"),
action: () => { action: () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.apply_assignment_rule( bulk_operations.apply_assignment_rule(
@@ -1718,7 +1725,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_add_tags = () => { const bulk_add_tags = () => {
return { return {
label: __("Add Tags"),
label: __("Add Tags", null, "Button in list view actions menu"),
action: () => { action: () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.add_tags( bulk_operations.add_tags(
@@ -1736,7 +1743,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_printing = () => { const bulk_printing = () => {
return { return {
label: __("Print"),
label: __("Print", null, "Button in list view actions menu"),
action: () => bulk_operations.print(this.get_checked_items()), action: () => bulk_operations.print(this.get_checked_items()),
standard: true, standard: true,
}; };
@@ -1744,13 +1751,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_delete = () => { const bulk_delete = () => {
return { return {
label: __("Delete"),
label: __("Delete", null, "Button in list view actions menu"),
action: () => { action: () => {
const docnames = this.get_checked_items(true).map( const docnames = this.get_checked_items(true).map(
(docname) => docname.toString() (docname) => docname.toString()
); );
frappe.confirm( frappe.confirm(
__("Delete {0} items permanently?", [docnames.length]),
__("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"),
() => { () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.delete(docnames, () => { bulk_operations.delete(docnames, () => {
@@ -1767,12 +1774,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_cancel = () => { const bulk_cancel = () => {
return { return {
label: __("Cancel"),
label: __("Cancel", null, "Button in list view actions menu"),
action: () => { action: () => {
const docnames = this.get_checked_items(true); const docnames = this.get_checked_items(true);
if (docnames.length > 0) { if (docnames.length > 0) {
frappe.confirm( frappe.confirm(
__("Cancel {0} documents?", [docnames.length]),
__("Cancel {0} documents?", [docnames.length], "Title of confirmation dialog"),
() => { () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.submit_or_cancel( bulk_operations.submit_or_cancel(
@@ -1793,12 +1800,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_submit = () => { const bulk_submit = () => {
return { return {
label: __("Submit"),
label: __("Submit", null, "Button in list view actions menu"),
action: () => { action: () => {
const docnames = this.get_checked_items(true); const docnames = this.get_checked_items(true);
if (docnames.length > 0) { if (docnames.length > 0) {
frappe.confirm( frappe.confirm(
__("Submit {0} documents?", [docnames.length]),
__("Submit {0} documents?", [docnames.length], "Title of confirmation dialog"),
() => { () => {
this.disable_list_update = true; this.disable_list_update = true;
bulk_operations.submit_or_cancel( bulk_operations.submit_or_cancel(
@@ -1820,7 +1827,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_edit = () => { const bulk_edit = () => {
return { return {
label: __("Edit"),
label: __("Edit", null, "Button in list view actions menu"),
action: () => { action: () => {
let field_mappings = {}; let field_mappings = {};


@@ -1850,7 +1857,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const bulk_export = () => { const bulk_export = () => {
return { return {
label: __("Export"),
label: __("Export", null, "Button in list view actions menu"),
action: () => { action: () => {
const docnames = this.get_checked_items(true); const docnames = this.get_checked_items(true);




+ 1
- 1
frappe/public/js/frappe/model/sync.js Ver arquivo

@@ -39,7 +39,7 @@ Object.assign(frappe.model, {
} }


frappe.model.sync_docinfo(r); frappe.model.sync_docinfo(r);
return r.docs;
}, },


rename_after_save: (d, i) => { rename_after_save: (d, i) => {


+ 3
- 3
frappe/public/js/frappe/router.js Ver arquivo

@@ -133,14 +133,14 @@ frappe.router = {
// /app/user/user-001 = ["Form", "User", "user-001"] // /app/user/user-001 = ["Form", "User", "user-001"]
// /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] // /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]]) { if (frappe.workspaces[route[0]]) {
// public workspace // public workspace
route = ['Workspaces', frappe.workspaces[route[0]].title]; 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 // private workspace
route = ['Workspaces', 'private', frappe.workspaces[private_wspace].title];
route = ['Workspaces', 'private', frappe.workspaces[private_workspace].title];
} else if (this.routes[route[0]]) { } else if (this.routes[route[0]]) {
// route // route
route = this.set_doctype_route(route); route = this.set_doctype_route(route);


+ 4
- 2
frappe/public/js/frappe/ui/dialog.js Ver arquivo

@@ -57,8 +57,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
// show footer // show footer
this.action = this.action || { primary: { }, secondary: { } }; this.action = this.action || { primary: { }, secondary: { } };
if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) { 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) { if (this.secondary_action) {


+ 1
- 1
frappe/public/js/frappe/ui/messages.js Ver arquivo

@@ -63,7 +63,7 @@ frappe.warn = function(title, message_html, proceed_action, primary_label, is_mi
if (proceed_action) proceed_action(); if (proceed_action) proceed_action();
d.hide(); d.hide();
}, },
secondary_action_label: __("Cancel"),
secondary_action_label: __("Cancel", null, "Secondary button in warning dialog"),
secondary_action: () => d.hide(), secondary_action: () => d.hide(),
minimizable: is_minimizable minimizable: is_minimizable
}); });


+ 24
- 22
frappe/public/js/frappe/ui/sort_selector.js Ver arquivo

@@ -113,42 +113,44 @@ frappe.ui.SortSelector = class SortSelector {
if(!this.args.options) { if(!this.args.options) {
// default options // default options
var _options = [ var _options = [
{'fieldname': 'modified'}
{'fieldname': 'modified'},
{'fieldname': 'name'},
{'fieldname': 'creation'},
{'fieldname': 'idx'},
] ]


// title field // 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) { 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}); _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 // de-duplicate
this.args.options = _options.uniqBy(function(obj) {
this.args.options = _options.uniqBy(obj => {
return obj.fieldname; return obj.fieldname;
}); });

// add missing labels
this.args.options.forEach(function(o) {
if(!o.label) {
o.label = me.get_label(o.fieldname);
}
});
} }


// set default // set default


+ 22
- 0
frappe/public/js/frappe/ui/workspace_sidebar_loading_skeleton.html Ver arquivo

@@ -0,0 +1,22 @@
<div class="workspace-sidebar-skeleton">
<div class="widget-group-body">
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
</div>
<div class="widget-group-body">
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box child skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
<div class="widget sidebar-box skeleton-card"></div>
</div>
</div>

+ 1
- 1
frappe/public/js/frappe/utils/diffview.js Ver arquivo

@@ -54,7 +54,7 @@ frappe.ui.DiffView = class DiffView {
fieldname: "diff", fieldname: "diff",
}, },
], ],
size: "large",
size: "extra-large",
}); });
return dialog; return dialog;
} }


+ 22
- 3
frappe/public/js/frappe/utils/utils.js Ver arquivo

@@ -243,9 +243,28 @@ Object.assign(frappe.utils, {
'=': '&#x3D;' '=': '&#x3D;'
}; };


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 = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&#x2F;': '/',
'&#x60;': '`',
'&#x3D;': '='
};

return String(txt).replace(
/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F;|&#x60;|&#x3D;/g,
char => unescape_html_mapping[char] || char
);
}, },


html2text: function(html) { html2text: function(html) {


+ 9
- 3
frappe/public/js/frappe/views/reports/report_view.js Ver arquivo

@@ -340,7 +340,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
options: columns_in_picker 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', fieldname: 'insert_before',
fieldtype: 'Check' fieldtype: 'Check'
} }
@@ -789,7 +789,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
} else { } else {
this.fields.splice(col_index, 0, field); 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]; const field = [col, doctype];
this.fields.push(field); this.fields.push(field);
this.refresh(); 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()])
);
} }
} }




+ 247
- 29
frappe/public/js/frappe/views/workspace/blocks/block.js Ver arquivo

@@ -7,7 +7,7 @@ export default class Block {


make(block, block_name, widget_type = block) { make(block, block_name, widget_type = block) {
let block_data = this.config.page_data[block+'s'].items.find(obj => { 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; if (!block_data) return false;
this.wrapper.innerHTML = ''; this.wrapper.innerHTML = '';
@@ -28,12 +28,64 @@ export default class Block {
return true; 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) { new(block, widget_type = block) {
let me = this;
const dialog_class = get_dialog_constructor(widget_type); const dialog_class = get_dialog_constructor(widget_type);
let block_name = block+'_name'; let block_name = block+'_name';
this.dialog = new dialog_class({ this.dialog = new dialog_class({
@@ -53,13 +105,18 @@ export default class Block {
}); });
this.block_widget.customize(this.options); this.block_widget.customize(this.options);
this.wrapper.setAttribute(block_name, this.block_widget.label); 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.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]) { if (!this.readOnly && this.data && !this.data[block_name]) {
this.dialog.make(); 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(); 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 = $(`
<div class="new-block-button">${frappe.utils.icon('add-round', 'lg')}</div>
`);

$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 = $(`
<div class="dropdown-btn">
<button class="btn btn-secondary btn-xs setting-btn" title="${__('Setting')}">
${frappe.utils.icon('dot-horizontal', 'xs')}
</button>
<div class="dropdown-list hidden"></div>
</div>
`);


let dropdown_item = function(label, title, icon, action) {
let html = $(`
<div class="dropdown-item" title="${title}">
<span class="dropdown-item-icon">${icon}</span>
<span class="dropdown-item-label">${label}</span>
</div>
`);

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() { get_col() {
let col = this.col || 12; let col = this.col || 12;
let class_name = "col-12";
let class_name = "col-xs-12";
let wrapper = this.wrapper.closest('.ce-block'); let wrapper = this.wrapper.closest('.ce-block');
const col_class = new RegExp(/\bcol-.+?\b/, "g"); const col_class = new RegExp(/\bcol-.+?\b/, "g");
if (wrapper && wrapper.className.match(col_class)) { if (wrapper && wrapper.className.match(col_class)) {
wrapper.classList.forEach(function (cn) { 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("-"); let parts = class_name.split("-");
col = parseInt(parts[1]);
col = parseInt(parts[2]);
} }
return col; 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);
}
} }

+ 7
- 4
frappe/public/js/frappe/views/workspace/blocks/card.js Ver arquivo

@@ -3,7 +3,7 @@ export default class Card extends Block {
static get toolbox() { static get toolbox() {
return { return {
title: 'Card', title: 'Card',
icon: '<svg height="20" width="20" viewBox="2 2 20 20"><path d="M7 15h3a1 1 0 000-2H7a1 1 0 000 2zM19 5H5a3 3 0 00-3 3v9a3 3 0 003 3h14a3 3 0 003-3V8a3 3 0 00-3-3zm1 12a1 1 0 01-1 1H5a1 1 0 01-1-1v-6h16zm0-8H4V8a1 1 0 011-1h14a1 1 0 011 1z"/></svg>'
icon: frappe.utils.icon('card', 'sm')
}; };
} }


@@ -22,6 +22,7 @@ export default class Card extends Block {
allow_delete: this.allow_customization, allow_delete: this.allow_customization,
allow_hiding: false, allow_hiding: false,
allow_edit: true, allow_edit: true,
allow_resize: true
}; };
} }


@@ -35,7 +36,9 @@ export default class Card extends Block {
} }


if (!this.readOnly) { 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; return this.wrapper;
@@ -49,9 +52,9 @@ export default class Card extends Block {
return true; return true;
} }


save(blockContent) {
save() {
return { return {
card_name: blockContent.getAttribute('card_name'),
card_name: this.wrapper.getAttribute('card_name'),
col: this.get_col(), col: this.get_col(),
new: this.new_block_widget new: this.new_block_widget
}; };


+ 9
- 5
frappe/public/js/frappe/views/workspace/blocks/chart.js Ver arquivo

@@ -3,7 +3,7 @@ export default class Chart extends Block {
static get toolbox() { static get toolbox() {
return { return {
title: 'Chart', title: 'Chart',
icon: '<svg height="18" width="18" viewBox="0 0 512 512"><path d="M117.547 234.667H10.88c-5.888 0-10.667 4.779-10.667 10.667v256C.213 507.221 4.992 512 10.88 512h106.667c5.888 0 10.667-4.779 10.667-10.667v-256a10.657 10.657 0 00-10.667-10.666zM309.12 0H202.453c-5.888 0-10.667 4.779-10.667 10.667v490.667c0 5.888 4.779 10.667 10.667 10.667H309.12c5.888 0 10.667-4.779 10.667-10.667V10.667C319.787 4.779 315.008 0 309.12 0zM501.12 106.667H394.453c-5.888 0-10.667 4.779-10.667 10.667v384c0 5.888 4.779 10.667 10.667 10.667H501.12c5.888 0 10.667-4.779 10.667-10.667v-384c0-5.889-4.779-10.667-10.667-10.667z"/></svg>'
icon: frappe.utils.icon('chart', 'sm')
}; };
} }


@@ -21,7 +21,9 @@ export default class Chart extends Block {
allow_delete: this.allow_customization, allow_delete: this.allow_customization,
allow_hiding: false, allow_hiding: false,
allow_edit: true, 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) { 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; return this.wrapper;
@@ -49,9 +53,9 @@ export default class Chart extends Block {
return true; return true;
} }


save(blockContent) {
save() {
return { return {
chart_name: blockContent.getAttribute('chart_name'),
chart_name: this.wrapper.getAttribute('chart_name'),
col: this.get_col(), col: this.get_col(),
new: this.new_block_widget new: this.new_block_widget
}; };


+ 24
- 221
frappe/public/js/frappe/views/workspace/blocks/header.js Ver arquivo

@@ -4,16 +4,8 @@ export default class Header extends Block {
constructor({ data, config, api, readOnly }) { constructor({ data, config, api, readOnly }) {
super({ 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._settings = this.config;
this._data = this.normalizeData(data); this._data = this.normalizeData(data);
this.settingsButtons = [];
this._element = this.getTag(); this._element = this.getTag();


this.data = data; this.data = data;
@@ -27,8 +19,7 @@ export default class Header extends Block {
data = {}; 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; newData.col = parseInt(data.col) || 12;


return newData; return newData;
@@ -36,7 +27,6 @@ export default class Header extends Block {


render() { render() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.contentEditable = this.readOnly ? 'false' : 'true';
if (!this.readOnly) { if (!this.readOnly) {
let $widget_head = $(`<div class="widget-head"></div>`); let $widget_head = $(`<div class="widget-head"></div>`);
let $widget_control = $(`<div class="widget-control"></div>`); let $widget_control = $(`<div class="widget-control"></div>`);
@@ -45,27 +35,10 @@ export default class Header extends Block {
$widget_control.appendTo($widget_head); $widget_control.appendTo($widget_head);
$widget_head.appendTo(this.wrapper); $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.add_custom_button(
frappe.utils.icon('drag', 'xs'), frappe.utils.icon('drag', 'xs'),
@@ -76,67 +49,14 @@ export default class Header extends Block {
$widget_control $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.wrapper;
} }
return this._element; 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) { merge(data) {
const newData = { const newData = {
text: this.data.text + data.text,
level: this.data.level,
text: this.data.text + data.text
}; };


this.data = newData; this.data = newData;
@@ -146,31 +66,28 @@ export default class Header extends Block {
return blockData.text.trim() !== ''; return blockData.text.trim() !== '';
} }


save(toolsContent) {
save() {
this.wrapper = this._element; this.wrapper = this._element;
return { return {
text: toolsContent.innerText,
level: this.currentLevel.number,
text: this.wrapper.innerHTML.replace(/&nbsp;/gi, ''),
col: this.get_col() col: this.get_col()
}; };
} }


rendered() { 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() { static get sanitize() {
return { return {
level: false, 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() { get data() {
this._data.text = this._element.innerHTML; this._data.text = this._element.innerHTML;
this._data.level = this.currentLevel.number;


return this._data; return this._data;
} }
@@ -188,15 +104,11 @@ export default class Header extends Block {
set data(data) { set data(data) {
this._data = this.normalizeData(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) { 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 : `<span class="h${this._settings.default_size}">${text}</span>`;
} }


if (!this.readOnly && this.wrapper) { if (!this.readOnly && this.wrapper) {
@@ -205,11 +117,12 @@ export default class Header extends Block {
} }


getTag() { getTag() {
const tag = document.createElement(this.currentLevel.tag);
const tag = document.createElement('DIV');


tag.innerHTML = this._data.text || '';
let text = this._data.text || '&nbsp';
tag.innerHTML = `<span class="h${this._settings.default_size}"><b>${text}</b></span>`;


tag.classList.add(this._CSS.wrapper);
tag.classList.add('ce-header');


if (!this.readOnly) { if (!this.readOnly) {
tag.contentEditable = true; tag.contentEditable = true;
@@ -220,120 +133,10 @@ export default class Header extends Block {
return tag; 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: '<svg width="16" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.14 1.494V4.98h4.62V1.494c0-.498.098-.871.293-1.12A.927.927 0 0 1 7.82 0c.322 0 .583.123.782.37.2.246.3.62.3 1.124v9.588c0 .503-.101.88-.303 1.128a.957.957 0 0 1-.779.374.921.921 0 0 1-.77-.378c-.193-.251-.29-.626-.29-1.124V6.989H2.14v4.093c0 .503-.1.88-.302 1.128a.957.957 0 0 1-.778.374.921.921 0 0 1-.772-.378C.096 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.285.374A.922.922 0 0 1 1.06 0c.321 0 .582.123.782.37.199.246.299.62.299 1.124zm11.653 9.985V5.27c-1.279.887-2.14 1.33-2.583 1.33a.802.802 0 0 1-.563-.228.703.703 0 0 1-.245-.529c0-.232.08-.402.241-.511.161-.11.446-.25.854-.424.61-.259 1.096-.532 1.462-.818a5.84 5.84 0 0 0 .97-.962c.282-.355.466-.573.552-.655.085-.082.246-.123.483-.123.267 0 .481.093.642.28.161.186.242.443.242.77v7.813c0 .914-.345 1.371-1.035 1.371-.307 0-.554-.093-.74-.28-.187-.186-.28-.461-.28-.825z"/></svg>',
},
{
number: 2,
tag: 'H2',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm10.99 9.288h3.527c.351 0 .62.072.804.216.185.144.277.34.277.588 0 .22-.073.408-.22.56-.146.154-.368.23-.665.23h-4.972c-.338 0-.601-.093-.79-.28a.896.896 0 0 1-.284-.659c0-.162.06-.377.182-.645s.255-.478.399-.631a38.617 38.617 0 0 1 1.621-1.598c.482-.444.827-.735 1.034-.875.369-.261.676-.523.922-.787.245-.263.432-.534.56-.81.129-.278.193-.549.193-.815 0-.288-.069-.546-.206-.773a1.428 1.428 0 0 0-.56-.53 1.618 1.618 0 0 0-.774-.19c-.59 0-1.054.26-1.392.777-.045.068-.12.252-.226.554-.106.302-.225.534-.358.696-.133.162-.328.243-.585.243a.76.76 0 0 1-.56-.223c-.149-.148-.223-.351-.223-.608 0-.31.07-.635.21-.972.139-.338.347-.645.624-.92a3.093 3.093 0 0 1 1.054-.665c.426-.169.924-.253 1.496-.253.69 0 1.277.108 1.764.324.315.144.592.343.83.595.24.252.425.544.558.875.133.33.2.674.2 1.03 0 .558-.14 1.066-.416 1.523-.277.457-.56.815-.848 1.074-.288.26-.771.666-1.45 1.22-.677.554-1.142.984-1.394 1.29a3.836 3.836 0 0 0-.331.44z"/></svg>',
},
{
number: 3,
tag: 'H3',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm11.61 4.919c.418 0 .778-.123 1.08-.368.301-.245.452-.597.452-1.055 0-.35-.12-.65-.36-.902-.241-.252-.566-.378-.974-.378-.277 0-.505.038-.684.116a1.1 1.1 0 0 0-.426.306 2.31 2.31 0 0 0-.296.49c-.093.2-.178.388-.255.565a.479.479 0 0 1-.245.225.965.965 0 0 1-.409.081.706.706 0 0 1-.5-.22c-.152-.148-.228-.345-.228-.59 0-.236.071-.484.214-.745a2.72 2.72 0 0 1 .627-.746 3.149 3.149 0 0 1 1.024-.568 4.122 4.122 0 0 1 1.368-.214c.44 0 .842.06 1.205.18.364.12.679.294.947.52.267.228.47.49.606.79.136.3.204.622.204.967 0 .454-.099.843-.296 1.168-.198.324-.48.64-.848.95.354.19.653.408.895.653.243.245.426.516.548.813.123.298.184.619.184.964 0 .413-.083.812-.248 1.198-.166.386-.41.73-.732 1.031a3.49 3.49 0 0 1-1.147.708c-.443.17-.932.256-1.467.256a3.512 3.512 0 0 1-1.464-.293 3.332 3.332 0 0 1-1.699-1.64c-.142-.314-.214-.573-.214-.777 0-.263.085-.475.255-.636a.89.89 0 0 1 .637-.242c.127 0 .25.037.367.112a.53.53 0 0 1 .232.27c.236.63.489 1.099.759 1.405.27.306.65.46 1.14.46a1.714 1.714 0 0 0 1.46-.824c.17-.273.256-.588.256-.947 0-.53-.145-.947-.436-1.249-.29-.302-.694-.453-1.212-.453-.09 0-.231.01-.422.028-.19.018-.313.027-.367.027-.25 0-.443-.062-.579-.187-.136-.125-.204-.299-.204-.521 0-.218.081-.394.245-.528.163-.134.406-.2.728-.2h.28z"/></svg>',
},
{
number: 4,
tag: 'H4',
svg: '<svg width="20" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm13.003 10.09v-1.252h-3.38c-.427 0-.746-.097-.96-.29-.213-.193-.32-.456-.32-.788 0-.085.016-.171.048-.259.031-.088.078-.18.141-.276.063-.097.128-.19.195-.28.068-.09.15-.2.25-.33l3.568-4.774a5.44 5.44 0 0 1 .576-.683.763.763 0 0 1 .542-.212c.682 0 1.023.39 1.023 1.171v5.212h.29c.346 0 .623.047.832.142.208.094.313.3.313.62 0 .26-.086.45-.256.568-.17.12-.427.179-.768.179h-.41v1.252c0 .346-.077.603-.23.771-.152.168-.356.253-.612.253a.78.78 0 0 1-.61-.26c-.154-.173-.232-.427-.232-.764zm-2.895-2.76h2.895V4.91L12.26 8.823z"/></svg>',
},
{
number: 5,
tag: 'H5',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm14.16 2.645h-3.234l-.388 2.205c.644-.344 1.239-.517 1.783-.517.436 0 .843.082 1.222.245.38.164.712.39.998.677.286.289.51.63.674 1.025.163.395.245.82.245 1.273 0 .658-.148 1.257-.443 1.797-.295.54-.72.97-1.276 1.287-.556.318-1.197.477-1.923.477-.813 0-1.472-.15-1.978-.45-.506-.3-.865-.643-1.076-1.031-.21-.388-.316-.727-.316-1.018 0-.177.073-.345.22-.504a.725.725 0 0 1 .556-.238c.381 0 .665.22.85.66.182.404.427.719.736.943.309.225.654.337 1.035.337.35 0 .656-.09.919-.272.263-.182.466-.431.61-.749.142-.318.214-.678.214-1.082 0-.436-.078-.808-.232-1.117a1.607 1.607 0 0 0-.62-.69 1.674 1.674 0 0 0-.864-.229c-.39 0-.67.048-.837.143-.168.095-.41.262-.725.5-.316.239-.576.358-.78.358a.843.843 0 0 1-.592-.242c-.173-.16-.259-.344-.259-.548 0-.022.025-.177.075-.463l.572-3.26c.063-.39.181-.675.354-.852.172-.177.454-.265.844-.265h3.595c.708 0 1.062.27 1.062.81a.711.711 0 0 1-.26.572c-.172.145-.426.218-.762.218z"/></svg>',
},
{
number: 6,
tag: 'H6',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zM12.53 7.058a3.093 3.093 0 0 1 1.004-.814 2.734 2.734 0 0 1 1.214-.264c.43 0 .827.08 1.19.24.365.161.684.39.957.686.274.296.485.645.635 1.048a3.6 3.6 0 0 1 .223 1.262c0 .637-.145 1.216-.437 1.736-.292.52-.699.926-1.221 1.218-.522.292-1.114.438-1.774.438-.76 0-1.416-.186-1.967-.557-.552-.37-.974-.919-1.265-1.645-.292-.726-.438-1.613-.438-2.662 0-.855.088-1.62.265-2.293.176-.674.43-1.233.76-1.676.33-.443.73-.778 1.2-1.004.47-.226 1.006-.339 1.608-.339.579 0 1.089.113 1.53.34.44.225.773.506.997.84.224.335.335.656.335.964 0 .185-.07.354-.21.505a.698.698 0 0 1-.536.227.874.874 0 0 1-.529-.18 1.039 1.039 0 0 1-.36-.498 1.42 1.42 0 0 0-.495-.655 1.3 1.3 0 0 0-.786-.247c-.24 0-.479.069-.716.207a1.863 1.863 0 0 0-.6.56c-.33.479-.525 1.333-.584 2.563zm1.832 4.213c.456 0 .834-.186 1.133-.56.298-.373.447-.862.447-1.468 0-.412-.07-.766-.21-1.062a1.584 1.584 0 0 0-.577-.678 1.47 1.47 0 0 0-.807-.234c-.28 0-.548.074-.804.224-.255.149-.461.365-.617.647a2.024 2.024 0 0 0-.234.994c0 .61.158 1.12.475 1.527.316.407.714.61 1.194.61z"/></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() { static get toolbox() {
return { return {
icon: '<svg width="10" height="14" viewBox="0 0 10 14"><path d="M7.6 8.15H2.25v4.525a1.125 1.125 0 0 1-2.25 0V1.125a1.125 1.125 0 1 1 2.25 0V5.9H7.6V1.125a1.125 1.125 0 0 1 2.25 0v11.55a1.125 1.125 0 0 1-2.25 0V8.15z"></path></svg>',
title: 'Heading', title: 'Heading',
icon: frappe.utils.icon('header', 'sm')
}; };
} }
} }

+ 117
- 0
frappe/public/js/frappe/views/workspace/blocks/header_size.js Ver arquivo

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

+ 5
- 5
frappe/public/js/frappe/views/workspace/blocks/index.js Ver arquivo

@@ -8,11 +8,11 @@ import Spacer from "./spacer";
import Onboarding from "./onboarding"; import Onboarding from "./onboarding";


// import tunes // 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, header: Header,
paragraph: Paragraph, paragraph: Paragraph,
card: Card, card: Card,
@@ -22,6 +22,6 @@ frappe.wspace_block.blocks = {
onboarding: Onboarding, onboarding: Onboarding,
}; };


frappe.wspace_block.tunes = {
spacing_tune: SpacingTune
frappe.workspace_block.tunes = {
header_size: HeaderSize,
}; };

+ 18
- 9
frappe/public/js/frappe/views/workspace/blocks/onboarding.js Ver arquivo

@@ -4,7 +4,7 @@ export default class Onboarding extends Block {
static get toolbox() { static get toolbox() {
return { return {
title: 'Onboarding', title: 'Onboarding',
icon: '<svg width="24" height="24" viewBox="2 0 20 24" fill="none"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 11.09v5.455" stroke="#1F272E" fill="none"/><path d="M12.41 7.455a.41.41 0 11-.82 0 .41.41 0 01.82 0z" stroke="#1F272E"/></svg>'
icon: frappe.utils.icon('onboarding', 'sm')
}; };
} }


@@ -21,19 +21,21 @@ export default class Onboarding extends Block {
allow_create: this.allow_customization, allow_create: this.allow_customization,
allow_delete: this.allow_customization, allow_delete: this.allow_customization,
allow_hiding: false, allow_hiding: false,
allow_edit: true
allow_edit: true,
allow_resize: false
}; };
} }


rendered() { 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')) { 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) { new(block, widget_type = block) {
let me = this;
const dialog_class = get_dialog_constructor(widget_type); const dialog_class = get_dialog_constructor(widget_type);
let block_name = block+'_name'; let block_name = block+'_name';
this.dialog = new dialog_class({ this.dialog = new dialog_class({
@@ -54,13 +56,18 @@ export default class Onboarding extends Block {
}); });
this.block_widget.customize(this.options); this.block_widget.customize(this.options);
this.wrapper.setAttribute(block_name, this.block_widget.label || this.block_widget.onboarding_name); 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.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]) { if (!this.readOnly && this.data && !this.data[block_name]) {
this.dialog.make(); 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) { 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"); $(this.wrapper).css("padding-bottom", "20px");
return this.wrapper; return this.wrapper;
@@ -119,9 +128,9 @@ export default class Onboarding extends Block {
return true; return true;
} }


save(blockContent) {
save() {
return { return {
onboarding_name: blockContent.getAttribute('onboarding_name'),
onboarding_name: this.wrapper.getAttribute('onboarding_name'),
col: this.get_col(), col: this.get_col(),
new: this.new_block_widget new: this.new_block_widget
}; };


+ 61
- 44
frappe/public/js/frappe/views/workspace/blocks/paragraph.js Ver arquivo

@@ -27,6 +27,8 @@ export default class Paragraph extends Block {
} }


onKeyUp(e) { onKeyUp(e) {
if (!this.wrapper) return;
this.show_hide_block_list(true);
if (e.code !== 'Backspace' && e.code !== 'Delete') { if (e.code !== 'Backspace' && e.code !== 'Delete') {
return; return;
} }
@@ -34,55 +36,86 @@ export default class Paragraph extends Block {
const {textContent} = this._element; const {textContent} = this._element;


if (textContent === '') { if (textContent === '') {
this.show_hide_block_list();
this._element.innerHTML = ''; 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() { drawView() {
let div = document.createElement('DIV'); let div = document.createElement('DIV');


div.classList.add(this._CSS.wrapper, this._CSS.block, 'widget'); div.classList.add(this._CSS.wrapper, this._CSS.block, 'widget');
div.contentEditable = false; div.contentEditable = false;
div.dataset.placeholder = this.api.i18n.t(this._placeholder);


if (!this.readOnly) { if (!this.readOnly) {
div.contentEditable = true; 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); div.addEventListener('keyup', this.onKeyUp);
} }
return div; return div;
} }


open_block_list() {
let dropdown_title = 'Templates';
let $block_list_container = $(`
<div class="block-list-container dropdown-list">
<div class="dropdown-title">${dropdown_title.toUpperCase()}</div>
</div>
`);

let all_blocks = frappe.workspace_block.blocks;
Object.keys(all_blocks).forEach(key => {
let $block_list_item = $(`
<div class="block-list-item dropdown-item">
<span class="dropdown-item-icon">${all_blocks[key].toolbox.icon}</span>
<span class="dropdown-item-label">${__(all_blocks[key].toolbox.title)}</span>
</div>
`);

$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() { render() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.contentEditable = this.readOnly ? 'false' : 'true';
if (!this.readOnly) { if (!this.readOnly) {
let $para_control = $(`<div class="paragraph-control"></div>`);
let $para_control = $(`<div class="widget-control paragraph-control"></div>`);


this.wrapper.appendChild(this._element); this.wrapper.appendChild(this._element);
this._element.classList.remove('widget'); this._element.classList.remove('widget');
$para_control.appendTo(this.wrapper); $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.add_custom_button(
frappe.utils.icon('drag', 'xs'), frappe.utils.icon('drag', 'xs'),
@@ -93,15 +126,6 @@ export default class Paragraph extends Block {
$para_control $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.wrapper;
} }
return this._element; return this._element;
@@ -132,8 +156,7 @@ export default class Paragraph extends Block {
} }


rendered() { rendered() {
var e = this._element.closest('.ce-block');
e.classList.add("col-" + this.get_col());
super.rendered(this._element);
} }


onPaste(event) { onPaste(event) {
@@ -144,20 +167,14 @@ export default class Paragraph extends Block {
this.data = data; 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() { static get sanitize() {
return { return {
text: { text: {
br: true, br: true,
b: true, b: true,
i: true, i: true,
a: true
a: true,
span: true
} }
}; };
} }
@@ -188,8 +205,8 @@ export default class Paragraph extends Block {


static get toolbox() { static get toolbox() {
return { return {
icon: '<svg viewBox="0.2 -0.3 9 11.4" width="12" height="14"><path d="M0 2.77V.92A1 1 0 01.2.28C.35.1.56 0 .83 0h7.66c.28.01.48.1.63.28.14.17.21.38.21.64v1.85c0 .26-.08.48-.23.66-.15.17-.37.26-.66.26-.28 0-.5-.09-.64-.26a1 1 0 01-.21-.66V1.69H5.6v7.58h.5c.25 0 .45.08.6.23.17.16.25.35.25.6s-.08.45-.24.6a.87.87 0 01-.62.22H3.21a.87.87 0 01-.61-.22.78.78 0 01-.24-.6c0-.25.08-.44.24-.6a.85.85 0 01.61-.23h.5V1.7H1.73v1.08c0 .26-.08.48-.23.66-.15.17-.37.26-.66.26-.28 0-.5-.09-.64-.26A1 1 0 010 2.77z"/></svg>',
title: 'Text'
title: 'Text',
icon: frappe.utils.icon('text', 'sm')
}; };
} }
} }

+ 35
- 6
frappe/public/js/frappe/views/workspace/blocks/shortcut.js Ver arquivo

@@ -3,7 +3,7 @@ export default class Shortcut extends Block {
static get toolbox() { static get toolbox() {
return { return {
title: 'Shortcut', title: 'Shortcut',
icon: '<svg height="18" width="18" viewBox="0 0 122.88 115.71"><path d="M116.56 3.69l-3.84 53.76-17.69-15c-19.5 8.72-29.96 23.99-30.51 43.77-17.95-26.98-7.46-50.4 12.46-65.97L64.96 3l51.6.69zM28.3 0h14.56v19.67H32.67c-4.17 0-7.96 1.71-10.72 4.47-2.75 2.75-4.46 6.55-4.46 10.72l-.03 46c.03 4.16 1.75 7.95 4.5 10.71 2.76 2.76 6.56 4.48 10.71 4.48h58.02c4.15 0 7.95-1.72 10.71-4.48 2.76-2.76 4.48-6.55 4.48-10.71V73.9h17.01v11.33c0 7.77-3.2 17.04-8.32 22.16-5.12 5.12-12.21 8.32-19.98 8.32H28.3c-7.77 0-14.86-3.2-19.98-8.32C3.19 102.26 0 95.18 0 87.41l.03-59.1c-.03-7.79 3.16-14.88 8.28-20C13.43 3.19 20.51 0 28.3 0z" fill-rule="evenodd" clip-rule="evenodd"/></svg>'
icon: frappe.utils.icon('shortcut', 'sm')
}; };
} }


@@ -13,17 +13,39 @@ export default class Shortcut extends Block {


constructor({ data, api, config, readOnly, block }) { constructor({ data, api, config, readOnly, block }) {
super({ 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.allow_customization = !this.readOnly;
this.options = { this.options = {
allow_sorting: this.allow_customization, allow_sorting: this.allow_customization,
allow_create: this.allow_customization, allow_create: this.allow_customization,
allow_delete: this.allow_customization, allow_delete: this.allow_customization,
allow_hiding: false, 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() { render() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.new('shortcut'); this.new('shortcut');
@@ -34,7 +56,14 @@ export default class Shortcut extends Block {
} }


if (!this.readOnly) { 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($(`<div class="divider"></div>`));
} }
return this.wrapper; return this.wrapper;
} }
@@ -47,9 +76,9 @@ export default class Shortcut extends Block {
return true; return true;
} }


save(blockContent) {
save() {
return { return {
shortcut_name: blockContent.getAttribute('shortcut_name'),
shortcut_name: this.wrapper.getAttribute('shortcut_name'),
col: this.get_col(), col: this.get_col(),
new: this.new_block_widget new: this.new_block_widget
}; };


+ 7
- 32
frappe/public/js/frappe/views/workspace/blocks/spacer.js Ver arquivo

@@ -3,7 +3,7 @@ export default class Spacer extends Block {
static get toolbox() { static get toolbox() {
return { return {
title: 'Spacer', title: 'Spacer',
icon: '<svg width="18" height="18" viewBox="0 0 400 400"><path d="M377.87 24.126C361.786 8.042 342.417 0 319.769 0H82.227C59.579 0 40.211 8.042 24.125 24.126 8.044 40.212.002 59.576.002 82.228v237.543c0 22.647 8.042 42.014 24.123 58.101 16.086 16.085 35.454 24.127 58.102 24.127h237.542c22.648 0 42.011-8.042 58.102-24.127 16.085-16.087 24.126-35.453 24.126-58.101V82.228c-.004-22.648-8.046-42.016-24.127-58.102zm-12.422 295.645c0 12.559-4.47 23.314-13.415 32.264-8.945 8.945-19.698 13.411-32.265 13.411H82.227c-12.563 0-23.317-4.466-32.264-13.411-8.945-8.949-13.418-19.705-13.418-32.264V82.228c0-12.562 4.473-23.316 13.418-32.264 8.947-8.946 19.701-13.418 32.264-13.418h237.542c12.566 0 23.319 4.473 32.265 13.418 8.945 8.947 13.415 19.701 13.415 32.264v237.543h-.001z"/></svg>'
icon: frappe.utils.icon('spacer', 'sm')
}; };
} }


@@ -18,40 +18,24 @@ export default class Spacer extends Block {


render() { render() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.classList.add('widget', 'spacer');
if (!this.readOnly) { if (!this.readOnly) {
let $spacer = $(` let $spacer = $(`
<div class="widget-head"> <div class="widget-head">
<div></div>
<div class="spacer-left"></div>
<div>Spacer</div> <div>Spacer</div>
<div class="widget-control"></div> <div class="widget-control"></div>
</div> </div>
`); `);
$spacer.appendTo(this.wrapper); $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'); 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.add_custom_button(
frappe.utils.icon('drag', 'xs'), frappe.utils.icon('drag', 'xs'),
@@ -61,15 +45,6 @@ export default class Spacer extends Block {
null, null,
$widget_control $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; return this.wrapper;
} }


+ 0
- 123
frappe/public/js/frappe/views/workspace/blocks/spacing_tune.js Ver arquivo

@@ -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 = `<svg version="1.1" height="10" x="0px" y="0px" viewBox="-674 380 17 10" style="enable-background:new -674 380 17 10;" xml:space="preserve"><path d="M-674,383.9h3.6l-1.7-1.7c-0.4-0.4-0.4-1.2,0-1.6c0.4-0.4,1.1-0.4,1.6,0l3.2,3.2c0.6,0.2,0.8,0.8,0.6,1.4 c-0.1,0.1-0.1,0.3-0.2,0.4l-3.8,3.8c-0.4,0.4-1.1,0.4-1.5,0c-0.4-0.4-0.4-1.1,0-1.5l1.8-1.8h-3.6V383.9z"/><path d="M-657,386.1h-3.6l1.7,1.7c0.4,0.4,0.4,1.2,0,1.6c-0.4,0.4-1.1,0.4-1.6,0l-3.2-3.2c-0.6-0.2-0.8-0.8-0.6-1.4 c0.1-0.1,0.1-0.3,0.2-0.4l3.8-3.8c0.4-0.4,1.1-0.4,1.5,0c0.4,0.4,0.4,1.1,0,1.5l-1.8,1.8h3.6V386.1z"/></svg>`;
this.api.tooltip.onHover(decreaseWidthButton, 'Shrink', {
placement: 'top',
hidingDelay: 500,
});
this.api.listeners.on(
decreaseWidthButton,
'click',
() => me.decreaseWidth(),
false
);

increaseWidthButton.innerHTML = `<svg width="17" height="10" viewBox="0 0 17 10"><path d="M13.568 5.925H4.056l1.703 1.703a1.125 1.125 0 0 1-1.59 1.591L.962 6.014A1.069 1.069 0 0 1 .588 4.26L4.38.469a1.069 1.069 0 0 1 1.512 1.511L4.084 3.787h9.606l-1.85-1.85a1.069 1.069 0 1 1 1.512-1.51l3.792 3.791a1.069 1.069 0 0 1-.475 1.788L13.514 9.16a1.125 1.125 0 0 1-1.59-1.591l1.644-1.644z"/></svg>`;
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);
}
}
}
}

+ 637
- 191
frappe/public/js/frappe/views/workspace/workspace.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 8
- 6
frappe/public/js/frappe/web_form/web_form.js Ver arquivo

@@ -160,17 +160,17 @@ export default class WebForm extends frappe.ui.FieldGroup {
} }


setup_primary_action() { 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.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() this.save()
); );
} }


setup_cancel_button() { 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() { setup_delete_button() {
@@ -216,16 +216,18 @@ export default class WebForm extends frappe.ui.FieldGroup {


let message = ''; let message = '';
if (invalid_values.length) { if (invalid_values.length) {
message += __('Invalid values for fields:') + '<br><br><ul><li>' + invalid_values.join('<li>') + '</ul>';
message += __('Invalid values for fields:', null, 'Error message in web form');
message += '<br><br><ul><li>' + invalid_values.join('<li>') + '</ul>';
} }


if (errors.length) { if (errors.length) {
message += __('Mandatory fields required:') + '<br><br><ul><li>' + errors.join('<li>') + '</ul>';
message += __('Mandatory fields required:', null, 'Error message in web form');
message += '<br><br><ul><li>' + errors.join('<li>') + '</ul>';
} }


if (invalid_values.length || errors.length) { if (invalid_values.length || errors.length) {
frappe.msgprint({ frappe.msgprint({
title: __('Error'),
title: __('Error', null, 'Title of error message in web form'),
message: message, message: message,
indicator: 'orange' indicator: 'orange'
}); });


+ 4
- 34
frappe/public/js/frappe/widgets/base_widget.js Ver arquivo

@@ -34,16 +34,6 @@ export default class Widget {
this.action_area 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 (options.allow_hiding) {
if (this.hidden) { if (this.hidden) {
this.widget.removeClass("hidden"); this.widget.removeClass("hidden");
@@ -71,27 +61,11 @@ export default class Widget {
frappe.utils.add_custom_button( frappe.utils.add_custom_button(
frappe.utils.icon("edit", "xs"), frappe.utils.icon("edit", "xs"),
() => this.edit(), () => this.edit(),
null,
"edit-button",
`${__('Edit')}`, `${__('Edit')}`,
null, null,
this.action_area this.action_area
); );

if (options.allow_resize) {
const title = this.width == 'Full'? `${__('Collapse')}` : `${__('Expand')}`;
frappe.utils.add_custom_button(
'<i class="fa fa-expand" aria-hidden="true"></i>',
() => this.toggle_width(),
"resize-button",
title,
null,
this.action_area
);

this.resize_button = this.action_area.find(
".resize-button"
);
}
} }


make() { make() {
@@ -100,9 +74,7 @@ export default class Widget {
} }


make_widget() { make_widget() {
this.widget = $(`<div class="widget
${ this.shadow ? "widget-shadow" : " " }
" data-widget-name="${this.name ? this.name : ''}">
this.widget = $(`<div class="widget" data-widget-name="${this.name ? this.name : ''}">
<div class="widget-head"> <div class="widget-head">
<div class="widget-label"> <div class="widget-label">
<div class="widget-title"></div> <div class="widget-title"></div>
@@ -110,10 +82,8 @@ export default class Widget {
</div> </div>
<div class="widget-control"></div> <div class="widget-control"></div>
</div> </div>
<div class="widget-body">
</div>
<div class="widget-footer">
</div>
<div class="widget-body"></div>
<div class="widget-footer"></div>
</div>`); </div>`);


this.title_field = this.widget.find(".widget-title"); this.title_field = this.widget.find(".widget-title");


+ 1
- 1
frappe/public/js/frappe/widgets/chart_widget.js Ver arquivo

@@ -28,7 +28,7 @@ export default class ChartWidget extends Widget {
} }


set_chart_title() { 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); this.set_title(max_chars);
} }




+ 3
- 1
frappe/public/js/frappe/widgets/links_widget.js Ver arquivo

@@ -80,7 +80,9 @@ export default class LinksWidget extends Widget {


return $(`<a href="${route}" class="link-item ellipsis ${ return $(`<a href="${route}" class="link-item ellipsis ${
item.onboard ? "onboard-spotlight" : "" item.onboard ? "onboard-spotlight" : ""
} ${disabled_dependent(item)}" type="${item.type}">
} ${disabled_dependent(item)}" type="${item.type}" title="${
item.label ? item.label : item.name
}">
<span class="indicator-pill no-margin ${get_indicator_color(item)}"></span> <span class="indicator-pill no-margin ${get_indicator_color(item)}"></span>
${get_link_for_item(item)} ${get_link_for_item(item)}
</a>`); </a>`);


+ 8
- 10
frappe/public/js/frappe/widgets/widget_dialog.js Ver arquivo

@@ -9,6 +9,7 @@ class WidgetDialog {
this.setup_dialog_events(); this.setup_dialog_events();
this.dialog.show(); this.dialog.show();


window.cur_dialog = this.dialog;
this.editing && this.set_default_values(); this.editing && this.set_default_values();
} }


@@ -181,19 +182,16 @@ class CardDialog extends WidgetDialog {
fieldtype: "Select", fieldtype: "Select",
in_list_view: 1, in_list_view: 1,
label: "Link Type", label: "Link Type",
options: ["DocType", "Page", "Report"],
onchange: (e) => {
me.link_to = e.currentTarget.value;
}
options: ["DocType", "Page", "Report"]
}, },
{ {
fieldname: "link_to", fieldname: "link_to",
fieldtype: "Dynamic Link", fieldtype: "Dynamic Link",
in_list_view: 1, in_list_view: 1,
label: "Link To", 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() { setup_dialog_events() {
if (!this.document_type) { 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.document_type = this.default_values['doctype'];
this.setup_filter(this.default_values['doctype']); this.setup_filter(this.default_values['doctype']);
this.set_aggregate_function_fields(); this.set_aggregate_function_fields();
@@ -518,7 +516,7 @@ class NumberCardDialog extends WidgetDialog {


set_aggregate_function_fields() { set_aggregate_function_fields() {
let 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 => { frappe.get_meta(this.document_type).fields.map(df => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
if (df.fieldtype == 'Currency') { if (df.fieldtype == 'Currency') {
@@ -537,7 +535,7 @@ class NumberCardDialog extends WidgetDialog {
if (data.new_or_existing == 'Existing Card') { if (data.new_or_existing == 'Existing Card') {
data.name = data.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; data.document_type = this.document_type;


return data; return data;


+ 1
- 1
frappe/public/scss/common/buttons.scss Ver arquivo

@@ -62,7 +62,7 @@
background-color: var(--control-bg); background-color: var(--control-bg);
color: var(--text-color); color: var(--text-color);
&:hover, &:active { &:hover, &:active {
background-color: var(--gray-300);
background-color: var(--gray-400);
color: var(--text-color); color: var(--text-color);
} }
} }


+ 2
- 0
frappe/public/scss/desk/dark.scss Ver arquivo

@@ -87,6 +87,8 @@


--highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500); --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
--input-disabled-bg: none; --input-disabled-bg: none;




+ 340
- 83
frappe/public/scss/desk/desktop.scss Ver arquivo

@@ -107,13 +107,20 @@ body {
} }
} }


.divider {
height: 30%;
position: absolute;
top: 18px;
right: 0;
border-right: 1px solid var(--gray-400);
}

.widget { .widget {
@include flex(flex, null, null, column); @include flex(flex, null, null, column);
min-height: 1px; min-height: 1px;
padding: 15px;
padding: 7px;
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
height: 100%; height: 100%;
box-shadow: var(--card-shadow);
background-color: var(--card-bg); background-color: var(--card-bg);


.btn { .btn {
@@ -143,6 +150,7 @@ body {
font-weight: 500; font-weight: 500;
line-height: 1.3em; line-height: 1.3em;
color: var(--heading-color); color: var(--heading-color);
cursor: default;


svg { svg {
flex: none; flex: none;
@@ -329,9 +337,28 @@ body {
} }


&.onboarding-widget-box { &.onboarding-widget-box {
margin-top: var(--margin-xs);
margin-bottom: var(--margin-2xl); 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 { .widget-head {
display: flex; display: flex;
@@ -390,12 +417,6 @@ body {
.step-index.step-pending { .step-index.step-pending {
display: flex; display: flex;
} }

&.active {
.step-index.step-pending {
background-color: var(--fg-color);
}
}
} }


&.complete { &.complete {
@@ -418,7 +439,11 @@ body {


&.active, &.active,
&:hover { &:hover {
background-color: var(--bg-light-gray);
background-color: var(--fg-color);

.step-index {
background-color: var(--bg-color);
}


.step-skip { .step-skip {
visibility: visible; visibility: visible;
@@ -434,7 +459,7 @@ body {
height: 20px; height: 20px;
width: 20px; width: 20px;
color: var(--text-on-light-gray); color: var(--text-on-light-gray);
background-color: var(--bg-light-gray);
background-color: var(--fg-color);


margin-right: var(--margin-sm); margin-right: var(--margin-sm);
border-radius: var(--border-radius-full); border-radius: var(--border-radius-full);
@@ -447,7 +472,7 @@ body {
display: none; display: none;
background-color: var(--primary); background-color: var(--primary);
.icon use { .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 { .widget-body {
flex-direction: column; flex-direction: column;
.onboarding-steps-wrapper { .onboarding-steps-wrapper {
@@ -513,9 +538,19 @@ body {
&.shortcut-widget-box { &.shortcut-widget-box {
cursor: pointer; 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 { .indicator-pill {
@@ -631,8 +666,8 @@ body {
width: 18px; width: 18px;


.icon-xs { .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"] { [data-page-route="Workspaces"] {
@media (min-width: map-get($grid-breakpoints, "lg")) { @media (min-width: map-get($grid-breakpoints, "lg")) {
.layout-main { .layout-main {
@@ -764,7 +818,6 @@ body {
.layout-side-section, .layout-main-section-wrapper { .layout-side-section, .layout-main-section-wrapper {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
padding-right: 25px;
scrollbar-color: var(--gray-200) transparent; scrollbar-color: var(--gray-200) transparent;
[data-theme="dark"] & { [data-theme="dark"] & {
scrollbar-color: var(--gray-800) transparent; scrollbar-color: var(--gray-800) transparent;
@@ -783,7 +836,12 @@ body {
} }


.layout-side-section { .layout-side-section {
margin-right: 20px;
padding-right: 15px;
}

.layout-main-section {
padding: var(--padding-md);
margin-bottom: var(--margin-sm);
} }


.desk-sidebar { .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{ .block-menu-item-icon svg{
width: 12px;
height: 12px;
width: 18px;
height: 18px;
margin-right: 5px; margin-right: 5px;
} }


@@ -803,7 +867,6 @@ body {
padding: 0px; padding: 0px;


.sidebar-item-control { .sidebar-item-control {

> * { > * {
align-self: center; align-self: center;
margin-left: 3px; margin-left: 3px;
@@ -816,7 +879,7 @@ body {
display: none; display: none;
} }


.delete-page {
.setting-btn, .duplicate-page {
display: none; display: none;
} }


@@ -824,13 +887,13 @@ body {
padding: 10px 12px 10px 2px; padding: 10px 12px 10px 2px;
} }


.sidebar-info {
display: none;
}

svg { svg {
margin-right: 0; margin-right: 0;
} }

.dropdown-list {
top: 42px;
}
} }


.sidebar-item-label { .sidebar-item-label {
@@ -846,6 +909,7 @@ body {
} }


.sidebar-item-container { .sidebar-item-container {
position: relative;
.sidebar-item-container{ .sidebar-item-container{
margin-left: 10px; margin-left: 10px;


@@ -863,19 +927,14 @@ body {
display: inline-block; display: inline-block;
} }


.delete-page {
display: inline-block;
margin-right: 8px;
}

.sidebar-info {
.setting-btn, .duplicate-page {
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
} }


.drop-icon { .drop-icon {
padding: 10px 8px 10px 2px; padding: 10px 8px 10px 2px;
margin-left: -4px;
margin-left: -8px;
} }
} }


@@ -899,11 +958,81 @@ body {
margin: 0px -7px; margin: 0px -7px;
padding-bottom: 20px !important; padding-bottom: 20px !important;


.ce-block{
.ce-block {
width: 100%; width: 100%;
padding-left: 0; padding-left: 0;
padding-right: 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--selected {
.ce-block__content { .ce-block__content {
background-color: inherit; background-color: inherit;
@@ -923,50 +1052,125 @@ body {
pointer-events: none; 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 { .ce-header {
padding: 0 !important;
padding-left: 7px !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;
flex: 1; 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 { &.header {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex: 1; flex: 1;
padding-left: 15px !important;
padding-right: 15px !important;
min-height: 50px;
padding-left: 0px !important;
min-height: 40px;
box-shadow: none; box-shadow: none;
background-color: var(--control-bg); background-color: var(--control-bg);
color: var(--text-muted); 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 {

&.ce-toolbar--opened {
display: none;
}

svg { svg {
fill: currentColor; fill: currentColor;
} }


.icon { .icon {
stroke: none; stroke: none;
width: fit-content;
height: fit-content;

&.icon--plus {
width: 14px;
}
} }


.ce-settings { .ce-settings {
@@ -993,6 +1204,10 @@ body {


.ce-settings__button, .cdx-settings-button { .ce-settings__button, .cdx-settings-button {
color: #707684; color: #707684;

.icon {
width: 14px;
}
} }


.cdx-settings-button--active { .cdx-settings-button--active {
@@ -1024,6 +1239,10 @@ body {
.icon { .icon {
fill: currentColor; fill: currentColor;
} }

svg {
stroke: none;
}
} }


@media (min-width: 1199px) { @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;
}

} }

+ 0
- 6
frappe/public/scss/website/index.scss Ver arquivo

@@ -31,12 +31,6 @@
@import 'my_account'; @import 'my_account';




body {
@include media-breakpoint-up(sm) {
background-color: var(--bg-color);
}
}

.ql-editor.read-mode { .ql-editor.read-mode {
padding: 0; padding: 0;
line-height: 1.6; line-height: 1.6;


+ 2
- 1
frappe/public/scss/website/my_account.scss Ver arquivo

@@ -1,7 +1,8 @@
//styles for my account and edit-profile page //styles for my account and edit-profile page
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
body[data-path="me"], body[data-path="me"],
body[data-path="list"] {
body[data-path="list"],
body[data-path="update-profile"] {
background-color: var(--bg-color); background-color: var(--bg-color);
} }
} }


+ 1
- 1
frappe/public/scss/website/web_form.scss Ver arquivo

@@ -6,7 +6,7 @@
.breadcrumb-container.container { .breadcrumb-container.container {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
padding-left: var(--padding-sm);
padding-left: 0;
} }
} }




+ 57
- 1
frappe/tests/test_db_update.py Ver arquivo

@@ -34,6 +34,58 @@ class TestDBUpdate(unittest.TestCase):
self.assertEqual(fieldtype, table_column.type) self.assertEqual(fieldtype, table_column.type)
self.assertIn(cstr(table_column.default) or 'NULL', [cstr(default), "'{}'".format(default)]) 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): def get_fieldtype_from_def(field_def):
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0)) fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0))
fieldtype = fieldtuple[0] fieldtype = fieldtuple[0]
@@ -69,4 +121,8 @@ def get_other_fields_meta(meta):
fields = dict(default_fields_map, **optional_fields_map) 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()] field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()]


return field_map
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)

+ 15
- 0
frappe/tests/test_patches.py Ver arquivo

@@ -15,3 +15,18 @@ class TestPatches(unittest.TestCase):
self.assertTrue(frappe.get_attr(patchmodule.split()[0] + ".execute")) self.assertTrue(frappe.get_attr(patchmodule.split()[0] + ".execute"))


frappe.flags.in_install = False 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))

+ 4
- 3
frappe/tests/test_twofactor.py Ver arquivo

@@ -222,9 +222,10 @@ def disable_2fa():
def toggle_2fa_all_role(state=None): def toggle_2fa_all_role(state=None):
'''Enable or disable 2fa for 'all' role on the system.''' '''Enable or disable 2fa for 'all' role on the system.'''
all_role = frappe.get_doc('Role','All') 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.two_factor_auth = cint(state)
all_role.save(ignore_permissions=True) all_role.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()


+ 8
- 7
frappe/tests/ui_test_helpers.py Ver arquivo

@@ -248,10 +248,11 @@ def create_topic_and_reply(web_page):


@frappe.whitelist() @frappe.whitelist()
def update_webform_to_multistep(): 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()

+ 1
- 0
frappe/translations/de.csv Ver arquivo

@@ -1393,6 +1393,7 @@ Is Spam,ist Spam,
Is Standard,Ist Standard, Is Standard,Ist Standard,
Is Submittable,Ist übertragbar, Is Submittable,Ist übertragbar,
Is Table,ist eine Tabelle, Is Table,ist eine Tabelle,
Is Template, Ist Vorlage,
Is Your Company Address,Ist Ihre Unternehmensadresse, 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.", 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, Item cannot be added to its own descendents,Artikel kann nicht zu seinen eigenen Abkömmlingen hinzugefügt werden,


+ 3
- 2
frappe/utils/__init__.py Ver arquivo

@@ -901,10 +901,11 @@ def dictify(arg):
def add_user_info(user, user_info): def add_user_info(user, user_info):
if user not in user_info: if user not in user_info:
info = frappe.db.get_value("User", 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( user_info[user] = frappe._dict(
fullname = info.full_name or user, fullname = info.full_name or user,
image = info.user_image, image = info.user_image,
name = user, name = user,
email = info.email
email = info.email,
time_zone = info.time_zone
) )

+ 9
- 3
frappe/utils/background_jobs.py Ver arquivo

@@ -20,11 +20,17 @@ from frappe.utils.redis_queue import RedisQueue
from frappe.utils.commands import log 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 default_timeout = 300
queue_timeout = { 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 redis_connection = None


+ 6
- 1
frappe/utils/boilerplate.py Ver arquivo

@@ -1,6 +1,11 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, os, re, git

import git
import os
import re

import frappe
from frappe.utils import touch_file, cstr from frappe.utils import touch_file, cstr


def make_boilerplate(dest, app_name, no_git=False): def make_boilerplate(dest, app_name, no_git=False):


+ 1
- 1
frappe/utils/redis_wrapper.py Ver arquivo

@@ -22,7 +22,7 @@ class RedisWrapper(redis.Redis):
if shared: if shared:
return key return key
if user: if user:
if user == True:
if user is True:
user = frappe.session.user user = frappe.session.user


key = "user:{0}:{1}".format(user, key) key = "user:{0}:{1}".format(user, key)


+ 2
- 2
frappe/website/doctype/web_form_field/web_form_field.json Ver arquivo

@@ -39,7 +39,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Fieldtype", "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", "fieldname": "label",
@@ -146,7 +146,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-30 12:02:25.422345",
"modified": "2022-01-24 20:43:25.422345",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Web Form Field", "name": "Web Form Field",


+ 3
- 3
frappe/website/workspace/website/website.json Ver arquivo

@@ -1,6 +1,6 @@
{ {
"charts": [], "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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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", "creation": "2020-03-02 14:13:51.089373",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@@ -232,7 +232,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-05 12:16:03.154033",
"modified": "2022-01-13 17:49:41.527194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Website", "name": "Website",
@@ -241,7 +241,7 @@
"public": 1, "public": 1,
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 28,
"sequence_id": 28.0,
"shortcuts": [ "shortcuts": [
{ {
"color": "Green", "color": "Green",


+ 4
- 4
frappe/www/printview.py Ver arquivo

@@ -2,9 +2,8 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe, os, copy, json, re 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.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cint, sanitize_html, strip_html from frappe.utils import cint, sanitize_html, strip_html
from frappe.utils.jinja_globals import is_rtl from frappe.utils.jinja_globals import is_rtl
@@ -251,8 +250,9 @@ def get_print_format(doctype, print_format):
frappe.DoesNotExistError) frappe.DoesNotExistError)


# server, find template # 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): if os.path.exists(path):
with open(path, "r") as pffile: with open(path, "r") as pffile:


+ 4
- 9
yarn.lock Ver arquivo

@@ -2774,15 +2774,10 @@ nan@^2.13.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== 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: native-request@^1.0.5:
version "1.0.8" version "1.0.8"


Carregando…
Cancelar
Salvar