seitime-frappe/frappe/public/js/frappe/list/list_view.js

1841 lines
44 KiB
JavaScript

import BulkOperations from "./bulk_operations";
import ListSettings from "./list_settings";
frappe.provide("frappe.views");
frappe.views.ListView = class ListView extends frappe.views.BaseList {
static load_last_view() {
const route = frappe.get_route();
const doctype = route[1];
if (route.length === 2) {
const user_settings = frappe.get_user_settings(doctype);
const last_view = user_settings.last_view;
frappe.set_route(
"list",
frappe.router.doctype_layout || doctype,
frappe.views.is_valid(last_view) ? last_view.toLowerCase() : "list"
);
return true;
}
return false;
}
constructor(opts) {
super(opts);
this.show();
}
has_permissions() {
const can_read = frappe.perm.has_perm(this.doctype, 0, "read");
return can_read;
}
show() {
this.parent.disable_scroll_to_top = true;
if (!this.has_permissions()) {
frappe.set_route('');
frappe.msgprint(__("Not permitted to view {0}", [this.doctype]));
return;
}
super.show();
}
get view_name() {
return "List";
}
get view_user_settings() {
return this.user_settings[this.view_name] || {};
}
setup_defaults() {
super.setup_defaults();
this.view = "List";
// initialize with saved order by
this.sort_by = this.view_user_settings.sort_by || "modified";
this.sort_order = this.view_user_settings.sort_order || "desc";
// set filters from user_settings or list_settings
if (
this.view_user_settings.filters &&
this.view_user_settings.filters.length
) {
// Priority 1: user_settings
const saved_filters = this.view_user_settings.filters;
this.filters = this.validate_filters(saved_filters);
} else {
// Priority 2: filters in listview_settings
this.filters = (this.settings.filters || []).map((f) => {
if (f.length === 3) {
f = [this.doctype, f[0], f[1], f[2]];
}
return f;
});
}
// build menu items
this.menu_items = this.menu_items.concat(this.get_menu_items());
if (
this.view_user_settings.filters &&
this.view_user_settings.filters.length
) {
// Priority 1: saved filters
const saved_filters = this.view_user_settings.filters;
this.filters = this.validate_filters(saved_filters);
} else {
// Priority 2: filters in listview_settings
this.filters = (this.settings.filters || []).map((f) => {
if (f.length === 3) {
f = [this.doctype, f[0], f[1], f[2]];
}
return f;
});
}
if (this.view_name == 'List') this.toggle_paging = true;
this.patch_refresh_and_load_lib();
return this.get_list_view_settings();
}
get_list_view_settings() {
return frappe
.call("frappe.desk.listview.get_list_settings", {
doctype: this.doctype,
})
.then((doc) => (this.list_view_settings = doc.message || {}));
}
on_sort_change(sort_by, sort_order) {
this.sort_by = sort_by;
this.sort_order = sort_order;
super.on_sort_change();
}
validate_filters(filters) {
let valid_fields = this.meta.fields.map(df => df.fieldname);
valid_fields = valid_fields.concat(frappe.model.std_fields_list);
return filters
.filter((f) => valid_fields.includes(f[1]))
.uniqBy((f) => f[1]);
}
setup_page() {
this.parent.list_view = this;
super.setup_page();
}
setup_page_head() {
super.setup_page_head();
this.set_primary_action();
this.set_actions_menu_items();
}
set_actions_menu_items() {
this.actions_menu_items = this.get_actions_menu_items();
this.workflow_action_menu_items = this.get_workflow_action_menu_items();
this.workflow_action_items = {};
const actions = this.actions_menu_items.concat(
this.workflow_action_menu_items
);
actions.map((item) => {
const $item = this.page.add_actions_menu_item(
item.label,
item.action,
item.standard
);
if (item.class) {
$item.addClass(item.class);
}
if (item.is_workflow_action && $item) {
// can be used to dynamically show or hide action
this.workflow_action_items[item.name] = $item;
}
});
}
show_restricted_list_indicator_if_applicable() {
const match_rules_list = frappe.perm.get_match_rules(this.doctype);
if (match_rules_list.length) {
this.restricted_list = $(
`<button class="btn btn-default btn-xs restricted-button flex align-center">
${frappe.utils.icon('lock', 'xs')}
</button>`
)
.click(() => this.show_restrictions(match_rules_list))
.appendTo(this.page.page_form);
}
}
show_restrictions(match_rules_list = []) {
frappe.msgprint(
frappe.render_template("list_view_permission_restrictions", {
condition_list: match_rules_list,
}),
__("Restrictions")
);
}
set_fields() {
let fields = [].concat(
frappe.model.std_fields_list,
this.get_fields_in_list_view(),
[this.meta.title_field, this.meta.image_field],
this.settings.add_fields || [],
this.meta.track_seen ? "_seen" : null,
this.sort_by,
"enabled",
"disabled",
"color"
);
fields.forEach((f) => this._add_field(f));
this.fields.forEach((f) => {
const df = frappe.meta.get_docfield(f[1], f[0]);
if (
df &&
df.fieldtype === "Currency" &&
df.options &&
!df.options.includes(":")
) {
this._add_field(df.options);
}
});
}
patch_refresh_and_load_lib() {
// throttle refresh for 1s
this.refresh = this.refresh.bind(this);
this.refresh = frappe.utils.throttle(this.refresh, 1000);
this.load_lib = new Promise((resolve) => {
if (this.required_libs) {
frappe.require(this.required_libs, resolve);
} else {
resolve();
}
});
// call refresh every 5 minutes
const interval = 5 * 60 * 1000;
setInterval(() => {
// don't call if route is different
if (frappe.get_route_str() === this.page_name) {
this.refresh();
}
}, interval);
}
set_primary_action() {
if (this.can_create) {
this.page.set_primary_action(
`${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`,
() => {
if (this.settings.primary_action) {
this.settings.primary_action();
} else {
this.make_new_doc();
}
},
"add"
);
} else {
this.page.clear_primary_action();
}
}
make_new_doc() {
const doctype = this.doctype;
const options = {};
this.filter_area.get().forEach((f) => {
if (f[2] === "=" && frappe.model.is_non_std_field(f[1])) {
options[f[1]] = f[3];
}
});
frappe.new_doc(doctype, options);
}
setup_view() {
this.setup_columns();
this.render_header();
this.render_skeleton();
this.setup_events();
this.settings.onload && this.settings.onload(this);
this.show_restricted_list_indicator_if_applicable();
}
refresh_columns(meta, list_view_settings) {
this.meta = meta;
this.list_view_settings = list_view_settings;
this.setup_columns();
this.refresh(true);
}
refresh(refresh_header=false) {
super.refresh().then(() => {
this.render_header(refresh_header);
});
}
setup_freeze_area() {
this.$freeze = $(
`<div class="freeze flex justify-center align-center text-muted">${__(
"Loading"
)}...</div>`
).hide();
this.$result.append(this.$freeze);
}
setup_columns() {
// setup columns for list view
this.columns = [];
const get_df = frappe.meta.get_docfield.bind(null, this.doctype);
// 1st column: title_field or name
if (this.meta.title_field) {
this.columns.push({
type: "Subject",
df: get_df(this.meta.title_field),
});
} else {
this.columns.push({
type: "Subject",
df: {
label: __("Name"),
fieldname: "name",
},
});
}
this.columns.push({
type: "Tag"
});
// 2nd column: Status indicator
if (frappe.has_indicator(this.doctype)) {
// indicator
this.columns.push({
type: "Status",
});
}
const fields_in_list_view = this.get_fields_in_list_view();
// Add rest from in_list_view docfields
this.columns = this.columns.concat(
fields_in_list_view
.filter((df) => {
if (
frappe.has_indicator(this.doctype) &&
df.fieldname === "status"
) {
return false;
}
if (!df.in_list_view) {
return false;
}
return df.fieldname !== this.meta.title_field;
})
.map((df) => ({
type: "Field",
df,
}))
);
if (this.list_view_settings.fields) {
this.columns = this.reorder_listview_fields();
}
// limit max to 8 columns if no total_fields is set in List View Settings
// Screen with low density no of columns 4
// Screen with medium density no of columns 6
// Screen with high density no of columns 8
let total_fields = 6;
if (window.innerWidth <= 1366) {
total_fields = 4;
} else if (window.innerWidth >= 1920) {
total_fields = 8;
}
this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields);
if (
!this.settings.hide_name_column &&
this.meta.title_field !== 'name'
) {
this.columns.push({
type: "Field",
df: {
label: __("Name"),
fieldname: "name",
},
});
}
}
reorder_listview_fields() {
let fields_order = [];
let fields = JSON.parse(this.list_view_settings.fields);
//title and tags field is fixed
fields_order.push(this.columns[0]);
fields_order.push(this.columns[1]);
this.columns.splice(0, 2);
for (let fld in fields) {
for (let col in this.columns) {
let field = fields[fld];
let column = this.columns[col];
if (column.type == "Status" && field.fieldname == "status_field") {
fields_order.push(column);
break;
} else if (column.type == "Field" && field.fieldname === column.df.fieldname) {
fields_order.push(column);
break;
}
}
}
return fields_order;
}
get_documentation_link() {
if (this.meta.documentation) {
return `<a href="${
this.meta.documentation
}" target="blank" class="meta-description small text-muted">Need Help?</a>`;
}
return "";
}
get_no_result_message() {
let help_link = this.get_documentation_link();
let filters = this.filter_area.get();
let no_result_message = filters.length
? __("No {0} found", [__(this.doctype)])
: __("You haven't created a {0} yet", [__(this.doctype)]);
let new_button_label = filters.length
? __("Create a new {0}", [__(this.doctype)])
: __("Create your first {0}", [__(this.doctype)]);
let empty_state_image =
this.settings.empty_state_image ||
"/assets/frappe/images/ui-states/list-empty-state.svg";
const new_button = this.can_create
? `<p><button class="btn btn-primary btn-sm btn-new-doc hidden-xs">
${new_button_label}
</button> <button class="btn btn-primary btn-new-doc visible-xs">${__('Create New')}</button></p>`
: "";
return `<div class="msg-box no-border">
<div>
<img src="${empty_state_image}" alt="Generic Empty State" class="null-state">
</div>
<p>${no_result_message}</p>
${new_button}
${help_link}
</div>`;
}
freeze() {
if (this.list_view_settings && !this.list_view_settings.disable_count) {
this.$result
.find(".list-count")
.html(`<span>${__("Refreshing")}...</span>`);
}
}
get_args() {
const args = super.get_args();
return Object.assign(args, {
with_comment_count: true,
});
}
before_refresh() {
if (frappe.route_options) {
this.filters = this.parse_filters_from_route_options();
frappe.route_options = null;
if (this.filters.length > 0) {
return this.filter_area
.clear(false)
.then(() => this.filter_area.set(this.filters));
}
}
return Promise.resolve();
}
parse_filters_from_settings() {
return (this.settings.filters || []).map((f) => {
if (f.length === 3) {
f = [this.doctype, f[0], f[1], f[2]];
}
return f;
});
}
toggle_result_area() {
super.toggle_result_area();
this.toggle_actions_menu_button(
this.$result.find(".list-row-check:checked").length > 0
);
}
toggle_actions_menu_button(toggle) {
if (toggle) {
this.page.show_actions_menu();
this.page.clear_primary_action();
this.toggle_workflow_actions();
} else {
this.page.hide_actions_menu();
this.set_primary_action();
}
}
render_header() {
if (this.$result.find(".list-row-head").length === 0) {
// append header once
this.$result.prepend(this.get_header_html());
}
}
render_skeleton() {
const $row = this.get_list_row_html_skeleton(
'<div><input type="checkbox" /></div>'
);
this.$result.append($row);
}
before_render() {
this.settings.before_render && this.settings.before_render();
frappe.model.user_settings.save(
this.doctype,
"last_view",
this.view_name
);
this.save_view_user_settings({
filters: this.filter_area.get(),
sort_by: this.sort_selector.sort_by,
sort_order: this.sort_selector.sort_order,
});
this.toggle_paging && this.$paging_area.toggle(false);
}
after_render() {
this.$no_result.html(`
<div class="no-result text-muted flex justify-center align-center">
${this.get_no_result_message()}
</div>
`);
this.setup_new_doc_event();
this.list_sidebar && this.list_sidebar.reload_stats();
this.toggle_paging && this.$paging_area.toggle(true);
}
render() {
this.render_list();
this.on_row_checked();
this.render_count();
}
render_list() {
// clear rows
this.$result.find(".list-row-container").remove();
if (this.data.length > 0) {
// append rows
this.$result.append(
this.data
.map((doc, i) => {
doc._idx = i;
return this.get_list_row_html(doc);
})
.join("")
);
}
}
render_count() {
if (!this.list_view_settings.disable_count) {
this.get_count_str().then((str) => {
this.$result.find(".list-count").html(`<span>${str}</span>`);
});
}
}
get_header_html() {
if (!this.columns) {
return;
}
const subject_field = this.columns[0].df;
let subject_html = `
<input class="level-item list-check-all hidden-xs" type="checkbox"
title="${__("Select All")}">
<span class="level-item list-liked-by-me">
<span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span>
</span>
<span class="level-item">${__(subject_field.label)}</span>
`;
const $columns = this.columns
.map(col => {
let classes = [
"list-row-col ellipsis",
col.type == "Subject" ? "list-subject level" : "hidden-xs",
col.type == "Tag" ? "tag-col hide": "",
frappe.model.is_numeric_field(col.df) ? "text-right" : "",
].join(" ");
return `
<div class="${classes}">
${col.type === "Subject" ? subject_html : `
<span>${__((col.df && col.df.label) || col.type)}</span>`}
</div>
`;
})
.join("");
return this.get_header_html_skeleton(
$columns,
'<span class="list-count"></span>'
);
}
get_header_html_skeleton(left = "", right = "") {
return `
<header class="level list-row-head text-muted">
<div class="level-left list-header-subject">
${left}
</div>
<div class="level-left checkbox-actions">
<div class="level list-subject">
<input class="level-item list-check-all hidden-xs" type="checkbox"
title="${__("Select All")}">
<span class="level-item list-header-meta"></span>
</div>
</div>
<div class="level-right">
${right}
</div>
</header>
`;
}
get_left_html(doc) {
return this.columns
.map((col) => this.get_column_html(col, doc))
.join("");
}
get_right_html(doc) {
return this.get_meta_html(doc);
}
get_list_row_html(doc) {
return this.get_list_row_html_skeleton(
this.get_left_html(doc),
this.get_right_html(doc)
);
}
get_list_row_html_skeleton(left = "", right = "") {
return `
<div class="list-row-container" tabindex="1">
<div class="level list-row">
<div class="level-left ellipsis">
${left}
</div>
<div class="level-right text-muted ellipsis">
${right}
</div>
</div>
</div>
`;
}
get_column_html(col, doc) {
if (col.type === "Status") {
return `
<div class="list-row-col hidden-xs ellipsis">
${this.get_indicator_html(doc)}
</div>
`;
}
if (col.type === "Tag") {
const tags_display_class = !this.tags_shown ? 'hide' : '';
let tags_html = doc._user_tags ? this.get_tags_html(doc._user_tags) : '<div class="tags-empty">-</div>';
return `
<div class="list-row-col tag-col ${tags_display_class} hidden-xs ellipsis">
${tags_html}
</div>
`;
}
const df = col.df || {};
const label = df.label;
const fieldname = df.fieldname;
const value = doc[fieldname] || "";
const format = () => {
if (df.fieldtype === "Code") {
return value;
} else if (df.fieldtype === "Percent") {
return `<div class="progress level" style="margin: 0px;">
<div class="progress-bar progress-bar-success" role="progressbar"
aria-valuenow="${value}"
aria-valuemin="0" aria-valuemax="100" style="width: ${Math.round(value)}%;">
</div>
</div>`;
} else {
return frappe.format(value, df, null, doc);
}
};
const field_html = () => {
let html;
let _value;
// listview_setting formatter
if (
this.settings.formatters &&
this.settings.formatters[fieldname]
) {
_value = this.settings.formatters[fieldname](value, df, doc);
} else {
let strip_html_required =
df.fieldtype == "Text Editor" ||
(df.fetch_from &&
["Text", "Small Text"].includes(df.fieldtype));
if (strip_html_required) {
_value = strip_html(value);
} else {
_value =
typeof value === "string"
? frappe.utils.escape_html(value)
: value;
}
}
if (df.fieldtype === "Image") {
html = df.options ? `<img src="${doc[df.options]}"
style="max-height: 30px; max-width: 100%;">`
: `<div class="missing-image small">
<span class="octicon octicon-circle-slash"></span>
</div>`;
} else if (df.fieldtype === "Select") {
html = `<span class="filterable indicator-pill ${frappe.utils.guess_colour(
_value
)} ellipsis"
data-filter="${fieldname},=,${value}">
<span class="ellipsis"> ${__(_value)} </span>
</span>`;
} else if (df.fieldtype === "Link") {
html = `<a class="filterable ellipsis"
data-filter="${fieldname},=,${value}">
${_value}
</a>`;
} else if (
["Text Editor", "Text", "Small Text", "HTML Editor"].includes(
df.fieldtype
)
) {
html = `<span class="ellipsis">
${_value}
</span>`;
} else {
html = `<a class="filterable ellipsis"
data-filter="${fieldname},=,${value}">
${format()}
</a>`;
}
return `<span class="ellipsis"
title="${__(label)}: ${escape(_value)}">
${html}
</span>`;
};
const class_map = {
Subject: "list-subject level",
Field: "hidden-xs",
};
const css_class = [
"list-row-col ellipsis",
class_map[col.type],
frappe.model.is_numeric_field(df) ? "text-right" : "",
].join(" ");
const html_map = {
Subject: this.get_subject_html(doc),
Field: field_html(),
};
const column_html = html_map[col.type];
return `
<div class="${css_class}">
${column_html}
</div>
`;
}
get_tags_html(user_tags) {
let get_tag_html = tag => {
if (tag) {
return `<div class="tag-pill ellipsis" title="${tag}">${tag}</div>`;
}
};
return user_tags.split(',').slice(1, 3).map(get_tag_html).join('');
}
get_meta_html(doc) {
let html = "";
let settings_button = null;
if (this.settings.button && this.settings.button.show(doc)) {
settings_button = `
<span class="list-actions">
<button class="btn btn-action btn-default btn-xs"
data-name="${doc.name}" data-idx="${doc._idx}"
title="${this.settings.button.get_description(doc)}">
${this.settings.button.get_label(doc)}
</button>
</span>
`;
}
const modified = comment_when(doc.modified, true);
let assigned_to = `<div class="list-assignments">
<span class="avatar avatar-small">
<span class="avatar-empty"></span>
</div>`;
let assigned_users = JSON.parse(doc._assign || "[]");
if (assigned_users.length) {
assigned_to = `<div class="list-assignments">
${frappe.avatar_group(assigned_users, 3, { filterable: true })[0].outerHTML}
</div>`;
}
const comment_count = `<span class="${
!doc._comment_count ? "text-extra-muted" : ""
} comment-count">
${frappe.utils.icon('small-message')}
${doc._comment_count > 99 ? "99+" : doc._comment_count}
</span>`;
html += `
<div class="level-item list-row-activity hidden-xs">
<div class="hidden-md hidden-xs">
${settings_button || assigned_to}
</div>
${modified}
${comment_count}
</div>
<div class="level-item visible-xs text-right">
${this.get_indicator_dot(doc)}
</div>
`;
return html;
}
get_count_str() {
let current_count = this.data.length;
let count_without_children = this.data.uniqBy((d) => d.name).length;
return frappe.db.count(this.doctype, {
filters: this.get_filters_for_args()
}).then(total_count => {
this.total_count = total_count || current_count;
let str = __('{0} of {1}', [current_count, this.total_count]);
if (count_without_children !== current_count) {
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
}
return str;
});
}
get_form_link(doc) {
if (this.settings.get_form_link) {
return this.settings.get_form_link(doc);
}
const docname = doc.name.match(/[%'"\s]/)
? encodeURIComponent(doc.name)
: doc.name;
return `/app/${frappe.router.slug(frappe.router.doctype_layout || this.doctype)}/${docname}`;
}
get_seen_class(doc) {
return JSON.parse(doc._seen || '[]').includes(frappe.session.user)
? ''
: 'bold';
}
get_like_html(doc) {
const liked_by = JSON.parse(doc._liked_by || "[]");
let heart_class = liked_by.includes(frappe.session.user)
? "liked-by liked"
: "not-liked";
return `<span
class="like-action ${heart_class}"
data-name="${doc.name}" data-doctype="${this.doctype}"
data-liked-by="${encodeURI(doc._liked_by) || "[]"}"
title="${liked_by.map(u => frappe.user_info(u).fullname).join(', ')}">
${frappe.utils.icon('heart', 'sm', 'like-icon')}
</span>
<span class="likes-count">
${liked_by.length > 99 ? __("99") + "+" : __(liked_by.length || "")}
</span>`;
}
get_subject_html(doc) {
let subject_field = this.columns[0].df;
let value = doc[subject_field.fieldname] || doc.name;
let subject = strip_html(value.toString());
let escaped_subject = frappe.utils.escape_html(subject);
const seen = this.get_seen_class(doc);
let subject_html = `
<input class="level-item list-row-checkbox hidden-xs" type="checkbox"
data-name="${escape(doc.name)}">
<span class="level-item" style="margin-bottom: 1px;">
${this.get_like_html(doc)}
</span>
<span class="level-item ${seen} ellipsis" title="${escaped_subject}">
<a class="ellipsis"
href="${this.get_form_link(doc)}"
title="${escaped_subject}"
data-doctype="${this.doctype}"
data-name="${doc.name}">
${subject}
</a>
</span>
`;
return subject_html;
}
get_indicator_html(doc) {
const indicator = frappe.get_indicator(doc, this.doctype);
if (indicator) {
return `<span class="indicator-pill ${indicator[1]} filterable ellipsis"
data-filter='${indicator[2]}'>
<span class="ellipsis"> ${__(indicator[0])}</span>
<span>`;
}
return "";
}
get_indicator_dot(doc) {
const indicator = frappe.get_indicator(doc, this.doctype);
if (!indicator) return "";
return `<span class='indicator ${indicator[1]}' title='${__(
indicator[0]
)}'></span>`;
}
setup_events() {
this.setup_filterable();
this.setup_list_click();
this.setup_tag_event();
this.setup_new_doc_event();
this.setup_check_events();
this.setup_like();
this.setup_realtime_updates();
this.setup_action_handler();
this.setup_keyboard_navigation();
}
setup_keyboard_navigation() {
let focus_first_row = () => {
this.$result.find(".list-row-container:first").focus();
};
let focus_next = () => {
$(document.activeElement)
.next()
.focus();
};
let focus_prev = () => {
$(document.activeElement)
.prev()
.focus();
};
let list_row_focused = () => {
return $(document.activeElement).is(".list-row-container");
};
let check_row = ($row) => {
let $input = $row.find("input[type=checkbox]");
$input.click();
};
let get_list_row_if_focused = () =>
list_row_focused() ? $(document.activeElement) : null;
let is_current_page = () => this.page.wrapper.is(":visible");
let is_input_focused = () => $(document.activeElement).is("input");
let handle_navigation = (direction) => {
if (!is_current_page() || is_input_focused()) return false;
let $list_row = get_list_row_if_focused();
if ($list_row) {
direction === "down" ? focus_next() : focus_prev();
} else {
focus_first_row();
}
};
frappe.ui.keys.add_shortcut({
shortcut: "down",
action: () => handle_navigation("down"),
description: __("Navigate list down"),
page: this.page,
});
frappe.ui.keys.add_shortcut({
shortcut: "up",
action: () => handle_navigation("up"),
description: __("Navigate list up"),
page: this.page,
});
frappe.ui.keys.add_shortcut({
shortcut: "shift+down",
action: () => {
if (!is_current_page() || is_input_focused()) return false;
let $list_row = get_list_row_if_focused();
check_row($list_row);
focus_next();
},
description: __("Select multiple list items"),
page: this.page,
});
frappe.ui.keys.add_shortcut({
shortcut: "shift+up",
action: () => {
if (!is_current_page() || is_input_focused()) return false;
let $list_row = get_list_row_if_focused();
check_row($list_row);
focus_prev();
},
description: __("Select multiple list items"),
page: this.page,
});
frappe.ui.keys.add_shortcut({
shortcut: "enter",
action: () => {
let $list_row = get_list_row_if_focused();
if ($list_row) {
$list_row.find("a[data-name]")[0].click();
return true;
}
return false;
},
description: __("Open list item"),
page: this.page,
});
frappe.ui.keys.add_shortcut({
shortcut: "space",
action: () => {
let $list_row = get_list_row_if_focused();
if ($list_row) {
check_row($list_row);
return true;
}
return false;
},
description: __("Select list item"),
page: this.page,
});
}
setup_filterable() {
// filterable events
this.$result.on("click", ".filterable", (e) => {
if (e.metaKey || e.ctrlKey) return;
e.stopPropagation();
const $this = $(e.currentTarget);
const filters = $this.attr("data-filter").split("|");
const filters_to_apply = filters.map((f) => {
f = f.split(",");
if (f[2] === "Today") {
f[2] = frappe.datetime.get_today();
} else if (f[2] == "User") {
f[2] = frappe.session.user;
}
this.filter_area.remove(f[0]);
return [this.doctype, f[0], f[1], f.slice(2).join(",")];
});
this.filter_area.add(filters_to_apply);
});
}
setup_list_click() {
this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
const $target = $(e.target);
// tick checkbox if Ctrl/Meta key is pressed
if (e.ctrlKey || (e.metaKey && !$target.is("a"))) {
const $list_row = $(e.currentTarget);
const $check = $list_row.find(".list-row-checkbox");
$check.prop("checked", !$check.prop("checked"));
e.preventDefault();
this.on_row_checked();
return;
}
// don't open form when checkbox, like, filterable are clicked
if (
$target.hasClass("filterable") ||
$target.hasClass("icon-heart") ||
$target.is(":checkbox")
) {
e.stopPropagation();
return;
}
// link, let the event be handled via set_route
if ($target.is("a")) { return; }
// clicked on the row, open form
const $row = $(e.currentTarget);
const link = $row.find(".list-subject a").get(0);
if (link) {
frappe.set_route(link.pathname);
return false;
}
});
}
setup_action_handler() {
this.$result.on("click", ".btn-action", (e) => {
const $button = $(e.currentTarget);
const doc = this.data[$button.attr("data-idx")];
this.settings.button.action(doc);
e.stopPropagation();
return false;
});
}
setup_check_events() {
this.$result.on("change", "input[type=checkbox]", (e) => {
const $target = $(e.currentTarget);
if ($target.is(".list-header-subject .list-check-all")) {
const $check = this.$result.find(
".checkbox-actions .list-check-all"
);
$check.prop("checked", $target.prop("checked"));
$check.trigger("change");
} else if ($target.is(".checkbox-actions .list-check-all")) {
const $check = this.$result.find(
".list-header-subject .list-check-all"
);
$check.prop("checked", $target.prop("checked"));
this.$result
.find(".list-row-checkbox")
.prop("checked", $target.prop("checked"));
} else if ($target.attr('data-parent')) {
this.$result
.find(`.${$target.attr('data-parent')}`)
.find('.list-row-checkbox')
.prop("checked", $target.prop("checked"));
}
this.on_row_checked();
});
this.$result.on("click", ".list-row-checkbox", (e) => {
const $target = $(e.currentTarget);
// shift select checkboxes
if (
e.shiftKey &&
this.$checkbox_cursor &&
!$target.is(this.$checkbox_cursor)
) {
const name_1 = this.$checkbox_cursor.data().name;
const name_2 = $target.data().name;
const index_1 = this.data.findIndex((d) => d.name === name_1);
const index_2 = this.data.findIndex((d) => d.name === name_2);
let [min_index, max_index] = [index_1, index_2];
if (min_index > max_index) {
[min_index, max_index] = [max_index, min_index];
}
let docnames = this.data
.slice(min_index + 1, max_index)
.map((d) => d.name);
const selector = docnames
.map((name) => `.list-row-checkbox[data-name="${name}"]`)
.join(",");
this.$result.find(selector).prop("checked", true);
}
this.$checkbox_cursor = $target;
});
}
setup_like() {
this.$result.on("click", ".like-action", frappe.ui.click_toggle_like);
this.$result.on("click", ".list-liked-by-me", (e) => {
const $this = $(e.currentTarget);
$this.toggleClass("active");
if ($this.hasClass("active")) {
this.filter_area.add(
this.doctype,
"_liked_by",
"like",
"%" + frappe.session.user + "%"
);
} else {
this.filter_area.remove("_liked_by");
}
});
}
setup_new_doc_event() {
this.$no_result.find(".btn-new-doc").click(() => {
if (this.settings.primary_action) {
this.settings.primary_action();
} else {
this.make_new_doc();
}
});
}
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_realtime_updates() {
if (
this.list_view_settings &&
this.list_view_settings.disable_auto_refresh
) {
return;
}
frappe.realtime.on("list_update", (data) => {
if (this.filter_area.is_being_edited()) {
return;
}
const { doctype, name } = data;
if (doctype !== this.doctype) return;
// filters to get only the doc with this name
const call_args = this.get_call_args();
call_args.args.filters.push([this.doctype, "name", "=", name]);
call_args.args.start = 0;
frappe.call(call_args).then(({ message }) => {
if (!message) return;
const data = frappe.utils.dict(message.keys, message.values);
if (!(data && data.length)) {
// this doc was changed and should not be visible
// in the listview according to filters applied
// let's remove it manually
this.data = this.data.filter((d) => d.name !== name);
this.render_list();
return;
}
const datum = data[0];
const index = this.data.findIndex((d) => d.name === datum.name);
if (index === -1) {
// append new data
this.data.push(datum);
} else {
// update this data in place
this.data[index] = datum;
}
this.data.sort((a, b) => {
const a_value = a[this.sort_by] || "";
const b_value = b[this.sort_by] || "";
let return_value = 0;
if (a_value > b_value) {
return_value = 1;
}
if (b_value > a_value) {
return_value = -1;
}
if (this.sort_order === "desc") {
return_value = -return_value;
}
return return_value;
});
this.toggle_result_area();
this.render_list();
if (this.$checks && this.$checks.length) {
this.set_rows_as_checked();
}
});
});
}
set_rows_as_checked() {
$.each(this.$checks, (i, el) => {
let docname = $(el).attr("data-name");
this.$result
.find(`.list-row-checkbox[data-name='${docname}']`)
.prop("checked", true);
});
this.on_row_checked();
}
on_row_checked() {
this.$list_head_subject =
this.$list_head_subject ||
this.$result.find("header .list-header-subject");
this.$checkbox_actions =
this.$checkbox_actions ||
this.$result.find("header .checkbox-actions");
this.$checks = this.$result.find(".list-row-checkbox:checked");
this.$list_head_subject.toggle(this.$checks.length === 0);
this.$checkbox_actions.toggle(this.$checks.length > 0);
if (this.$checks.length === 0) {
this.$list_head_subject
.find(".list-check-all")
.prop("checked", false);
} else {
this.$checkbox_actions
.find(".list-header-meta")
.html(__("{0} items selected", [this.$checks.length]));
this.$checkbox_actions.show();
this.$list_head_subject.hide();
}
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))
);
if (only_docnames) return docnames;
return this.data.filter((d) => docnames.includes(d.name));
}
save_view_user_settings(obj) {
return frappe.model.user_settings.save(
this.doctype,
this.view_name,
obj
);
}
on_update() {}
get_share_url() {
const query_params = this.get_filters_for_args()
.map((filter) => {
filter[3] = encodeURIComponent(filter[3]);
if (filter[2] === "=") {
return `${filter[1]}=${filter[3]}`;
}
return [
filter[1],
"=",
JSON.stringify([filter[2], filter[3]]),
].join("");
})
.join("&");
let full_url = window.location.href;
if (query_params) {
full_url += "?" + query_params;
}
return full_url;
}
share_url() {
const d = new frappe.ui.Dialog({
title: __("Share URL"),
fields: [
{
fieldtype: "Code",
fieldname: "url",
label: "URL",
default: this.get_share_url(),
read_only: 1,
},
],
});
d.show();
}
get_menu_items() {
const doctype = this.doctype;
const items = [];
if (frappe.model.can_import(doctype)) {
items.push({
label: __("Import"),
action: () =>
frappe.set_route("list", "data-import", {
reference_doctype: doctype,
}),
standard: true,
});
}
if (frappe.model.can_set_user_permissions(doctype)) {
items.push({
label: __("User Permissions"),
action: () =>
frappe.set_route("list", "user-permission", {
allow: doctype,
}),
standard: true,
});
}
if (frappe.user_roles.includes("System Manager")) {
items.push({
label: __("Role Permissions Manager"),
action: () =>
frappe.set_route("permission-manager", {
doctype,
}),
standard: true,
});
items.push({
label: __("Customize"),
action: () => {
if (!this.meta) return;
if (this.meta.custom) {
frappe.set_route("form", "doctype", doctype);
} else if (!this.meta.custom) {
frappe.set_route("form", "customize-form", {
doc_type: doctype,
});
}
},
standard: true,
shortcut: "Ctrl+J",
});
}
items.push({
label: __("Toggle Sidebar"),
action: () => this.toggle_side_bar(),
condition: () => !this.hide_sidebar,
standard: true,
shortcut: "Ctrl+K",
});
items.push({
label: __("Share URL"),
action: () => this.share_url(),
standard: true,
shortcut: "Ctrl+L",
});
if (
frappe.user.has_role("System Manager") &&
frappe.boot.developer_mode === 1
) {
// edit doctype
items.push({
label: __("Edit DocType"),
action: () => frappe.set_route("form", "doctype", doctype),
standard: true,
});
}
if (frappe.user.has_role("System Manager")) {
items.push({
label: __("List Settings"),
action: () => this.show_list_settings(),
standard: true,
});
}
return items;
}
show_list_settings() {
frappe.model.with_doctype(this.doctype, () => {
new ListSettings({
listview: this,
doctype: this.doctype,
settings: this.list_view_settings,
meta: frappe.get_meta(this.doctype)
});
});
}
get_workflow_action_menu_items() {
const workflow_actions = [];
if (frappe.model.has_workflow(this.doctype)) {
const actions = frappe.workflow.get_all_transition_actions(
this.doctype
);
actions.forEach((action) => {
workflow_actions.push({
label: __(action),
name: action,
action: () => {
frappe.xcall(
"frappe.model.workflow.bulk_workflow_approval",
{
docnames: this.get_checked_items(true),
doctype: this.doctype,
action: action,
}
);
},
is_workflow_action: true,
});
});
}
return workflow_actions;
}
toggle_workflow_actions() {
if (!frappe.model.has_workflow(this.doctype)) return;
const checked_items = this.get_checked_items();
frappe
.xcall("frappe.model.workflow.get_common_transition_actions", {
docs: checked_items,
doctype: this.doctype,
})
.then((actions) => {
Object.keys(this.workflow_action_items).forEach((key) => {
this.workflow_action_items[key].toggle(
actions.includes(key)
);
});
});
}
get_actions_menu_items() {
const doctype = this.doctype;
const actions_menu_items = [];
const bulk_operations = new BulkOperations({ doctype: this.doctype });
const is_field_editable = (field_doc) => {
return (
field_doc.fieldname &&
frappe.model.is_value_type(field_doc) &&
field_doc.fieldtype !== "Read Only" &&
!field_doc.hidden &&
!field_doc.read_only
);
};
const has_editable_fields = (doctype) => {
return frappe.meta
.get_docfields(doctype)
.some((field_doc) => is_field_editable(field_doc));
};
const has_submit_permission = (doctype) => {
return frappe.perm.has_perm(doctype, 0, "submit");
};
// utility
const bulk_assignment = () => {
return {
label: __("Assign To"),
action: () =>
bulk_operations.assign(
this.get_checked_items(true),
this.refresh
),
standard: true,
};
};
const bulk_assignment_rule = () => {
return {
label: __("Apply Assignment Rule"),
action: () =>
bulk_operations.apply_assignment_rule(
this.get_checked_items(true),
this.refresh
),
standard: true,
};
};
const bulk_add_tags = () => {
return {
label: __("Add Tags"),
action: () =>
bulk_operations.add_tags(
this.get_checked_items(true),
this.refresh
),
standard: true,
};
};
const bulk_printing = () => {
return {
label: __("Print"),
action: () => bulk_operations.print(this.get_checked_items()),
standard: true,
};
};
const bulk_delete = () => {
return {
label: __("Delete"),
action: () => {
const docnames = this.get_checked_items(true).map(
(docname) => docname.toString()
);
frappe.confirm(
__("Delete {0} items permanently?", [docnames.length]),
() => bulk_operations.delete(docnames, this.refresh)
);
},
standard: true,
};
};
const bulk_cancel = () => {
return {
label: __("Cancel"),
action: () => {
const docnames = this.get_checked_items(true);
if (docnames.length > 0) {
frappe.confirm(
__("Cancel {0} documents?", [docnames.length]),
() =>
bulk_operations.submit_or_cancel(
docnames,
"cancel",
this.refresh
)
);
}
},
standard: true,
};
};
const bulk_submit = () => {
return {
label: __("Submit"),
action: () => {
const docnames = this.get_checked_items(true);
if (docnames.length > 0) {
frappe.confirm(
__("Submit {0} documents?", [docnames.length]),
() =>
bulk_operations.submit_or_cancel(
docnames,
"submit",
this.refresh
)
);
}
},
standard: true,
};
};
const bulk_edit = () => {
return {
label: __("Edit"),
action: () => {
let field_mappings = {};
frappe.meta.get_docfields(doctype).forEach((field_doc) => {
if (is_field_editable(field_doc)) {
field_mappings[field_doc.label] = Object.assign(
{},
field_doc
);
}
});
const docnames = this.get_checked_items(true);
bulk_operations.edit(
docnames,
field_mappings,
this.refresh
);
},
standard: true,
};
};
// bulk edit
if (has_editable_fields(doctype)) {
actions_menu_items.push(bulk_edit());
}
// bulk assignment
actions_menu_items.push(bulk_assignment());
actions_menu_items.push(bulk_assignment_rule());
actions_menu_items.push(bulk_add_tags());
// bulk printing
if (frappe.model.can_print(doctype)) {
actions_menu_items.push(bulk_printing());
}
// bulk submit
if (
frappe.model.is_submittable(doctype) &&
has_submit_permission(doctype) &&
!frappe.model.has_workflow(doctype)
) {
actions_menu_items.push(bulk_submit());
}
// bulk cancel
if (
frappe.model.can_cancel(doctype) &&
!frappe.model.has_workflow(doctype)
) {
actions_menu_items.push(bulk_cancel());
}
// bulk delete
if (frappe.model.can_delete(doctype)) {
actions_menu_items.push(bulk_delete());
}
return actions_menu_items;
}
parse_filters_from_route_options() {
const filters = [];
for (let field in frappe.route_options) {
let doctype = null;
let value = frappe.route_options[field];
let value_array;
if (
$.isArray(value) &&
value[0].startsWith("[") &&
value[0].endsWith("]")
) {
value_array = [];
for (var i = 0; i < value.length; i++) {
value_array.push(JSON.parse(value[i]));
}
} else if (
typeof value === "string" &&
value.startsWith("[") &&
value.endsWith("]")
) {
value = JSON.parse(value);
}
// if `Child DocType.fieldname`
if (field.includes(".")) {
doctype = field.split(".")[0];
field = field.split(".")[1];
}
// find the table in which the key exists
// for example the filter could be {"item_code": "X"}
// where item_code is in the child table.
// we can search all tables for mapping the doctype
if (!doctype) {
doctype = frappe.meta.get_doctype_for_field(
this.doctype,
field
);
}
if (doctype) {
if (value_array) {
for (var j = 0; j < value_array.length; j++) {
if ($.isArray(value_array[j])) {
filters.push([
doctype,
field,
value_array[j][0],
value_array[j][1],
]);
} else {
filters.push([doctype, field, "=", value_array[j]]);
}
}
} else if ($.isArray(value)) {
filters.push([doctype, field, value[0], value[1]]);
} else {
filters.push([doctype, field, "=", value]);
}
}
}
return filters;
}
static trigger_list_update(data) {
const doctype = data.doctype;
if (!doctype) return;
frappe.provide("frappe.views.trees");
// refresh tree view
if (frappe.views.trees[doctype]) {
frappe.views.trees[doctype].tree.refresh();
return;
}
// refresh list view
const page_name = frappe.get_route_str();
const list_view = frappe.views.list_view[page_name];
list_view && list_view.on_update(data);
}
};
$(document).on("save", (event, doc) => {
frappe.views.ListView.trigger_list_update(doc);
});
frappe.get_list_view = (doctype) => {
let route = `List/${doctype}/List`;
return frappe.views.list_view[route];
};