Merge pull request #35223 from iamejaaz/remove-list-sidebar

feat: remove list sidebar
This commit is contained in:
Ejaaz Khan 2025-12-15 22:49:08 +05:30 committed by GitHub
commit 329243f646
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 545 additions and 669 deletions

View file

@ -41,6 +41,7 @@ module.exports = defineConfig({
excludeSpecPattern: [
"./cypress/integration/workspace.js",
"./cypress/integration/workspace_blocks.js",
"./cypress/integration/customize_form.js",
],
},
});

View file

@ -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");

View file

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

View file

@ -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",

View file

@ -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

View file

@ -60,7 +60,7 @@ export default class GridRowForm {
<button class="btn btn-secondary btn-sm pull-right grid-insert-row-below hidden-xs">
${__("Insert Below")}</button>
<button class="btn btn-danger btn-sm pull-right grid-delete-row">
${frappe.utils.icon("delete")}
${frappe.utils.icon("trash-2")} ${__("Delete")}
</button>
</span>
</div>

View file

@ -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 `<div class="group-by-field list-link form-group frappe-control input-max-width">
<a class="btn btn-default btn-sm flex justify-between list-sidebar-button w-100" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
data-label="${label}" data-fieldname="${fieldname}" data-fieldtype="${fieldtype}"
href="#" onclick="return false;">
<span class="ellipsis">${__(label)}</span>
<span>${frappe.utils.icon("select", "xs")}</span>
</a>
<ul class="dropdown-menu group-by-dropdown" role="menu">
</ul>
</div>`;
};
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 = `
<div class="dropdown-search mb-1">
<input type="text"
placeholder="${__("Search")}"
data-element="search"
class="dropdown-search-input form-control input-xs"
>
</div>
`;
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
? `<span class="applied"> ${frappe.utils.icon("tick", "xs")} </span>`
: "";
return `<div class="group-by-item ${applied ? "selected" : ""}" data-value="${value}">
<a class="dropdown-item flex justify-between" href="#" onclick="return false;">
<span class="group-by-value ellipsis" data-name="${field.name}">
${applied_html}
${label}
</span>
<span class="group-by-count">${field.count}</span>
</a>
</div>`;
}
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(`<li>
<div class="empty-state group-by-loading">
${__("Loading...")}
</div>
</li>`);
}
setup_search($dropdown) {
frappe.utils.setup_search($dropdown, ".group-by-item", ".group-by-value", "data-name");
}
set_empty_state($dropdown) {
$dropdown.html(
`<div class="empty-state group-by-empty">
${__("No filters found")}
</div>`
);
}
remove_filters(filters) {
filters.map((f) => {
this.remove(f[1]);
@ -985,7 +1254,7 @@ class FilterArea {
const $inputGroup = $input.parent();
const $dropdown = $(`
<div class="input-group-btn">
<div class="input-group-btn mr-0">
<button type="button"
class="btn btn-default match-type-dropdown-btn"
data-toggle="dropdown"

View file

@ -27,7 +27,7 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
frappe.provide("frappe.views.list_view." + doctype);
const hide_sidebar = view_class.no_sidebar || !frappe.boot.desk_settings.list_sidebar;
const hide_sidebar = true;
frappe.views.list_view[me.page_name] = new view_class({
doctype: doctype,

View file

@ -1,169 +1,156 @@
frappe.provide("frappe.ui");
export default class ListFilter {
constructor({ wrapper, doctype }) {
constructor(list_view) {
this.list_view = list_view;
Object.assign(this, arguments[0]);
this.can_add_global = frappe.user.has_role(["System Manager", "Administrator"]);
this.filters = [];
this.make();
this.bind();
this.refresh();
this.refresh_list_filter();
}
make() {
// init dom
this.wrapper.html(`
<div class="input-area"></div>
<div class="sidebar-action">
<a class="saved-filters-preview">${__("Show Saved")}</a>
</div>
<div class="saved-filters"></div>
`);
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 `<div class="list-link filter-pill list-sidebar-button btn btn-default" data-name="${
filter.name
}">
<a class="ellipsis filter-name">${filter.filter_name}</a>
<a class="remove">${frappe.utils.icon("close")}</a>
</div>`;
}
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 $(`
<li class="saved-filter-item" data-name="${filter.name}">
<a class="dropdown-item d-flex justify-content-between align-items-center">
<span class="filter-label">
${frappe.utils.escape_html(__(filter.filter_name))}
</span>
<span class="remove-filter ${add_new ? "d-none" : ""} ">
${frappe.utils.icon("x", "sm")}
</span>
</a>
</li>
`);
}
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");
}
}

View file

@ -1,90 +0,0 @@
<div class="sidebar-section user-actions hide">
</div>
<div class="sidebar-section views-section hide">
<div class="sidebar-label">
</div>
<div class="current-view">
<div class="list-link">
<a class="btn btn-default btn-sm list-sidebar-button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
href="#"
>
<span class="selected-view ellipsis">
</span>
<span>
<svg class="icon icon-xs">
<use href="#icon-select"></use>
</svg>
</span>
</a>
<ul class="dropdown-menu views-dropdown" role="menu">
</ul>
</div>
<div class="sidebar-action">
<a class="view-action"></a>
</div>
</div>
</div>
<div class="sidebar-section filter-section">
<div class="sidebar-label">
<svg class="es-icon es-line icon-xs" aria-hidden="true">
<use class="" href="#es-line-down"></use>
</svg>
<span>{{ __("Filter By") }}</span>
</div>
<div class="list-group-by">
</div>
</div>
<div class="sidebar-section tags-section">
<div class="sidebar-label">
<svg class="es-icon es-line icon-xs" aria-hidden="true">
<use class="" href="#es-line-right-chevron"></use>
</svg>
<span>{{ __("Tags") }}</span>
</div>
<div class="list-tags hide">
<div class="list-stats list-link">
<a
class="btn btn-default btn-sm list-sidebar-button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
href="#"
>
<span>{{ __("Tags") }}</span>
<span>
<svg class="icon icon-xs">
<use href="#icon-select"></use>
</svg>
</span>
</a>
<ul class="dropdown-menu list-stats-dropdown" role="menu">
<div class="dropdown-search">
<input type="text" placeholder={{__("Search") }} data-element="search" class="form-control input-xs">
</div>
<div class="stat-result">
</div>
</ul>
</div>
<div class="sidebar-action show-tags">
<a class="list-tag-preview">{{ __("Show Tags") }}</a>
</div>
</div>
</div>
<div class="sidebar-section save-filter-section">
<div class="sidebar-label">
<svg class="es-icon es-line icon-xs" aria-hidden="true">
<use class="" href="#es-line-right-chevron"></use>
</svg>
<span>{{ __("Saved Filters") }}</span>
</div>
<div class="list-filters list-link hide"></div>
</div>

View file

@ -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 = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>')
.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;
}
$(
'<li><a href="#' + route + '">' + __(r.title || r.name) + "</a></li>"
).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(`<div>
<div class="empty-state">
${__("Loading...")}
</div>
</div>`);
}
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();
}
};

View file

@ -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();

View file

@ -1,4 +1,12 @@
{% if (stats.length) { %}
<div class="dropdown-search mb-1">
<input type="text"
placeholder="${__("Search")}"
data-element="search"
class="dropdown-search-input form-control input-xs"
>
</div>
{% } %}
{% if (!stats.length) { %}
<li class="stat-no-records text-muted">{{ __("No records tagged.") }}</li>
{% } else {
@ -7,8 +15,14 @@
var stat_count = stats[i][1];
%}
<li>
<a class="stat-link dropdown-item" data-label="{{ stat_label %}" data-field="_user_tags" href="#" onclick="return false;">
<span class="stat-label">{{ __(stat_label) }}</span>
<a class="stat-link dropdown-item flex justify-between group-by-item" data-label="{{ stat_label }}" data-value="{{ stat_label }}" data-field="_user_tags" href="#" onclick="return false;">
<span class="stat-label">
{% if (applied_filter == stat_label) { %}
<span class="applied"> {{ frappe.utils.icon("tick", "xs") }} </span>
{% } %}
{{ __(stat_label) }}
</span>
<span>{{ stat_count }}</span>
</a>
</li>

View file

@ -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",

View file

@ -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 = `<div class="empty-state">
${__("No {0} Found", [__(view)])}
</div>`;
} 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 += `<li><a class="dropdown-item" href="${item.route}">${item.name}</a></li>`;
}
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) {

View file

@ -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: () => {

View file

@ -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";

View file

@ -481,6 +481,12 @@ input.list-header-checkbox {
.form-group {
min-width: 150px;
}
.group-by-field {
.group-by-dropdown {
max-width: 220px;
}
}
}
.filter-section {