feat: move tags from sidebar to nav

This commit is contained in:
Ejaaz Khan 2025-12-14 19:57:32 +05:30
parent 5974be9534
commit bfff0178bd
8 changed files with 155 additions and 145 deletions

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

@ -289,10 +289,6 @@ frappe.views.BaseList = class BaseList {
});
}
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);
@ -701,7 +697,7 @@ class FilterArea {
this.user_setting_fields =
frappe.get_user_settings(this.list_view.doctype)?.group_by_fields || [];
if (["assigned_to", "owner"].some((v) => this.user_setting_fields.includes(v))) {
if (["assigned_to", "owner", "tags"].some((v) => this.user_setting_fields.includes(v))) {
this.render_non_standard_fields_filter();
}
}
@ -823,6 +819,8 @@ class FilterArea {
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">
@ -848,6 +846,10 @@ class FilterArea {
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();
@ -863,11 +865,21 @@ class FilterArea {
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) {
let applied_filter = this.list_view.get_filter_value(
fieldname == "assigned_to" ? "_assign" : fieldname
);
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,
@ -896,7 +908,13 @@ class FilterArea {
typeof $target.data("value") === "string"
? decodeURIComponent($target.data("value").trim())
: $target.data("value");
fieldname = fieldname === "assigned_to" ? "_assign" : fieldname;
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;
@ -905,20 +923,6 @@ class FilterArea {
});
}
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);
}
render_dropdown_items(fields, fieldtype, $dropdown, applied_filter) {
let standard_html = `
<div class="dropdown-search mb-1">
@ -945,18 +949,6 @@ class FilterArea {
$dropdown.html(dropdown_html);
}
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>`
);
}
get_dropdown_html(field, fieldtype, applied = false) {
let label;
if (field.name == null) {
@ -985,18 +977,61 @@ class FilterArea {
</div>`;
}
set_dropdown_loading_state($dropdown) {
$dropdown.html(`<li>
<div class="empty-state group-by-loading">
${__("Loading...")}
</div>
</li>`);
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();
// remove filter of the current field
current_filters = current_filters.filter(
(f_arr) => !f_arr.includes(field === "assigned_to" ? "_assign" : field)
);
@ -1020,6 +1055,40 @@ class FilterArea {
});
}
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]);

View file

@ -28,44 +28,6 @@
</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">

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({
@ -2059,6 +2041,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
label: "Created By",
fieldname: "owner",
},
{
label: "Tags",
fieldname: "tags",
},
];
fields = fields.concat(default_fields_dict);

View file

@ -64,7 +64,7 @@ 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);
@ -145,39 +145,21 @@ 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");
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: () => {