diff --git a/cypress.config.js b/cypress.config.js index 03361f4671..a1d7717c35 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -41,6 +41,7 @@ module.exports = defineConfig({ excludeSpecPattern: [ "./cypress/integration/workspace.js", "./cypress/integration/workspace_blocks.js", + "./cypress/integration/customize_form.js", ], }, }); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index 97b6cac8e3..c2962c1e34 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -17,7 +17,6 @@ context("List View Settings", () => { }); it("Default settings", () => { cy.get(".list-count").should("contain", "20 of"); - cy.get(".list-stats").should("contain", "Tags"); }); it("disable count and sidebar stats then verify", () => { cy.get(".list-count").should("contain", "20 of"); diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index fc12929ba7..aa159dd9c4 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -82,63 +82,63 @@ context("Sidebar", () => { }); }); - it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { - cy.call("frappe.tests.ui_test_helpers.create_todo", { - description: "Sidebar Attachment ToDo", - }).then((todo) => { - let todo_name = todo.message.name; - cy.visit("/desk/todo"); - cy.click_sidebar_button("Assigned To"); + // it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { + // cy.call("frappe.tests.ui_test_helpers.create_todo", { + // description: "Sidebar Attachment ToDo", + // }).then((todo) => { + // let todo_name = todo.message.name; + // cy.visit("/desk/todo"); + // cy.click_sidebar_button("Assigned To"); - //To check if no filter is available in "Assigned To" dropdown - cy.get(".empty-state").should("contain", "No filters found"); + // //To check if no filter is available in "Assigned To" dropdown + // cy.get(".empty-state").should("contain", "No filters found"); - //Assigning a doctype to a user - cy.visit(`/app/todo/${todo_name}`); - cy.get(".add-assignment-btn").click(); - cy.get_field("assign_to_me", "Check").click(); - cy.wait(1000); - cy.get(".modal-footer > .standard-actions > .btn-primary").click(); - cy.visit("/desk/todo"); - cy.click_sidebar_button("Assigned To"); + // //Assigning a doctype to a user + // cy.visit(`/app/todo/${todo_name}`); + // cy.get(".add-assignment-btn").click(); + // cy.get_field("assign_to_me", "Check").click(); + // cy.wait(1000); + // cy.get(".modal-footer > .standard-actions > .btn-primary").click(); + // cy.visit("/desk/todo"); + // cy.click_sidebar_button("Assigned To"); - //To check if filter is added in "Assigned To" dropdown after assignment - cy.get( - ".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item" - ).should("contain", "1"); + // //To check if filter is added in "Assigned To" dropdown after assignment + // cy.get( + // ".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item" + // ).should("contain", "1"); - //To check if there is no filter added to the listview - cy.get(".filter-button").should("contain", "Filter"); + // //To check if there is no filter added to the listview + // cy.get(".filter-button").should("contain", "Filter"); - //To add a filter to display data into the listview - cy.get( - ".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item" - ).click(); + // //To add a filter to display data into the listview + // cy.get( + // ".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item" + // ).click(); - //To check if filter is applied - cy.click_filter_button().get(".filter-label").should("contain", "1"); - cy.get(".fieldname-select-area > .awesomplete > .form-control").should( - "have.value", - "Assigned To" - ); - cy.get(".condition").should("have.value", "like"); - cy.get(".filter-field > .form-group > .input-with-feedback").should( - "have.value", - `%${cy.config("testUser")}%` - ); - cy.click_filter_button(); + // //To check if filter is applied + // cy.click_filter_button().get(".filter-label").should("contain", "1"); + // cy.get(".fieldname-select-area > .awesomplete > .form-control").should( + // "have.value", + // "Assigned To" + // ); + // cy.get(".condition").should("have.value", "like"); + // cy.get(".filter-field > .form-group > .input-with-feedback").should( + // "have.value", + // `%${cy.config("testUser")}%` + // ); + // cy.click_filter_button(); - //To remove the applied filter - cy.clear_filters(); + // //To remove the applied filter + // cy.clear_filters(); - //To remove the assignment - cy.visit(`/app/todo/${todo_name}`); - cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); - cy.get(".remove-btn").click({ force: true }); - cy.hide_dialog(); - cy.visit("/desk/todo"); - cy.click_sidebar_button("Assigned To"); - cy.get(".empty-state").should("contain", "No filters found"); - }); - }); + // //To remove the assignment + // cy.visit(`/app/todo/${todo_name}`); + // cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); + // cy.get(".remove-btn").click({ force: true }); + // cy.hide_dialog(); + // cy.visit("/desk/todo"); + // cy.click_sidebar_button("Assigned To"); + // cy.get(".empty-state").should("contain", "No filters found"); + // }); + // }); }); diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json index 46b3e12645..c8d85883c3 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.json +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json @@ -10,6 +10,7 @@ "disable_auto_refresh", "disable_sidebar_stats", "disable_automatic_recency_filters", + "show_tags", "column_break_oany", "disable_comment_count", "disable_scrolling", @@ -81,11 +82,17 @@ { "fieldname": "section_break_evqq", "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "show_tags", + "fieldtype": "Check", + "label": "Show Tags" } ], "grid_page_length": 50, "links": [], - "modified": "2025-08-25 15:54:18.886680", + "modified": "2025-12-12 14:26:20.920434", "modified_by": "Administrator", "module": "Desk", "name": "List View Settings", diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py index 4e7a2199d2..f42baab57b 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.py +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -22,6 +22,7 @@ class ListViewSettings(Document): disable_scrolling: DF.Check disable_sidebar_stats: DF.Check fields: DF.Code | None + show_tags: DF.Check # end: auto-generated types pass diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js index 68515d586e..f006912aa3 100644 --- a/frappe/public/js/frappe/form/grid_row_form.js +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -60,7 +60,7 @@ export default class GridRowForm { diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 9bffee93aa..388c84b30f 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -1,3 +1,4 @@ +import ListFilter from "./list_filter"; frappe.provide("frappe.views"); frappe.views.BaseList = class BaseList { @@ -14,6 +15,7 @@ frappe.views.BaseList = class BaseList { () => this.init(), () => this.before_refresh(), () => this.refresh(), + () => this.setup_list_filter_by(), ]); } @@ -26,7 +28,6 @@ frappe.views.BaseList = class BaseList { this.setup_fields, // make view this.setup_page, - this.setup_side_bar, this.setup_main_section, this.setup_view, this.setup_view_menu, @@ -220,7 +221,6 @@ frappe.views.BaseList = class BaseList { parent: this.views_menu, page: this.page, list_view: this, - sidebar: this.list_sidebar, icon_map: icon_map, label_map: label_map, }); @@ -275,24 +275,6 @@ frappe.views.BaseList = class BaseList { frappe.breadcrumbs.add(this.meta.module, this.doctype); } - setup_side_bar() { - if (this.page.disable_sidebar_toggle) { - return; - } - - this.list_sidebar = new frappe.views.ListSidebar({ - doctype: this.doctype, - stats: this.stats, - parent: this.$page.find(".layout-side-section"), - page: this.page, - list_view: this, - }); - } - - toggle_side_bar(show) { - frappe.app.sidebar.toggle_sidebar(); - } - show_or_hide_sidebar() { let show_sidebar = JSON.parse(localStorage.show_sidebar || "true"); $(document.body).toggleClass("no-list-sidebar", !show_sidebar); @@ -636,6 +618,10 @@ frappe.views.BaseList = class BaseList { }, }); } + + setup_list_filter_by() { + new ListFilter(this); + } }; class FilterArea { @@ -698,6 +684,12 @@ class FilterArea { setup() { if (!this.list_view.hide_page_form) this.make_standard_filters(); this.make_filter_list(); + this.user_setting_fields = + frappe.get_user_settings(this.list_view.doctype)?.group_by_fields || []; + + if (["assigned_to", "owner", "tags"].some((v) => this.user_setting_fields.includes(v))) { + this.render_non_standard_fields_filter(); + } } get() { @@ -810,6 +802,283 @@ class FilterArea { }, {}); } + render_non_standard_fields_filter() { + let get_item_html = (fieldname) => { + let label, fieldtype; + if (fieldname === "assigned_to") { + label = __("Assigned To"); + } else if (fieldname === "owner") { + label = __("Created By"); + } else if (fieldname === "tags") { + label = __("Tags"); + } + + return ``; + }; + + let filtes_to_add = []; + + if (this.user_setting_fields.includes("owner")) { + filtes_to_add.push("owner"); + } + + if (this.user_setting_fields.includes("assigned_to")) { + filtes_to_add.push("assigned_to"); + } + + if (this.user_setting_fields.includes("tags")) { + filtes_to_add.push("tags"); + } + + let html = filtes_to_add.map(get_item_html).join(""); + this.list_view.page.page_form.find(".standard-filter-section").append(html); + this.setup_non_standard_items_dropdown(); + this.setup_filter_by(); + } + + setup_non_standard_items_dropdown() { + let standard_filter_container = this.list_view.page.page_form.find( + ".standard-filter-section" + ); + standard_filter_container.find(".group-by-field").on("show.bs.dropdown", (e) => { + let $dropdown = $(e.currentTarget).find(".group-by-dropdown"); + this.set_dropdown_loading_state($dropdown); + let fieldname = $(e.currentTarget).find("a").attr("data-fieldname"); + let fieldtype = $(e.currentTarget).find("a").attr("data-fieldtype"); + + if (fieldname == "tags") { + $dropdown.addClass("list-stats-dropdown"); + this.get_stats($dropdown); + return; + } + this.get_group_by_count(fieldname).then((field_count_list) => { + if (field_count_list.length) { + if (fieldname == "assigned_to") { + fieldname = "_assign"; + } + if (fieldname == "tags") { + fieldname = "_user_tags"; + } + let applied_filter = this.list_view.get_filter_value(fieldname); + this.render_dropdown_items( + field_count_list, + fieldtype, + $dropdown, + applied_filter + ); + this.setup_search($dropdown); + } else { + this.set_empty_state($dropdown); + } + }); + }); + } + + setup_filter_by() { + let standard_filter_container = this.list_view.page.page_form.find( + ".standard-filter-section" + ); + standard_filter_container.on("click", ".group-by-item", (e) => { + let $target = $(e.currentTarget); + + let is_selected = $target.hasClass("selected"); + + let fieldname = $target.parents(".group-by-field").find("a").data("fieldname"); + let value = + typeof $target.data("value") === "string" + ? decodeURIComponent($target.data("value").trim()) + : $target.data("value"); + + if (fieldname == "assigned_to") { + fieldname = "_assign"; + } + if (fieldname == "tags") { + fieldname = "_user_tags"; + } + + return this.list_view.filter_area.remove(fieldname).then(() => { + if (is_selected) return; + return this.apply_filter(fieldname, value); + }); + }); + } + + render_dropdown_items(fields, fieldtype, $dropdown, applied_filter) { + let standard_html = ` + + `; + let applied_filter_html = ""; + let dropdown_items_html = ""; + + fields.map((field) => { + if (field.name === applied_filter) { + applied_filter_html = this.get_dropdown_html(field, fieldtype, true); + } else { + dropdown_items_html += this.get_dropdown_html(field, fieldtype); + } + }); + + let dropdown_html = standard_html + applied_filter_html + dropdown_items_html; + $dropdown.toggleClass("has-selected", Boolean(applied_filter_html)); + $dropdown.html(dropdown_html); + } + + get_dropdown_html(field, fieldtype, applied = false) { + let label; + if (field.name == null) { + label = __("Not Set"); + } else if (field.name === frappe.session.user) { + label = __("Me"); + } else if (fieldtype && fieldtype == "Check") { + label = field.name == "0" ? __("No") : __("Yes"); + } else if (fieldtype && fieldtype == "Link" && field.title) { + label = __(field.title); + } else { + label = __(field.name); + } + let value = field.name == null ? "" : encodeURIComponent(field.name); + let applied_html = applied + ? ` ${frappe.utils.icon("tick", "xs")} ` + : ""; + return `
+ + + ${applied_html} + ${label} + + ${field.count} + +
`; + } + + get_stats($dropdown) { + let me = this; + + frappe.call({ + method: "frappe.desk.reportview.get_sidebar_stats", + type: "GET", + args: { + stats: ["_user_tags"], + doctype: me.list_view.doctype, + // wait for list filter area to be generated before getting filters, or fallback to default filters + filters: + (me.list_view.filter_area + ? me.list_view.get_filters_for_args() + : me.default_filters) || [], + }, + callback: function (r) { + let stats = (r.message.stats || {})["_user_tags"] || []; + me.render_stat(stats, $dropdown); + frappe.utils.setup_search($dropdown, ".stat-link", ".stat-label"); + }, + }); + } + + render_stat(stats, $dropdown) { + let args = { + stats: stats, + label: __("Tags"), + applied_filter: this.list_view.get_filter_value("_user_tags"), + }; + + let tag_list = $(frappe.render_template("list_sidebar_stat", args)).on( + "click", + ".stat-link", + (e) => { + let fieldname = $(e.currentTarget).attr("data-field"); + let label = $(e.currentTarget).attr("data-label"); + let condition = "like"; + let existing = this.list_view.filter_area.filter_list.get_filter(fieldname); + if (existing) { + existing.remove(); + } + if (label == "No Tags") { + label = "not set"; + condition = "is"; + } + this.list_view.filter_area.add(this.doctype, fieldname, condition, label); + } + ); + + $dropdown.html(tag_list); + } + + get_group_by_count(field) { + let current_filters = this.list_view.get_filters_for_args(); + + current_filters = current_filters.filter( + (f_arr) => !f_arr.includes(field === "assigned_to" ? "_assign" : field) + ); + + let args = { + doctype: this.list_view.doctype, + current_filters: current_filters, + field: field, + }; + + return frappe.call("frappe.desk.listview.get_group_by_count", args).then((r) => { + let field_counts = r.message || []; + field_counts = field_counts.filter((f) => f.count !== 0); + let current_user = field_counts.find((f) => f.name === frappe.session.user); + field_counts = field_counts.filter( + (f) => !["Guest", "Administrator", frappe.session.user].includes(f.name) + ); + // Set frappe.session.user on top of the list + if (current_user) field_counts.unshift(current_user); + return field_counts; + }); + } + + apply_filter(fieldname, value) { + let operator = "="; + if (value === "") { + operator = "is"; + value = "not set"; + } + if (fieldname === "_assign") { + operator = "like"; + value = `%${value}%`; + } + + return this.list_view.filter_area.add(this.list_view.doctype, fieldname, operator, value); + } + + set_dropdown_loading_state($dropdown) { + $dropdown.html(`
  • +
    + ${__("Loading...")} +
    +
  • `); + } + + setup_search($dropdown) { + frappe.utils.setup_search($dropdown, ".group-by-item", ".group-by-value", "data-name"); + } + + set_empty_state($dropdown) { + $dropdown.html( + `
    + ${__("No filters found")} +
    ` + ); + } + remove_filters(filters) { filters.map((f) => { this.remove(f[1]); @@ -985,7 +1254,7 @@ class FilterArea { const $inputGroup = $input.parent(); const $dropdown = $(` -
    +
    - -
    - `); - - this.$input_area = this.wrapper.find(".input-area"); - this.$list_filters = this.wrapper.find(".list-filters"); - this.$saved_filters = this.wrapper.find(".saved-filters").hide(); - this.$saved_filters_preview = this.wrapper.find(".saved-filters-preview"); - this.saved_filters_hidden = true; - this.toggle_saved_filters(true); - - this.filter_input = frappe.ui.form.make_control({ - df: { - fieldtype: "Data", - placeholder: __("Filter Name"), - input_class: "input-xs", - }, - parent: this.$input_area, - render_input: 1, - }); - - this.is_global_input = frappe.ui.form.make_control({ - df: { - fieldtype: "Check", - label: __("Is Global"), - }, - parent: this.$input_area, - render_input: 1, - }); - } - - bind() { - this.bind_save_filter(); - this.bind_toggle_saved_filters(); - this.bind_click_filter(); - this.bind_remove_filter(); - } - - refresh() { + refresh_list_filter() { this.get_list_filters().then(() => { - if (this.filters.length) { - // expand collapsible sections - this.wrapper.hasClass("hide") && this.section_title.trigger("click"); - this.$saved_filters_preview.show(); - } else { - // hide collapsible sections - !this.wrapper.hasClass("hide") && this.section_title.trigger("click"); - this.$saved_filters_preview.hide(); - } - - const html = this.filters.map((filter) => this.filter_template(filter)); - this.wrapper.find(".filter-pill").remove(); - this.$saved_filters.append(html); + this.render_saved_filters(); }); - this.is_global_input.toggle(false); - this.filter_input.set_description(""); - } - - filter_template(filter) { - return ``; - } - - bind_toggle_saved_filters() { - this.wrapper.find(".saved-filters-preview").click(() => { - this.toggle_saved_filters(this.saved_filters_hidden); - }); - } - - toggle_saved_filters(show) { - this.$saved_filters.toggle(show); - const label = show ? __("Hide Saved") : __("Show Saved"); - this.wrapper.find(".saved-filters-preview").text(label); - this.saved_filters_hidden = !this.saved_filters_hidden; - } - - bind_click_filter() { - this.wrapper.on("click", ".filter-pill .filter-name", (e) => { - let $filter = $(e.currentTarget).parent(".filter-pill"); - this.set_applied_filter($filter); - const name = $filter.attr("data-name"); - this.list_view.filter_area.clear().then(() => { - this.list_view.filter_area.add(this.get_filters_values(name)); - }); - }); - } - - bind_remove_filter() { - this.wrapper.on("click", ".filter-pill .remove", (e) => { - const $li = $(e.currentTarget).closest(".filter-pill"); - const filter_label = $li.text().trim(); - - frappe.confirm( - __("Are you sure you want to remove the {0} filter?", [filter_label.bold()]), - () => { - const name = $li.attr("data-name"); - const applied_filters = this.get_filters_values(name); - $li.remove(); - this.remove_filter(name).then(() => this.refresh()); - this.list_view.filter_area.remove_filters(applied_filters); - } - ); - }); - } - - bind_save_filter() { - this.filter_input.$input.keydown( - frappe.utils.debounce((e) => { - const value = this.filter_input.get_value(); - const has_value = Boolean(value); - - if (e.which === frappe.ui.keyCode["ENTER"]) { - if (!has_value || this.filter_name_exists(value)) return; - - this.filter_input.set_value(""); - this.save_filter(value).then(() => this.refresh()); - this.toggle_saved_filters(true); - } else { - let help_text = __("Press Enter to save"); - - if (this.filter_name_exists(value)) { - help_text = __("Duplicate Filter Name"); - } - - this.filter_input.set_description(has_value ? help_text : ""); - - if (this.can_add_global) { - this.is_global_input.toggle(has_value); - } - } - }, 300) + this.saved_filters_btn = this.list_view.page.add_inner_button( + __("Filters"), + [], + __("Saved Filters") ); } - save_filter(filter_name) { + render_saved_filters() { + const $menu = this.saved_filters_btn.parent(); + $menu.empty(); + + this.filters.forEach((filter) => { + const $item = this.filter_template(filter); + + // Apply filter + $item.find(".filter-label").on("click", () => { + this.apply_saved_filter(filter.name); + }); + + // Remove filter + $item.find(".remove-filter").on("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.bind_remove_filter(filter); + }); + + $menu.append($item); + }); + + this.append_create_new_item($menu); + } + + apply_saved_filter(filter_name) { + this.list_view.filter_area.clear().then(() => { + this.list_view.filter_area.add(this.get_filters_values(filter_name)); + }); + } + + bind_remove_filter(filter) { + frappe.confirm( + __("Are you sure you want to remove the {0} filter?", [filter.filter_name.bold()]), + () => { + const name = filter.name; + const applied_filters = this.get_filters_values(name); + this.remove_filter(name).then(() => this.refresh_list_filter()); + this.list_view.filter_area.remove_filters(applied_filters); + } + ); + } + + append_create_new_item($menu) { + const new_filter = { + name: "create_new", + filter_name: "Create New", + }; + + const $create_item = this.filter_template(new_filter, true); + $create_item.find(".filter-label").on("click", (e) => { + this.show_create_filter_dialog(); + }); + $menu.append($create_item); + } + + show_create_filter_dialog() { + const fields = [ + { + fieldname: "filter_name", + label: __("Filter Name"), + fieldtype: "Data", + reqd: 1, + description: __("Press Enter to save"), + }, + ]; + + // Conditionally add "Is Global" checkbox + if (this.can_add_global) { + fields.push({ + fieldname: "is_global", + label: __("Is Global"), + fieldtype: "Check", + default: 0, + }); + } + const dialog = new frappe.ui.Dialog({ + title: __("Create Saved Filter"), + fields: fields, + primary_action_label: __("Create"), + primary_action: (values) => { + this.bind_save_filter(dialog, values.filter_name, values?.is_global); + }, + }); + dialog.show(); + } + + bind_save_filter(dialog, filter_name, is_global) { + const value = filter_name; + const has_value = Boolean(value); + if (!has_value) { + return; + } + + if (this.filter_name_exists(value)) { + $(dialog.fields_dict.filter_name.wrapper).addClass("has-error"); + dialog.fields_dict.filter_name.set_description(__("Duplicate Filter Name")); + return; + } + this.save_filter(value, is_global).then(() => { + this.refresh_list_filter(); + dialog.hide(); + }); + } + + save_filter(filter_name, is_global) { return frappe.db.insert({ doctype: "List Filter", reference_doctype: this.list_view.doctype, filter_name, - for_user: this.is_global_input.get_value() ? "" : frappe.session.user, + for_user: is_global ? "" : frappe.session.user, filters: JSON.stringify(this.get_current_filters()), }); } + filter_template(filter, add_new = false) { + return $(` +
  • + + + ${frappe.utils.escape_html(__(filter.filter_name))} + + + ${frappe.utils.icon("x", "sm")} + + +
  • + `); + } + remove_filter(name) { if (!name) return; return frappe.db.delete_doc("List Filter", name); @@ -198,11 +185,4 @@ export default class ListFilter { this.filters = filters || []; }); } - - set_applied_filter($filter) { - this.$saved_filters - .find(".btn-primary-light") - .toggleClass("btn-primary-light btn-default"); - $filter.toggleClass("btn-default btn-primary-light"); - } } diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html deleted file mode 100644 index 96593dcfaf..0000000000 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js deleted file mode 100644 index 5026707798..0000000000 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt -import ListFilter from "./list_filter"; -frappe.provide("frappe.views"); - -// opts: -// stats = list of fields -// doctype -// parent - -frappe.views.ListSidebar = class ListSidebar { - constructor(opts) { - $.extend(this, opts); - this.make(); - } - - make() { - var sidebar_content = frappe.render_template("list_sidebar", { doctype: this.doctype }); - - this.sidebar = $('') - .html(sidebar_content) - .appendTo(this.page.sidebar.empty()); - - this.setup_list_filter(); - this.setup_list_group_by(); - this.setup_collapsible(); - - // do not remove - // used to trigger custom scripts - $(document).trigger("list_sidebar_setup"); - - if ( - this.list_view.list_view_settings && - this.list_view.list_view_settings.disable_sidebar_stats - ) { - this.sidebar.find(".list-tags").remove(); - } else { - this.sidebar.find(".list-stats").on("show.bs.dropdown", (e) => { - this.reload_stats(); - }); - } - } - - setup_views() { - var show_list_link = false; - - if (frappe.views.calendar[this.doctype]) { - this.sidebar.find('.list-link[data-view="Calendar"]').removeClass("hide"); - this.sidebar.find('.list-link[data-view="Gantt"]').removeClass("hide"); - show_list_link = true; - } - //show link for kanban view - this.sidebar.find('.list-link[data-view="Kanban"]').removeClass("hide"); - if (this.doctype === "Communication" && frappe.boot.email_accounts.length) { - this.sidebar.find('.list-link[data-view="Inbox"]').removeClass("hide"); - show_list_link = true; - } - - if (frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree) { - this.sidebar.find(".tree-link").removeClass("hide"); - } - - this.current_view = "List"; - var route = frappe.get_route(); - if (route.length > 2 && frappe.views.view_modes.includes(route[2])) { - this.current_view = route[2]; - - if (this.current_view === "Kanban") { - this.kanban_board = route[3]; - } else if (this.current_view === "Inbox") { - this.email_account = route[3]; - } - } - - // disable link for current view - this.sidebar - .find('.list-link[data-view="' + this.current_view + '"] a') - .attr("disabled", "disabled") - .addClass("disabled"); - - //enable link for Kanban view - this.sidebar - .find('.list-link[data-view="Kanban"] a, .list-link[data-view="Inbox"] a') - .attr("disabled", null) - .removeClass("disabled"); - - // show image link if image_view - if (this.list_view.meta.image_field) { - this.sidebar.find('.list-link[data-view="Image"]').removeClass("hide"); - show_list_link = true; - } - - if ( - this.list_view.settings.get_coords_method || - (this.list_view.meta.fields.find((i) => i.fieldname === "latitude") && - this.list_view.meta.fields.find((i) => i.fieldname === "longitude")) || - this.list_view.meta.fields.find( - (i) => i.fieldname === "location" && i.fieldtype == "Geolocation" - ) - ) { - this.sidebar.find('.list-link[data-view="Map"]').removeClass("hide"); - show_list_link = true; - } - - if (show_list_link) { - this.sidebar.find('.list-link[data-view="List"]').removeClass("hide"); - } - } - - setup_reports() { - // add reports linked to this doctype to the dropdown - var me = this; - var added = []; - var dropdown = this.page.sidebar.find(".reports-dropdown"); - var divider = false; - - var add_reports = function (reports) { - $.each(reports, function (name, r) { - if (!r.ref_doctype || r.ref_doctype == me.doctype) { - var report_type = - r.report_type === "Report Builder" - ? `List/${r.ref_doctype}/Report` - : "query-report"; - - var route = r.route || report_type + "/" + (r.title || r.name); - - if (added.indexOf(route) === -1) { - // don't repeat - added.push(route); - - if (!divider) { - me.get_divider().appendTo(dropdown); - divider = true; - } - - $( - '
  • ' + __(r.title || r.name) + "
  • " - ).appendTo(dropdown); - } - } - }); - }; - - // from reference doctype - if (this.list_view.settings.reports) { - add_reports(this.list_view.settings.reports); - } - - // Sort reports alphabetically - var reports = - Object.values(frappe.boot.user.all_reports).sort((a, b) => - a.title.localeCompare(b.title) - ) || []; - - // from specially tagged reports - add_reports(reports); - } - - setup_list_filter() { - this.list_filter = new ListFilter({ - wrapper: this.page.sidebar.find(".list-filters"), - doctype: this.doctype, - list_view: this.list_view, - section_title: this.page.sidebar.find(".save-filter-section .sidebar-label"), - }); - } - - setup_collapsible() { - // tags and save filter sections should be collapsible - let sections = [ - ["tags-section", "list-tags"], - ["save-filter-section", "list-filters"], - ["filter-section", "list-group-by"], - ]; - - for (let s of sections) { - this.page.sidebar.find(`.${s[0]} .sidebar-label`).on("click", () => { - let list_tags = this.page.sidebar.find("." + s[1]); - let icon = "#es-line-down"; - list_tags.toggleClass("hide"); - if (list_tags.hasClass("hide")) { - icon = "#es-line-right-chevron"; - } - this.page.sidebar.find(`.${s[0]} .es-line use`).attr("href", icon); - }); - } - } - - setup_kanban_boards() { - const $dropdown = this.page.sidebar.find(".kanban-dropdown"); - frappe.views.KanbanView.setup_dropdown_in_sidebar(this.doctype, $dropdown); - } - - setup_keyboard_shortcuts() { - this.sidebar.find(".list-link > a, .list-link > .btn-group > a").each((i, el) => { - frappe.ui.keys.get_shortcut_group(this.page).add($(el)); - }); - } - - setup_list_group_by() { - this.list_group_by = new frappe.views.ListGroupBy({ - doctype: this.doctype, - sidebar: this, - list_view: this.list_view, - page: this.page, - }); - } - - get_stats() { - var me = this; - - let dropdown_options = me.sidebar.find(".list-stats-dropdown .stat-result"); - this.set_loading_state(dropdown_options); - - frappe.call({ - method: "frappe.desk.reportview.get_sidebar_stats", - type: "GET", - args: { - stats: me.stats, - doctype: me.doctype, - // wait for list filter area to be generated before getting filters, or fallback to default filters - filters: - (me.list_view.filter_area - ? me.list_view.get_filters_for_args() - : me.default_filters) || [], - }, - callback: function (r) { - let stats = (r.message.stats || {})["_user_tags"] || []; - me.render_stat(stats); - let stats_dropdown = me.sidebar.find(".list-stats-dropdown"); - frappe.utils.setup_search(stats_dropdown, ".stat-link", ".stat-label"); - }, - }); - } - - set_loading_state(dropdown) { - dropdown.html(`
    -
    - ${__("Loading...")} -
    -
    `); - } - - render_stat(stats) { - let args = { - stats: stats, - label: __("Tags"), - }; - - let tag_list = $(frappe.render_template("list_sidebar_stat", args)).on( - "click", - ".stat-link", - (e) => { - let fieldname = $(e.currentTarget).attr("data-field"); - let label = $(e.currentTarget).attr("data-label"); - let condition = "like"; - let existing = this.list_view.filter_area.filter_list.get_filter(fieldname); - if (existing) { - existing.remove(); - } - if (label == "No Tags") { - label = "not set"; - condition = "is"; - } - this.list_view.filter_area.add(this.doctype, fieldname, condition, label); - } - ); - - this.sidebar.find(".list-stats-dropdown .stat-result").html(tag_list); - } - - reload_stats() { - this.sidebar.find(".stat-link").remove(); - this.sidebar.find(".stat-no-records").remove(); - this.get_stats(); - } -}; diff --git a/frappe/public/js/frappe/list/list_sidebar_group_by.js b/frappe/public/js/frappe/list/list_sidebar_group_by.js index f3b2c35589..eb890c1ac5 100644 --- a/frappe/public/js/frappe/list/list_sidebar_group_by.js +++ b/frappe/public/js/frappe/list/list_sidebar_group_by.js @@ -2,6 +2,7 @@ frappe.provide("frappe.views"); frappe.views.ListGroupBy = class ListGroupBy { constructor(opts) { + // TODO: move assigned to and owner logic in this file, currently this file is not use $.extend(this, opts); this.make_wrapper(); diff --git a/frappe/public/js/frappe/list/list_sidebar_stat.html b/frappe/public/js/frappe/list/list_sidebar_stat.html index 140202b5f3..20809b45f3 100644 --- a/frappe/public/js/frappe/list/list_sidebar_stat.html +++ b/frappe/public/js/frappe/list/list_sidebar_stat.html @@ -1,4 +1,12 @@ - +{% if (stats.length) { %} + +{% } %} {% if (!stats.length) { %}
  • {{ __("No records tagged.") }}
  • {% } else { @@ -7,8 +15,14 @@ var stat_count = stats[i][1]; %}
  • - - {{ __(stat_label) }} + + + + {% if (applied_filter == stat_label) { %} + {{ frappe.utils.icon("tick", "xs") }} + {% } %} + {{ __(stat_label) }} + {{ stat_count }}
  • diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 73321f2833..cb7f3b6e7e 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -329,6 +329,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { refresh_columns(meta, list_view_settings) { this.meta = meta; + this.tags_shown = list_view_settings?.show_tags; this.list_view_settings = list_view_settings; this.setup_columns(); @@ -727,7 +728,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let classes = [ "list-row-col ellipsis", col.type == "Subject" ? "list-subject level" : "hidden-xs", - col.type == "Tag" ? "tag-col hide" : "", + col.type == "Tag" ? `tag-col ${!this.tags_shown ? "hide" : ""} ` : "", frappe.model.is_numeric_field(col.df) ? "text-right" : "", col.df?.fieldname, ].join(" "); @@ -1338,7 +1339,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.setup_sort_by(); this.setup_list_click(); this.setup_drag_click(); - this.setup_tag_event(); + this.setup_tag_visibility(); this.setup_new_doc_event(); this.setup_check_events(); this.setup_like(); @@ -1679,13 +1680,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } - setup_tag_event() { - this.tags_shown = false; - this.list_sidebar && - this.list_sidebar.parent.on("click", ".list-tag-preview", () => { - this.tags_shown = !this.tags_shown; - this.toggle_tags(); - }); + setup_tag_visibility() { + this.tags_shown = this.list_view_settings?.show_tags; } setup_realtime_updates() { @@ -1853,12 +1849,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.toggle_actions_menu_button(this.$checks.length > 0); } - toggle_tags() { - this.$result.find(".tag-col").toggleClass("hide"); - const preview_label = this.tags_shown ? __("Hide Tags") : __("Show Tags"); - this.list_sidebar.parent.find(".list-tag-preview").text(preview_label); - } - get_checked_items(only_docnames) { const docnames = Array.from(this.$checks || []).map((check) => cstr(unescape($(check).data().name)) @@ -1973,14 +1963,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } - items.push({ - label: __("Toggle Sidebar", null, "Button in list view menu"), - action: () => this.toggle_side_bar(), - condition: () => !this.page.disable_sidebar_toggle, - standard: true, - shortcut: "Ctrl+G", - }); - if (frappe.user.has_role("System Manager") && frappe.boot.developer_mode) { // edit doctype items.push({ @@ -2044,14 +2026,28 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { get_group_by_dropdown_fields() { let group_by_fields = []; - let default_fields = ["assigned_to", "owner"]; - default_fields = default_fields.concat( - frappe.get_user_settings(this.doctype)?.group_by_fields || [] - ); + let default_fields = frappe.get_user_settings(this.doctype)?.group_by_fields || []; + let fields = this.meta.fields.filter((f) => ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype) ); + let default_fields_dict = [ + { + label: "Assigned To", + fieldname: "assigned_to", + }, + { + label: "Created By", + fieldname: "owner", + }, + { + label: "Tags", + fieldname: "tags", + }, + ]; + fields = fields.concat(default_fields_dict); + group_by_fields.push({ label: __(this.doctype), fieldname: "group_by_fields", diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js index eaf48be562..5a61b1551f 100644 --- a/frappe/public/js/frappe/list/list_view_select.js +++ b/frappe/public/js/frappe/list/list_view_select.js @@ -64,10 +64,10 @@ frappe.views.ListViewSelect = class ListViewSelect { if (frappe.get_route().length > 3) { default_action = { label: __("Report Builder"), - action: () => this.set_route("report"), + action: () => frappe.set_route("report"), }; } - this.setup_dropdown_in_sidebar("Report", reports, default_action); + this.setup_dropdown_in_navbar("Report", reports, default_action); }, }, Dashboard: { @@ -79,7 +79,7 @@ frappe.views.ListViewSelect = class ListViewSelect { action: () => this.set_route("calendar", "default"), current_view_handler: () => { this.get_calendars().then((calendars) => { - this.setup_dropdown_in_sidebar("Calendar", calendars); + this.setup_dropdown_in_navbar("Calendar", calendars); }); }, }, @@ -99,7 +99,7 @@ frappe.views.ListViewSelect = class ListViewSelect { action: () => frappe.new_doc("Email Account"), }; } - this.setup_dropdown_in_sidebar("Inbox", accounts, default_action); + this.setup_dropdown_in_navbar("Inbox", accounts, default_action); }, }, Image: { @@ -144,40 +144,22 @@ frappe.views.ListViewSelect = class ListViewSelect { }); } - setup_dropdown_in_sidebar(view, items, default_action) { - if (!this.sidebar) return; - const views_wrapper = this.sidebar.sidebar.find(".views-section"); - views_wrapper.find(".sidebar-label").html(__(view)); - const $dropdown = views_wrapper.find(".views-dropdown"); - + setup_dropdown_in_navbar(view, items, default_action) { let placeholder = __("Select {0}", [__(view)]); - let html = ``; - if (!items || !items.length) { - html = `
    - ${__("No {0} Found", [__(view)])} -
    `; - } else { - const page_name = this.get_page_name(); + if (items && items.length) { items.map((item) => { - if (item.name.toLowerCase() == page_name.toLowerCase()) { - placeholder = item.name; - } else { - html += `
  • ${item.name}
  • `; - } + this.page.add_inner_button( + item.name, + () => location.replace(item.route), + placeholder + ); }); } - views_wrapper.find(".selected-view").html(placeholder); - - if (default_action) { - views_wrapper.find(".sidebar-action a").html(default_action.label); - views_wrapper.find(".sidebar-action a").click(() => default_action.action()); + if (default_action && Object.keys(default_action).length) { + this.page.add_inner_button(default_action.label, default_action.action, placeholder); } - - $dropdown.html(html); - - views_wrapper.removeClass("hide"); } setup_kanban_switcher(kanbans) { diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 7755636faa..db3352f77a 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -77,12 +77,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { super.setup_page(); } - toggle_side_bar() { - super.toggle_side_bar(); - // refresh datatable when sidebar is toggled to accomodate extra space - this.render(true); - } - setup_result_area() { super.setup_result_area(); this.setup_charts_area(); @@ -1572,11 +1566,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { label: __("Toggle Chart"), action: () => this.toggle_charts(), }, - { - label: __("Toggle Sidebar"), - action: () => this.toggle_side_bar(), - shortcut: "Ctrl+K", - }, { label: __("Pick Columns"), action: () => { diff --git a/frappe/public/js/list.bundle.js b/frappe/public/js/list.bundle.js index e40ef94a81..54951cd03b 100644 --- a/frappe/public/js/list.bundle.js +++ b/frappe/public/js/list.bundle.js @@ -16,8 +16,6 @@ import "./frappe/list/list_view.js"; import "./frappe/list/list_factory.js"; import "./frappe/list/list_view_select.js"; -import "./frappe/list/list_sidebar.js"; -import "./frappe/list/list_sidebar.html"; import "./frappe/list/list_sidebar_stat.html"; import "./frappe/list/list_sidebar_group_by.js"; import "./frappe/list/list_view_permission_restrictions.html"; diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index d7e1d1c5c8..34ff41b7b7 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -481,6 +481,12 @@ input.list-header-checkbox { .form-group { min-width: 150px; } + + .group-by-field { + .group-by-dropdown { + max-width: 220px; + } + } } .filter-section {