From 78489cd700986816da0432c1e94c40f4ff4bfc92 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 13 May 2022 15:34:35 +0530 Subject: [PATCH] feat: Quick List Block for Workspace (#16887) --- cypress/integration/workspace_blocks.js | 150 +++++++++++ frappe/desk/desktop.py | 26 +- frappe/desk/doctype/workspace/workspace.json | 18 +- .../doctype/workspace_quick_list/__init__.py | 0 .../workspace_quick_list.json | 60 +++++ .../workspace_quick_list.py | 9 + frappe/model/sync.py | 1 + .../js/frappe/ui/filters/filter_list.js | 2 +- frappe/public/js/frappe/utils/utils.js | 42 +++ .../js/frappe/views/workspace/blocks/index.js | 2 + .../views/workspace/blocks/quick_list.js | 63 +++++ .../js/frappe/views/workspace/workspace.js | 7 + .../public/js/frappe/widgets/base_widget.js | 5 + .../js/frappe/widgets/quick_list_widget.js | 247 ++++++++++++++++++ .../js/frappe/widgets/shortcut_widget.js | 13 +- .../public/js/frappe/widgets/widget_dialog.js | 96 +++++-- .../public/js/frappe/widgets/widget_group.js | 2 + frappe/public/scss/common/css_variables.scss | 4 + frappe/public/scss/desk/dark.scss | 4 + frappe/public/scss/desk/desktop.scss | 71 ++++- 20 files changed, 786 insertions(+), 36 deletions(-) create mode 100644 cypress/integration/workspace_blocks.js create mode 100644 frappe/desk/doctype/workspace_quick_list/__init__.py create mode 100644 frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json create mode 100644 frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py create mode 100644 frappe/public/js/frappe/views/workspace/blocks/quick_list.js create mode 100644 frappe/public/js/frappe/widgets/quick_list_widget.js diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js new file mode 100644 index 0000000000..ba707499c9 --- /dev/null +++ b/cypress/integration/workspace_blocks.js @@ -0,0 +1,150 @@ +context('Workspace Blocks', () => { + before(() => { + cy.login(); + cy.visit('/app'); + }); + + it('Create Test Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page' + }).as('new_page'); + + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field('title', 'Test Block Page', 'Data'); + cy.fill_field('icon', 'edit', 'Icon'); + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + // check if sidebar item is added in private section + cy.get('.sidebar-item-container[item-name="Test Block Page"]').should('have.attr', 'item-public', '0'); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Block Page"]').should('have.attr', 'item-public', '0'); + + cy.wait('@new_page'); + }); + + it('Quick List Block', () => { + cy.create_records([ + { + doctype: 'ToDo', + description: 'Quick List ToDo 1', + status: 'Open' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 2', + status: 'Open' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 3', + status: 'Open' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 4', + status: 'Open' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 5', + status: 'Closed' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 6', + status: 'Closed' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 7', + status: 'Closed' + }, + { + doctype: 'ToDo', + description: 'Quick List ToDo 8', + status: 'Closed' + } + ]); + + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); + + // test quick list creation + cy.get('.ce-block').first().click({force: true}).type('{enter}'); + cy.get('.block-list-container .block-list-item').contains('Quick List').click(); + + cy.get_open_dialog().find('.modal-header').click(); + + cy.fill_field('document_type', 'ToDo', 'Link').blur(); + cy.fill_field('label', 'ToDo', 'Data').blur(); + + cy.get_open_dialog().find('.filter-edit-area').should('contain', 'No filters selected'); + cy.get_open_dialog().find('.filter-area .add-filter').click(); + + cy.get_open_dialog().find('.fieldname-select-area input').type('Status{enter}').blur(); + cy.get_open_dialog().find('select.input-with-feedback').select('Open'); + + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + + + cy.get('.codex-editor__redactor .ce-block'); + + cy.get('.ce-block .quick-list-widget-box').first().as('todo-quick-list'); + + cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Open'); + + // test filter-list + cy.get('@todo-quick-list').find('.widget-control .filter-list').click(); + + cy.get_open_dialog().find('select.input-with-feedback').select('Closed'); + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Closed'); + + + // test refresh-list + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.reportview.get' + }).as('refresh-list'); + + cy.get('@todo-quick-list').find('.widget-control .refresh-list').click(); + cy.wait('@refresh-list'); + + + // test add-new + cy.get('@todo-quick-list').find('.widget-control .add-new').click(); + cy.url().should('include', `/todo/new-todo-1`); + cy.go('back'); + + + // test quick-list-item + cy.get('@todo-quick-list').find('.quick-list-item .title') + .first() + .invoke('attr', 'title') + .then(title => { + cy.get('@todo-quick-list').find('.quick-list-item').contains(title).click(); + cy.get_field('description', 'Text Editor').should('contain', title); + }); + cy.go('back'); + + + // test see-all + cy.get('@todo-quick-list').find('.widget-footer .see-all').click(); + + cy.get('.standard-filter-section select[data-fieldname="status"]') + .invoke('val') + .should('eq', 'Open'); + cy.go('back'); + }); + +}); \ No newline at end of file diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 4c82fe8c73..ca0d1e2353 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -166,6 +166,8 @@ class Workspace: self.onboardings = {"items": self.get_onboardings()} + self.quick_lists = {"items": self.get_quick_lists()} + def _doctype_contains_a_record(self, name): exists = self.table_counts.get(name, False) @@ -284,6 +286,21 @@ class Workspace: return items + @handle_not_exist + def get_quick_lists(self): + items = [] + quick_lists = self.doc.quick_lists + + for item in quick_lists: + new_item = item.as_dict().copy() + + # Translate label + new_item["label"] = _(item.label) if item.label else _(item.document_type) + + items.append(new_item) + + return items + @handle_not_exist def get_onboardings(self): if self.onboarding_list: @@ -336,6 +353,7 @@ def get_desktop_page(page): "shortcuts": workspace.shortcuts, "cards": workspace.cards, "onboardings": workspace.onboardings, + "quick_lists": workspace.quick_lists, } except DoesNotExistError: frappe.log_error("Workspace Missing") @@ -452,6 +470,8 @@ def save_new_widget(doc, page, blocks, new_widgets): 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.quick_list: + doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists")) if widgets.card: doc.build_links_table_from_card(widgets.card) @@ -481,12 +501,12 @@ def save_new_widget(doc, page, blocks, new_widgets): def clean_up(original_page, blocks): page_widgets = {} - for wid in ["shortcut", "card", "chart"]: + for wid in ["shortcut", "card", "chart", "quick_list"]: # get list of widget's name from blocks page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid] - # shortcut & chart cleanup - for wid in ["shortcut", "chart"]: + # shortcut, chart & quick_list cleanup + for wid in ["shortcut", "chart", "quick_list"]: updated_widgets = [] original_page.get(wid + "s").reverse() diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index fa8b81f5fd..032de9de4e 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -26,6 +26,8 @@ "shortcuts", "tab_break_18", "links", + "quick_lists_tab", + "quick_lists", "roles_tab", "roles" ], @@ -155,11 +157,22 @@ "fieldname": "roles_tab", "fieldtype": "Tab Break", "label": "Roles" + }, + { + "fieldname": "quick_lists_tab", + "fieldtype": "Tab Break", + "label": "Quick Lists" + }, + { + "fieldname": "quick_lists", + "fieldtype": "Table", + "label": "Quick Lists", + "options": "Workspace Quick List" } ], "in_create": 1, "links": [], - "modified": "2022-01-27 12:06:13.111743", + "modified": "2022-05-12 13:00:03.925605", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", @@ -189,5 +202,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/frappe/desk/doctype/workspace_quick_list/__init__.py b/frappe/desk/doctype/workspace_quick_list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json new file mode 100644 index 0000000000..1542ebe03c --- /dev/null +++ b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2022-05-12 12:58:41.824496", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_1", + "label", + "section_break_4", + "quick_list_filter" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "quick_list_filter", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Quick List Filter", + "options": "JSON" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-05-12 13:48:40.617623", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Quick List", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py new file mode 100644 index 0000000000..9f26424115 --- /dev/null +++ b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkspaceQuickList(Document): + pass diff --git a/frappe/model/sync.py b/frappe/model/sync.py index a56d1f267f..4c535b2811 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -80,6 +80,7 @@ def sync_for(app_name, force=0, reset_permissions=False): "workspace_link", "workspace_chart", "workspace_shortcut", + "workspace_quick_list", "workspace", ]: files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json")) diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index b0a1eee707..5e13f086cb 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -324,7 +324,7 @@ frappe.ui.FilterGroup = class { } add_filters_to_filter_group(filters) { - if (filters.length) { + if (filters && filters.length) { this.toggle_empty_filters(false); filters.forEach((filter) => { this.add_filter(filter[0], filter[1], filter[2], filter[3]); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 4690012dc2..aa305a9ce7 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1486,5 +1486,47 @@ Object.assign(frappe.utils, { case "f": case "false": case "n": case "no": case "0": case null: return false; default: return string; } + }, + + get_filter_as_json(filters) { + // convert filter array to json + let filter = null; + + if (filters.length) { + filter = {}; + filters.forEach(arr => { + filter[arr[1]] = [arr[2], arr[3]]; + }); + filter = JSON.stringify(filter); + } + + return filter; + }, + + get_filter_from_json(filter_json, doctype) { + // convert json to filter array + if (filter_json) { + if (!filter_json.length) { + return []; + } + + const filters_json = new Function(`return ${filter_json}`)(); + if (!doctype) { + // e.g. return { + // priority: (2) ['=', 'Medium'], + // status: (2) ['=', 'Open'] + // } + return filters_json || []; + } + + // e.g. return [ + // ['ToDo', 'status', '=', 'Open', false], + // ['ToDo', 'priority', '=', 'Medium', false] + // ] + return Object.keys(filters_json).map(filter => { + let val = filters_json[filter]; + return [doctype, filter, val[0], val[1], false]; + }); + } } }); diff --git a/frappe/public/js/frappe/views/workspace/blocks/index.js b/frappe/public/js/frappe/views/workspace/blocks/index.js index 5fac17bd02..1e491ccc6b 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/index.js +++ b/frappe/public/js/frappe/views/workspace/blocks/index.js @@ -6,6 +6,7 @@ import Chart from "./chart"; import Shortcut from "./shortcut"; import Spacer from "./spacer"; import Onboarding from "./onboarding"; +import QuickList from "./quick_list"; // import tunes import HeaderSize from "./header_size"; @@ -20,6 +21,7 @@ frappe.workspace_block.blocks = { shortcut: Shortcut, spacer: Spacer, onboarding: Onboarding, + quick_list: QuickList, }; frappe.workspace_block.tunes = { diff --git a/frappe/public/js/frappe/views/workspace/blocks/quick_list.js b/frappe/public/js/frappe/views/workspace/blocks/quick_list.js new file mode 100644 index 0000000000..d70a9faa0a --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/quick_list.js @@ -0,0 +1,63 @@ +import Block from "./block.js"; +export default class QuickList extends Block { + static get toolbox() { + return { + title: 'Quick List', + icon: frappe.utils.icon('list', 'sm') + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.col = this.data.col ? this.data.col : "4"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + allow_resize: true, + min_width: 4, + max_widget_count: 2 + }; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('quick_list'); + + if (this.data && this.data.quick_list_name) { + let has_data = this.make('quick_list', this.data.quick_list_name); + if (!has_data) return; + } + + if (!this.readOnly) { + $(this.wrapper).find('.widget').addClass('quick_list edit-mode'); + this.add_settings_button(); + this.add_new_block_button(); + } + + return this.wrapper; + } + + validate(savedData) { + if (!savedData.quick_list_name) { + return false; + } + + return true; + } + + save() { + return { + quick_list_name: this.wrapper.getAttribute('quick_list_name'), + col: this.get_col(), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 91f419b258..31e4f27e1f 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -325,6 +325,7 @@ frappe.views.Workspace = class Workspace { this.editor.configuration.tools.shortcut.config.page_data = this.page_data; this.editor.configuration.tools.card.config.page_data = this.page_data; this.editor.configuration.tools.onboarding.config.page_data = this.page_data; + this.editor.configuration.tools.quick_list.config.page_data = this.page_data; this.editor.render({ blocks: this.content || [] }); }); } else { @@ -1121,6 +1122,12 @@ frappe.views.Workspace = class Workspace { page_data: this.page_data || [] } }, + quick_list: { + class: this.blocks['quick_list'], + config: { + page_data: this.page_data || [] + } + }, spacer: this.blocks['spacer'], HeaderSize: frappe.workspace_block.tunes['header_size'], }; diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index 45d4926904..2a9be94774 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -11,6 +11,7 @@ export default class Widget { this.set_actions(); this.set_body(); this.setup_events(); + this.set_footer(); } get_config() { @@ -196,4 +197,8 @@ export default class Widget { set_body() { // } + + set_footer() { + // + } } diff --git a/frappe/public/js/frappe/widgets/quick_list_widget.js b/frappe/public/js/frappe/widgets/quick_list_widget.js new file mode 100644 index 0000000000..ce81333147 --- /dev/null +++ b/frappe/public/js/frappe/widgets/quick_list_widget.js @@ -0,0 +1,247 @@ +import Widget from "./base_widget.js"; + +frappe.provide("frappe.utils"); + +export default class QuickListWidget extends Widget { + constructor(opts) { + opts.shadow = true; + super(opts); + } + + get_config() { + return { + document_type: this.document_type, + label: this.label, + quick_list_filter: this.quick_list_filter + }; + } + + set_actions() { + if (this.in_customize_mode) return; + + this.setup_add_new_button(); + this.setup_refresh_list_button(); + this.setup_filter_list_button(); + } + + setup_add_new_button() { + this.add_new_button = $( + `