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 = $( + `
+ ${frappe.utils.icon('add', 'sm')} +
` + ); + + this.add_new_button.appendTo(this.action_area); + this.add_new_button.on("click", () => { + frappe.set_route( + frappe.utils.generate_route({type: 'doctype', name: this.document_type, doc_view: 'New'}) + ); + }); + } + + setup_refresh_list_button() { + this.refresh_list = $( + `
+ ${frappe.utils.icon('refresh', 'sm')} +
` + ); + + this.refresh_list.appendTo(this.action_area); + this.refresh_list.on("click", () => { + this.body.empty(); + this.set_body(); + }); + } + + setup_filter_list_button() { + this.filter_list = $( + `
+ ${frappe.utils.icon('filter', 'sm')} +
` + ); + + this.filter_list.appendTo(this.action_area); + this.filter_list.on("click", () => this.setup_filter_dialog()); + } + + setup_filter(doctype) { + if (this.filter_group) { + this.filter_group.wrapper.empty(); + delete this.filter_group; + } + + this.filters = frappe.utils.get_filter_from_json(this.quick_list_filter, doctype); + + this.filter_group = new frappe.ui.FilterGroup({ + parent: this.dialog.get_field("filter_area").$wrapper, + doctype: doctype, + on_change: () => {}, + }); + + frappe.model.with_doctype(doctype, () => { + this.filter_group.add_filters_to_filter_group(this.filters); + this.dialog.set_df_property("filter_area", "hidden", false); + }); + } + + setup_filter_dialog() { + let fields = [ + { + fieldtype: "HTML", + fieldname: "filter_area" + } + ]; + let me = this; + this.dialog = new frappe.ui.Dialog({ + title: __("Set Filters for {0}", [this.document_type]), + fields: fields, + primary_action: function() { + let old_filter = me.quick_list_filter; + let filters = me.filter_group.get_filters(); + me.quick_list_filter = frappe.utils.get_filter_as_json(filters); + + this.hide(); + + if (old_filter != me.quick_list_filter) { + me.body.empty(); + me.set_body(); + } + }, + primary_action_label: "Set" + }); + + this.dialog.show(); + this.setup_filter(this.document_type); + } + + render_loading_state() { + this.body.empty(); + this.loading = $( + `
${__( + "Loading..." + )}
` + ); + this.loading.appendTo(this.body); + } + + render_no_data_state() { + this.loading = $( + `
${__( + "No Data..." + )}
` + ); + this.loading.appendTo(this.body); + } + + setup_quick_list_item(doc) { + const indicator = frappe.get_indicator(doc, this.document_type); + + let $quick_list_item = $(` +
+
+
+ ${strip_html(doc[this.title_field_name])} +
+
+ ${frappe.datetime.prettyDate(doc.modified)} +
+
+
+ `); + + if (indicator) { + $(` +
+ ${__(indicator[0])} +
+ `).appendTo($quick_list_item); + } + + $(`
${frappe.utils.icon('right', 'xs')}
`).appendTo($quick_list_item); + + $quick_list_item.click(() => { + frappe.set_route(`${frappe.utils.get_form_link(this.document_type, doc.name)}`); + }); + + return $quick_list_item; + } + + set_body() { + this.widget.addClass("quick-list-widget-box"); + + this.render_loading_state(); + + frappe.model.with_doctype(this.document_type, () => { + let fields = ['name']; + + // get name of title field + if (!this.title_field_name) { + let meta = frappe.get_meta(this.document_type); + this.title_field_name = meta && meta.title_field || 'name'; + } + + if (this.title_field_name && this.title_field_name != 'name') { + fields.push(this.title_field_name); + } + + // check doctype has status field + this.has_status_field = frappe.meta.has_field(this.document_type, 'status'); + + if (this.has_status_field) { + fields.push('status'); + fields.push('docstatus'); + } + + fields.push('modified'); + + let quick_list_filter = frappe.utils.get_filter_from_json(this.quick_list_filter); + + let args = { + method: 'frappe.desk.reportview.get', + args: { + doctype: this.document_type, + fields: fields, + filters: quick_list_filter, + order_by: 'modified desc', + start: 0, + page_length: 4 + } + }; + + frappe.call(args).then((r) => { + if (!r.message) return; + let data = r.message; + + this.body.empty(); + data = !Array.isArray(data) + ? frappe.utils.dict(data.keys, data.values) + : data; + + if (!data.length) { + this.render_no_data_state(); + return; + } + + this.quick_list = data.map(doc => this.setup_quick_list_item(doc)); + this.quick_list.forEach($quick_list_item => $quick_list_item.appendTo(this.body)); + }); + }); + } + + set_footer() { + if (!this.see_all_button) { + this.see_all_button = $(` +
See all
+ `).appendTo(this.footer); + + this.see_all_button.click(() => { + let filters = frappe.utils.get_filter_from_json(this.quick_list_filter); + if (filters) { + frappe.route_options = filters; + } + frappe.set_route(frappe.utils.generate_route({type: 'doctype', name: this.document_type})); + }); + } + } +} diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index a60903f7dc..f8c76e5287 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -36,7 +36,7 @@ export default class ShortcutWidget extends Widget { doc_view: this.doc_view }); - let filters = this.get_doctype_filter(); + let filters = frappe.utils.get_filter_from_json(this.stats_filter); if (this.type == "DocType" && filters) { frappe.route_options = filters; } @@ -49,7 +49,7 @@ export default class ShortcutWidget extends Widget { this.widget.addClass("shortcut-widget-box"); - let filters = this.get_doctype_filter(); + let filters = frappe.utils.get_filter_from_json(this.stats_filter); if (this.type == "DocType" && filters) { frappe.db .count(this.link_to, { @@ -59,15 +59,6 @@ export default class ShortcutWidget extends Widget { } } - get_doctype_filter() { - let count_filter = new Function(`return ${this.stats_filter}`)(); - if (count_filter) { - return count_filter; - } - - return null; - } - set_count(count) { const get_label = () => { if (this.format) { diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index bba29ffaf9..903b7b0359 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -75,13 +75,7 @@ class WidgetDialog { this.filters = []; - if (this.values && this.values.stats_filter) { - const filters_json = new Function(`return ${this.values.stats_filter}`)(); - this.filters = Object.keys(filters_json).map((filter) => { - let val = filters_json[filter]; - return [this.values.link_to, filter, val[0], val[1], false]; - }); - } + this.generate_filter_from_json(); this.filter_group = new frappe.ui.FilterGroup({ parent: this.dialog.get_field("filter_area").$wrapper, @@ -124,6 +118,74 @@ class ChartDialog extends WidgetDialog { return data; } } +class QuickListDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + return [ + { + fieldtype: "Link", + fieldname: "document_type", + label: "DocType", + options: "DocType", + reqd: 1, + onchange: () => { + this.document_type = this.dialog.get_value("document_type"); + this.document_type && this.setup_filter(this.document_type); + }, + get_query: () => { + return { + filters: { + issingle: 0, + istable: 0 + } + }; + } + }, + { + fieldtype: "Column Break", + fieldname: "column_break_4", + }, + { + fieldtype: "Data", + fieldname: "label", + label: "Label", + }, + { + fieldtype: "Section Break", + fieldname: "filter_section", + label: __('Add Filters'), + depends_on: 'eval: doc.document_type' + }, + { + fieldtype: "HTML", + fieldname: "filter_area_loading", + }, + { + fieldtype: "HTML", + fieldname: "filter_area" + }, + ]; + } + + generate_filter_from_json() { + if (this.values && this.values.quick_list_filter) { + this.filters = frappe.utils.get_filter_from_json(this.values.quick_list_filter, this.values.document_type); + } + } + + process_data(data) { + if (this.filter_group) { + let filters = this.filter_group.get_filters(); + data.quick_list_filter = frappe.utils.get_filter_as_json(filters); + } + + data.label = data.label ? data.label : data.document_type; + return data; + } +} class OnboardingDialog extends WidgetDialog { constructor(opts) { @@ -410,20 +472,17 @@ class ShortcutDialog extends WidgetDialog { }); } + generate_filter_from_json() { + if (this.values && this.values.stats_filter) { + this.filters = frappe.utils.get_filter_from_json(this.values.stats_filter, this.values.link_to); + } + } + process_data(data) { if (this.dialog.get_value("type") == "DocType" && this.filter_group) { let filters = this.filter_group.get_filters(); - let stats_filter = null; - - if (filters.length) { - stats_filter = {}; - filters.forEach((arr) => { - stats_filter[arr[1]] = [arr[2], arr[3]]; - }); - stats_filter = JSON.stringify(stats_filter); - } - data.stats_filter = stats_filter; + data.stats_filter = frappe.utils.get_filter_as_json(filters); } data.label = data.label @@ -579,7 +638,8 @@ export default function get_dialog_constructor(type) { shortcut: ShortcutDialog, number_card: NumberCardDialog, links: CardDialog, - onboarding: OnboardingDialog + onboarding: OnboardingDialog, + quick_list: QuickListDialog }; return widget_map[type] || WidgetDialog; diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index cb3fc58ea3..a0f0f72dec 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -5,6 +5,7 @@ import LinksWidget from "../widgets/links_widget"; import OnboardingWidget from "../widgets/onboarding_widget"; import NewWidget from "../widgets/new_widget"; import NumberCardWidget from "../widgets/number_card_widget"; +import QuickListWidget from "../widgets/quick_list_widget"; frappe.provide("frappe.widget"); @@ -15,6 +16,7 @@ frappe.widget.widget_factory = { links: LinksWidget, onboarding: OnboardingWidget, number_card: NumberCardWidget, + quick_list: QuickListWidget }; frappe.widget.make_widget = (opts) => { diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 5871ac3626..a89ebb3b50 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -218,6 +218,10 @@ --awesomplete-hover-bg: var(--control-bg); + // Button Colors + --btn-default-bg: var(--gray-100); + --btn-default-hover-bg: var(--gray-300); + // Other Colors --sidebar-select-color: var(--gray-200); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index 6e5ebdb694..c627d88f89 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -40,6 +40,10 @@ --toast-bg: var(--modal-bg); --popover-bg: var(--bg-color); + // Button Colors + --btn-default-bg: var(--gray-700); + --btn-default-hover-bg: var(--gray-500); + // Background Text Color Pairs --bg-blue: var(--blue-600); --bg-light-blue: var(--blue-400); diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 52157c888a..bd363cc49d 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -716,6 +716,75 @@ body { } } } + + &.quick-list-widget-box { + .list-loading-state, .list-no-data-state { + display: flex; + justify-content: center; + align-items: center; + height: 202px; + } + + .refresh-list, .filter-list, .add-new { + background-color: var(--btn-default-bg); + cursor: pointer; + + &:hover { + background-color: var(--btn-default-hover-bg); + } + } + + .widget-body { + display: flex; + flex-direction: column; + margin: 6px 0px; + min-height: 202px; + + .quick-list-item { + display: flex; + justify-content: space-between; + align-items: center; + margin: 3px 0px; + padding: 3px 6px; + border-radius: var(--border-radius); + cursor: pointer; + + &:hover { + background-color: var(--btn-default-bg); + } + + .left { + display: flex; + flex-direction: column; + flex: 1; + margin-left: 3px; + + .timestamp { + font-size: smaller; + } + } + + .status { + margin-left: 12px; + } + + .right-arrow { + margin-left: 6px; + } + } + } + + .widget-footer { + .see-all { + background-color: var(--btn-default-bg); + width: 100%; + + &:hover { + background-color: var(--btn-default-hover-bg); + } + } + } + } } .onboarding-success { @@ -1002,7 +1071,7 @@ body { transition: visibility 0s, opacity 0.5s ease-in-out; } - .link-item { + .link-item, .quick-list-item, .see-all { pointer-events: none; } }