From ed3432b56888ff5a5d7e504fdc9987e60a301fa7 Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Wed, 18 Dec 2024 18:09:47 +0400
Subject: [PATCH] App loaded
---
.../frappe_enhanced_gridview/.frappe | 0
frappe_enhanced_gridview/hooks.py | 32 +-
.../public/js/enhanced_gridview.bundle.js | 349 ++++
frappe_enhanced_gridview/public/js/grid.js | 1325 +++++++++++++++
.../public/js/grid_pagination.js | 184 ++
.../public/js/grid_row.js | 1500 +++++++++++++++++
.../public/js/grid_row_form.js | 148 ++
.../public/scss/enhanced_gridview.bundle.scss | 135 ++
8 files changed, 3650 insertions(+), 23 deletions(-)
delete mode 100644 frappe_enhanced_gridview/frappe_enhanced_gridview/.frappe
create mode 100644 frappe_enhanced_gridview/public/js/enhanced_gridview.bundle.js
create mode 100644 frappe_enhanced_gridview/public/js/grid.js
create mode 100644 frappe_enhanced_gridview/public/js/grid_pagination.js
create mode 100644 frappe_enhanced_gridview/public/js/grid_row.js
create mode 100644 frappe_enhanced_gridview/public/js/grid_row_form.js
create mode 100644 frappe_enhanced_gridview/public/scss/enhanced_gridview.bundle.scss
diff --git a/frappe_enhanced_gridview/frappe_enhanced_gridview/.frappe b/frappe_enhanced_gridview/frappe_enhanced_gridview/.frappe
deleted file mode 100644
index e69de29..0000000
diff --git a/frappe_enhanced_gridview/hooks.py b/frappe_enhanced_gridview/hooks.py
index 87f9d2e..18335bd 100644
--- a/frappe_enhanced_gridview/hooks.py
+++ b/frappe_enhanced_gridview/hooks.py
@@ -1,32 +1,22 @@
app_name = "frappe_enhanced_gridview"
app_title = "Frappe Enhanced Gridview"
-app_publisher = "Jey ERP"
-app_description = "App that allows more than 10 columns"
-app_email = "info@jeyerp.az"
-app_license = "unlicense"
-
-# Apps
-# ------------------
-
+app_publisher = "Nesscale Solutions Private Limited"
+app_description = (
+ "Frappe Grid view with scrollablee child table and allow more than 10 columns"
+)
+app_email = "info@nesscale.com"
+app_license = "mit"
# required_apps = []
-# Each item in the list will be shown as an app in the apps page
-# add_to_apps_screen = [
-# {
-# "name": "frappe_enhanced_gridview",
-# "logo": "/assets/frappe_enhanced_gridview/logo.png",
-# "title": "Frappe Enhanced Gridview",
-# "route": "/frappe_enhanced_gridview",
-# "has_permission": "frappe_enhanced_gridview.api.permission.has_app_permission"
-# }
-# ]
-
# Includes in
# ------------------
# include js, css files in header of desk.html
# app_include_css = "/assets/frappe_enhanced_gridview/css/frappe_enhanced_gridview.css"
# app_include_js = "/assets/frappe_enhanced_gridview/js/frappe_enhanced_gridview.js"
+app_include_css = ["enhanced_gridview.bundle.css"]
+app_include_js = ["enhanced_gridview.bundle.js"]
+
# include js, css files in header of web template
# web_include_css = "/assets/frappe_enhanced_gridview/css/frappe_enhanced_gridview.css"
@@ -70,9 +60,6 @@ app_license = "unlicense"
# automatically create page for each record of this doctype
# website_generators = ["Web Page"]
-# automatically load and sync documents of this doctype from downstream apps
-# importable_doctypes = [doctype_1]
-
# Jinja
# ----------
@@ -244,4 +231,3 @@ app_license = "unlicense"
# default_log_clearing_doctypes = {
# "Logging DocType Name": 30 # days to retain logs
# }
-
diff --git a/frappe_enhanced_gridview/public/js/enhanced_gridview.bundle.js b/frappe_enhanced_gridview/public/js/enhanced_gridview.bundle.js
new file mode 100644
index 0000000..a6fd3e6
--- /dev/null
+++ b/frappe_enhanced_gridview/public/js/enhanced_gridview.bundle.js
@@ -0,0 +1,349 @@
+import GridRow from './grid_row';
+import Grid from './grid';
+
+class Custom_GridRow extends GridRow {
+
+ validate_columns_width() {
+ let total_column_width = 0.0;
+
+ this.selected_columns_for_grid.forEach((row) => {
+ if (row.columns && row.columns > 0) {
+ total_column_width += cint(row.columns);
+ }
+ });
+
+ // if (total_column_width && total_column_width > 10) {
+ // frappe.throw(__("The total column width cannot be more than 10."));
+ // }
+ }
+
+ show_form() {
+ super.show_form()
+
+ $(this.grid.form_grid).removeClass("relative-important");
+ }
+ hide_form() {
+ super.hide_form()
+
+ $(this.grid.form_grid).addClass("relative-important");
+ }
+}
+
+
+class Custom_Grid extends Grid {
+
+ make() {
+ let template = `
+
+
+
+
+
+
+
+
+ `;
+
+ this.wrapper = $(template).appendTo(this.parent);
+ $(this.parent).addClass("form-group");
+ this.set_grid_description();
+ this.set_doc_url();
+
+ frappe.utils.bind_actions_with_object(this.wrapper, this);
+
+ this.form_grid = this.wrapper.find(".form-grid");
+
+
+ // enhance slider changes
+ this.form_grid.addClass("relative-important");
+ this.form_grid_container = this.wrapper.find(".form-grid-container");
+ this.enhanced_slider = this.wrapper.find(".enhanced-slider");
+ let me = this
+ this.enhanced_slider.on("input", function (event) {
+ const value = event.target.value;
+ me.form_grid.css("left", `-${value}px`)
+ me.setup_scrollable_width()
+ })
+
+
+
+
+
+ this.setup_add_row();
+
+ this.setup_grid_pagination();
+
+ this.custom_buttons = {};
+ this.grid_buttons = this.wrapper.find(".grid-buttons");
+ this.grid_custom_buttons = this.wrapper.find(".grid-custom-buttons");
+ this.remove_rows_button = this.grid_buttons.find(".grid-remove-rows");
+ this.remove_all_rows_button = this.grid_buttons.find(".grid-remove-all-rows");
+
+ this.setup_allow_bulk_edit();
+ this.setup_check();
+ if (this.df.on_setup) {
+ this.df.on_setup(this);
+ }
+
+
+ }
+
+ make_head() {
+ if (this.prevent_build) return;
+
+ // labels
+ if (this.header_row) {
+ $(this.parent).find(".grid-heading-row .grid-row").remove();
+ }
+ // implement custom class
+ this.header_row = new Custom_GridRow({
+ parent: $(this.parent).find(".grid-heading-row"),
+ parent_df: this.df,
+ docfields: this.docfields,
+ frm: this.frm,
+ grid: this,
+ configure_columns: true,
+ });
+ // implement custom class
+ this.header_search = new Custom_GridRow({
+ parent: $(this.parent).find(".grid-heading-row"),
+ parent_df: this.df,
+ docfields: this.docfields,
+ frm: this.frm,
+ grid: this,
+ show_search: true,
+ });
+ this.header_search.row.addClass("filter-row");
+ if (this.header_search.show_search || this.header_search.show_search_row()) {
+ $(this.parent).find(".grid-heading-row").addClass("with-filter");
+ } else {
+ $(this.parent).find(".grid-heading-row").removeClass("with-filter");
+ }
+
+ this.filter_applied && this.update_search_columns();
+ }
+
+ render_result_rows($rows, append_row) {
+ let result_length = this.grid_pagination.get_result_length();
+ let page_index = this.grid_pagination.page_index;
+ let page_length = this.grid_pagination.page_length;
+ if (!this.grid_rows) {
+ return;
+ }
+ for (var ri = (page_index - 1) * page_length; ri < result_length; ri++) {
+ var d = this.data[ri];
+ if (!d) {
+ return;
+ }
+ if (d.idx === undefined) {
+ d.idx = ri + 1;
+ }
+ if (d.name === undefined) {
+ d.name = "row " + d.idx;
+ }
+ let grid_row;
+ if (this.grid_rows[ri] && !append_row) {
+ grid_row = this.grid_rows[ri];
+ grid_row.doc = d;
+ grid_row.refresh();
+ } else {
+ // implement custom class
+ grid_row = new Custom_GridRow({
+ parent: $rows,
+ parent_df: this.df,
+ docfields: this.docfields,
+ doc: d,
+ frm: this.frm,
+ grid: this,
+ });
+ this.grid_rows[ri] = grid_row;
+ }
+
+ this.grid_rows_by_docname[d.name] = grid_row;
+ }
+ }
+
+ setup_visible_columns() {
+ if (this.visible_columns && this.visible_columns.length > 0) return;
+
+ this.user_defined_columns = [];
+ this.setup_user_defined_columns();
+ var total_colsize = 1,
+ fields =
+ this.user_defined_columns && this.user_defined_columns.length > 0
+ ? this.user_defined_columns
+ : this.editable_fields || this.docfields;
+
+ this.visible_columns = [];
+
+ for (var ci in fields) {
+ var _df = fields[ci];
+
+ // get docfield if from fieldname
+ df =
+ this.user_defined_columns && this.user_defined_columns.length > 0
+ ? _df
+ : this.fields_map[_df.fieldname];
+
+ if (
+ df &&
+ !df.hidden &&
+ (this.editable_fields || df.in_list_view) &&
+ ((this.frm && this.frm.get_perm(df.permlevel, "read")) || !this.frm) &&
+ !frappe.model.layout_fields.includes(df.fieldtype)
+ ) {
+ if (df.columns) {
+ df.colsize = df.columns;
+ } else {
+ this.update_default_colsize(df);
+ }
+
+ // attach formatter on refresh
+ if (
+ df.fieldtype == "Link" &&
+ !df.formatter &&
+ df.parent &&
+ frappe.meta.docfield_map[df.parent]
+ ) {
+ const docfield = frappe.meta.docfield_map[df.parent][df.fieldname];
+ if (docfield && docfield.formatter) {
+ df.formatter = docfield.formatter;
+ }
+ }
+
+ total_colsize += df.colsize;
+ if (total_colsize > 100) return false; // Increased limit to 20
+ this.visible_columns.push([df, df.colsize]);
+ }
+
+ }
+
+ // redistribute if total-col size is less than 12
+ var passes = 0;
+ while (total_colsize < 11 && passes < 12) { // Adjusted loop conditions
+ for (var i in this.visible_columns) {
+ var df = this.visible_columns[i][0];
+ var colsize = this.visible_columns[i][1];
+ if (colsize > 1 && colsize < 11 && frappe.model.is_non_std_field(df.fieldname)) {
+ if (
+ passes < 3 &&
+ ["Int", "Currency", "Float", "Check", "Percent"].indexOf(df.fieldtype) !==
+ -1
+ ) {
+ // don't increase col size of these fields in first 3 passes
+ continue;
+ }
+
+ this.visible_columns[i][1] += 1;
+ total_colsize++;
+ }
+
+ if (total_colsize > 10) break;
+ }
+ passes++;
+ }
+
+ // set width of scrollable area
+ this.setup_scrollable_width()
+ this.verify_overflow_columns_width()
+ }
+
+
+ setup_scrollable_width() {
+ let width = 200
+ this.visible_columns.forEach(column => {
+ width += column[1] * 50 + 100
+ });
+ if (width > this.form_grid_container[0].clientWidth) {
+ this.enhanced_slider.prop("max", width - this.form_grid_container[0].clientWidth)
+ this.enhanced_slider.prop("style", "display:block")
+ } else {
+ this.form_grid.css("left", `0px`)
+ this.enhanced_slider.prop("max", this.form_grid_container[0].clientWidth)
+ this.enhanced_slider.prop("style", "display:none")
+ this.enhanced_slider.prop("value", 0)
+ }
+ }
+
+ verify_overflow_columns_width() {
+ let width = 200
+ this.visible_columns.forEach(column => {
+ width += column[1] * 50 + 100
+ });
+
+ if (width > this.form_grid_container[0].clientWidth) {
+ this.form_grid_container.addClass('enhanced-grid-container')
+ this.enhanced_slider.prop("style", "display:block")
+ } else {
+ this.enhanced_slider.prop("style", "display:none")
+ this.enhanced_slider.prop("value", 0)
+ }
+ }
+
+}
+
+
+frappe.ui.form.ControlTable = class CustomControlTable extends frappe.ui.form.ControlTable {
+ make() {
+ super.make();
+
+ // add title if prev field is not column / section heading or html
+ this.grid = new Custom_Grid({
+ frm: this.frm,
+ df: this.df,
+ parent: this.wrapper,
+ control: this,
+ });
+
+ }
+
+
+
+}
+
diff --git a/frappe_enhanced_gridview/public/js/grid.js b/frappe_enhanced_gridview/public/js/grid.js
new file mode 100644
index 0000000..8ca5857
--- /dev/null
+++ b/frappe_enhanced_gridview/public/js/grid.js
@@ -0,0 +1,1325 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See license.txt
+
+import GridRow from "./grid_row";
+import GridPagination from "./grid_pagination";
+
+frappe.ui.form.get_open_grid_form = function () {
+ return $(".grid-row-open").data("grid_row");
+};
+
+frappe.ui.form.close_grid_form = function () {
+ var open_form = frappe.ui.form.get_open_grid_form();
+ open_form && open_form.hide_form();
+
+ // hide editable row too
+ if (frappe.ui.form.editable_row) {
+ frappe.ui.form.editable_row.toggle_editable_row(false);
+ }
+};
+
+export default class Grid {
+ constructor(opts) {
+ $.extend(this, opts);
+ this.fieldinfo = {};
+ this.doctype = this.df.options;
+
+ if (this.doctype) {
+ this.meta = frappe.get_meta(this.doctype);
+ }
+ this.fields_map = {};
+ this.template = null;
+ this.multiple_set = false;
+ if (
+ this.frm &&
+ this.frm.meta.__form_grid_templates &&
+ this.frm.meta.__form_grid_templates[this.df.fieldname]
+ ) {
+ this.template = this.frm.meta.__form_grid_templates[this.df.fieldname];
+ }
+ this.filter = {};
+ this.is_grid = true;
+ this.debounced_refresh = this.refresh.bind(this);
+ this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100);
+ }
+
+ get perm() {
+ return this.control?.perm || this.frm?.perm || this.df.perm;
+ }
+
+ set perm(_perm) {
+ console.error("Setting perm on grid isn't supported, update form's perm instead");
+ }
+
+ allow_on_grid_editing() {
+ if ((this.meta && this.meta.editable_grid) || !this.meta) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ make() {
+ let template = `
+
+
+
+
+
+
+
+
+ `;
+
+ this.wrapper = $(template).appendTo(this.parent);
+ $(this.parent).addClass("form-group");
+ this.set_grid_description();
+ this.set_doc_url();
+
+ frappe.utils.bind_actions_with_object(this.wrapper, this);
+
+ this.form_grid = this.wrapper.find(".form-grid");
+
+ this.setup_add_row();
+
+ this.setup_grid_pagination();
+
+ this.custom_buttons = {};
+ this.grid_buttons = this.wrapper.find(".grid-buttons");
+ this.grid_custom_buttons = this.wrapper.find(".grid-custom-buttons");
+ this.remove_rows_button = this.grid_buttons.find(".grid-remove-rows");
+ this.remove_all_rows_button = this.grid_buttons.find(".grid-remove-all-rows");
+
+ this.setup_allow_bulk_edit();
+ this.setup_check();
+ if (this.df.on_setup) {
+ this.df.on_setup(this);
+ }
+ }
+ set_grid_description() {
+ let description_wrapper = $(this.parent).find(".grid-description");
+ if (this.df.description) {
+ description_wrapper.text(__(this.df.description));
+ } else {
+ description_wrapper.hide();
+ }
+ }
+
+ set_doc_url() {
+ let unsupported_fieldtypes = frappe.model.no_value_type.filter(
+ (x) => frappe.model.table_fields.indexOf(x) === -1
+ );
+
+ if (
+ !this.df.label ||
+ !this.df?.documentation_url ||
+ unsupported_fieldtypes.includes(this.df.fieldtype)
+ )
+ return;
+
+ let $help = $(this.parent).find("span.help");
+ $help.empty();
+ $(`
+ ${frappe.utils.icon("help", "sm")}
+ `).appendTo($help);
+ }
+
+ setup_grid_pagination() {
+ this.grid_pagination = new GridPagination({
+ grid: this,
+ wrapper: this.wrapper,
+ });
+ }
+
+ setup_check() {
+ this.wrapper.on("click", ".grid-row-check", (e) => {
+ const $check = $(e.currentTarget);
+ const checked = $check.prop("checked");
+ const is_select_all = $check.parents(".grid-heading-row:first").length !== 0;
+ const docname = $check.parents(".grid-row:first")?.attr("data-name");
+
+ if (is_select_all) {
+ // (un)check all visible checkboxes
+ this.form_grid.find(".grid-row-check").prop("checked", checked);
+
+ // set following rows as checked in model
+ let result_length = this.grid_pagination.get_result_length();
+ let page_index = this.grid_pagination.page_index;
+ let page_length = this.grid_pagination.page_length;
+ for (let ri = (page_index - 1) * page_length; ri < result_length; ri++) {
+ this.grid_rows[ri].select(checked);
+ }
+ } else if (docname) {
+ if (e.shiftKey && this.last_checked_docname) {
+ this.check_range(docname, this.last_checked_docname, checked);
+ }
+ this.grid_rows_by_docname[docname].select(checked);
+ this.last_checked_docname = docname;
+ }
+ this.refresh_remove_rows_button();
+ });
+ }
+
+ /**
+ * Checks or unchecks all checkboxes between two rows (included), given their docnames.
+ * Rows are only checked only if both parameters are valid docnames.
+ * @param {string} docname1
+ * @param {string} docname2
+ */
+ check_range(docname1, docname2, checked = true) {
+ const row_1 = this.grid_rows_by_docname[docname1];
+ const row_2 = this.grid_rows_by_docname[docname2];
+ const index_1 = this.grid_rows.indexOf(row_1);
+ const index_2 = this.grid_rows.indexOf(row_2);
+ if (index_1 === -1 || index_2 === -1) return;
+ const [start, end] = [index_1, index_2].sort((a, b) => a - b);
+ const rows = this.grid_rows.slice(start, end + 1);
+ for (const row of rows) {
+ row.select(checked);
+ row.row_check?.find(".grid-row-check").prop("checked", checked);
+ }
+ }
+
+ delete_rows() {
+ var dirty = false;
+
+ let tasks = [];
+ let selected_children = this.get_selected_children();
+ selected_children.forEach((doc) => {
+ tasks.push(() => {
+ if (!this.frm) {
+ this.df.data = this.get_data();
+ this.df.data = this.df.data.filter((row) => row.idx != doc.idx);
+ }
+ this.grid_rows_by_docname[doc.name].remove();
+ dirty = true;
+ });
+ tasks.push(() => frappe.timeout(0.1));
+ });
+
+ if (!this.frm) {
+ tasks.push(() => {
+ // reorder idx of df.data
+ this.df.data.forEach((row, index) => (row.idx = index + 1));
+ });
+ }
+
+ tasks.push(() => {
+ if (dirty) {
+ this.refresh();
+ this.frm &&
+ this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
+ }
+ });
+
+ frappe.run_serially(tasks);
+
+ this.wrapper.find(".grid-heading-row .grid-row-check:checked:first").prop("checked", 0);
+ if (selected_children.length == this.grid_pagination.page_length) {
+ this.scroll_to_top();
+ }
+ }
+
+ delete_all_rows() {
+ frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
+ this.frm.doc[this.df.fieldname] = [];
+ $(this.parent).find(".rows").empty();
+ this.grid_rows = [];
+ this.refresh();
+ this.frm &&
+ this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
+ this.frm && this.frm.dirty();
+ this.scroll_to_top();
+ });
+ }
+
+ scroll_to_top() {
+ frappe.utils.scroll_to(this.wrapper);
+ }
+
+ select_row(name) {
+ this.grid_rows_by_docname[name].select();
+ }
+
+ remove_all() {
+ this.grid_rows.forEach((row) => {
+ row.remove();
+ });
+ }
+
+ refresh_remove_rows_button() {
+ if (this.df.cannot_delete_rows) {
+ return;
+ }
+
+ this.remove_rows_button.toggleClass(
+ "hidden",
+ this.wrapper.find(".grid-body .grid-row-check:checked:first").length ? false : true
+ );
+
+ let select_all_checkbox_checked = this.wrapper.find(
+ ".grid-heading-row .grid-row-check:checked:first"
+ ).length;
+ let show_delete_all_btn =
+ select_all_checkbox_checked && this.data.length > this.get_selected_children().length;
+ this.remove_all_rows_button.toggleClass("hidden", !show_delete_all_btn);
+ }
+
+ get_selected() {
+ return (this.grid_rows || [])
+ .map((row) => {
+ return row.doc.__checked ? row.doc.name : null;
+ })
+ .filter((d) => {
+ return d;
+ });
+ }
+
+ get_selected_children() {
+ return (this.grid_rows || [])
+ .map((row) => {
+ return row.doc.__checked ? row.doc : null;
+ })
+ .filter((d) => {
+ return d;
+ });
+ }
+
+ reset_grid() {
+ this.visible_columns = [];
+ this.grid_rows = [];
+
+ $(this.parent).find(".grid-body .grid-row").remove();
+ this.refresh();
+ }
+
+ make_head() {
+ if (this.prevent_build) return;
+
+ // labels
+ if (this.header_row) {
+ $(this.parent).find(".grid-heading-row .grid-row").remove();
+ }
+ this.header_row = new GridRow({
+ parent: $(this.parent).find(".grid-heading-row"),
+ parent_df: this.df,
+ docfields: this.docfields,
+ frm: this.frm,
+ grid: this,
+ configure_columns: true,
+ });
+
+ this.header_search = new GridRow({
+ parent: $(this.parent).find(".grid-heading-row"),
+ parent_df: this.df,
+ docfields: this.docfields,
+ frm: this.frm,
+ grid: this,
+ show_search: true,
+ });
+ this.header_search.row.addClass("filter-row");
+ if (this.header_search.show_search || this.header_search.show_search_row()) {
+ $(this.parent).find(".grid-heading-row").addClass("with-filter");
+ } else {
+ $(this.parent).find(".grid-heading-row").removeClass("with-filter");
+ }
+
+ this.filter_applied && this.update_search_columns();
+ }
+
+ update_search_columns() {
+ for (const field in this.filter) {
+ if (this.filter[field] && !this.header_search.search_columns[field]) {
+ delete this.filter[field];
+ this.data = this.get_data(this.filter_applied);
+ break;
+ }
+
+ if (this.filter[field] && this.filter[field].value) {
+ let $input = this.header_search.row_index.find("input");
+ if (field && field !== "row-index") {
+ $input = this.header_search.search_columns[field].find("input");
+ }
+ $input.val(this.filter[field].value);
+ }
+ }
+ }
+
+ refresh() {
+ if (this.frm && this.frm.setting_dependency) return;
+
+ this.filter_applied = Object.keys(this.filter).length !== 0;
+ this.data = this.get_data(this.filter_applied);
+
+ !this.wrapper && this.make();
+ let $rows = $(this.parent).find(".rows");
+
+ this.setup_fields();
+
+ if (this.frm) {
+ this.display_status = frappe.perm.get_field_display_status(
+ this.df,
+ this.frm.doc,
+ this.perm
+ );
+ } else if (this.df.is_web_form && this.control) {
+ this.display_status = this.control.get_status();
+ } else {
+ // not in form
+ this.display_status = "Write";
+ }
+
+ if (this.display_status === "None") return;
+
+ // redraw
+ this.make_head();
+
+ if (!this.grid_rows) {
+ /** @type {GridRow[]} */
+ this.grid_rows = [];
+ }
+
+ this.truncate_rows();
+ /** @type {Record} */
+ this.grid_rows_by_docname = {};
+
+ this.grid_pagination.update_page_numbers();
+ this.render_result_rows($rows, false);
+ this.grid_pagination.check_page_number();
+ this.wrapper.find(".grid-empty").toggleClass("hidden", Boolean(this.data.length));
+
+ // toolbar
+ this.setup_toolbar();
+ this.toggle_checkboxes(this.display_status !== "Read");
+
+ // sortable
+ if (this.is_sortable() && !this.sortable_setup_done) {
+ this.make_sortable($rows);
+ this.sortable_setup_done = true;
+ }
+
+ this.last_display_status = this.display_status;
+ this.last_docname = this.frm && this.frm.docname;
+
+ // red if mandatory
+ this.form_grid.toggleClass("error", !!(this.df.reqd && !(this.data && this.data.length)));
+
+ this.refresh_remove_rows_button();
+
+ this.wrapper.trigger("change");
+ }
+
+ render_result_rows($rows, append_row) {
+ let result_length = this.grid_pagination.get_result_length();
+ let page_index = this.grid_pagination.page_index;
+ let page_length = this.grid_pagination.page_length;
+ if (!this.grid_rows) {
+ return;
+ }
+ for (var ri = (page_index - 1) * page_length; ri < result_length; ri++) {
+ var d = this.data[ri];
+ if (!d) {
+ return;
+ }
+ if (d.idx === undefined) {
+ d.idx = ri + 1;
+ }
+ if (d.name === undefined) {
+ d.name = "row " + d.idx;
+ }
+ let grid_row;
+ if (this.grid_rows[ri] && !append_row) {
+ grid_row = this.grid_rows[ri];
+ grid_row.doc = d;
+ grid_row.refresh();
+ } else {
+ grid_row = new GridRow({
+ parent: $rows,
+ parent_df: this.df,
+ docfields: this.docfields,
+ doc: d,
+ frm: this.frm,
+ grid: this,
+ });
+ this.grid_rows[ri] = grid_row;
+ }
+
+ this.grid_rows_by_docname[d.name] = grid_row;
+ }
+ }
+
+ setup_toolbar() {
+ if (this.is_editable()) {
+ this.wrapper.find(".grid-footer").toggle(true);
+
+ // show, hide buttons to add rows
+ if (this.cannot_add_rows || (this.df && this.df.cannot_add_rows)) {
+ // add 'hidden' to buttons
+ this.wrapper.find(".grid-add-row, .grid-add-multiple-rows").addClass("hidden");
+ } else {
+ // show buttons
+ this.wrapper.find(".grid-add-row").removeClass("hidden");
+
+ if (this.multiple_set) {
+ this.wrapper.find(".grid-add-multiple-rows").removeClass("hidden");
+ }
+ }
+ } else if (
+ this.grid_rows.length < this.grid_pagination.page_length &&
+ !this.df.allow_bulk_edit
+ ) {
+ this.wrapper.find(".grid-footer").toggle(false);
+ }
+
+ this.wrapper
+ .find(".grid-add-row, .grid-add-multiple-rows, .grid-upload")
+ .toggle(this.is_editable());
+ }
+
+ truncate_rows() {
+ if (this.grid_rows.length > this.data.length) {
+ // remove extra rows
+ for (var i = this.data.length; i < this.grid_rows.length; i++) {
+ var grid_row = this.grid_rows[i];
+ if (grid_row) grid_row.wrapper.remove();
+ }
+ this.grid_rows.splice(this.data.length);
+ }
+ }
+
+ setup_fields() {
+ // reset docfield
+ if (this.frm && this.frm.docname) {
+ // use doc specific docfield object
+ this.df = frappe.meta.get_docfield(
+ this.frm.doctype,
+ this.df.fieldname,
+ this.frm.docname
+ );
+ } else {
+ // use non-doc specific docfield
+ if (this.df.options) {
+ this.df =
+ frappe.meta.get_docfield(this.df.options, this.df.fieldname) ||
+ this.df ||
+ null;
+ }
+ }
+
+ if (this.doctype && this.frm) {
+ this.docfields = frappe.meta.get_docfields(this.doctype, this.frm.docname);
+ } else {
+ // fields given in docfield
+ this.docfields = this.df.fields;
+ }
+
+ this.docfields.forEach((df) => {
+ this.fields_map[df.fieldname] = df;
+ });
+ }
+
+ refresh_row(docname) {
+ this.grid_rows_by_docname[docname] && this.grid_rows_by_docname[docname].refresh();
+ }
+
+ make_sortable($rows) {
+ this.grid_sortable = new Sortable($rows.get(0), {
+ group: { name: this.df.fieldname },
+ handle: ".sortable-handle",
+ draggable: ".grid-row",
+ animation: 100,
+ filter: "li, a",
+ onMove: (event) => {
+ // don't move if editable
+ if (!this.is_editable()) {
+ return false;
+ }
+ // prevent drag behaviour if _sortable property is "false"
+ let idx = $(event.dragged).closest(".grid-row").attr("data-idx");
+ let doc = this.data[idx % this.grid_pagination.page_length];
+ if (doc && doc._sortable === false) {
+ return false;
+ }
+ },
+ onUpdate: (event) => {
+ let idx = $(event.item).closest(".grid-row").attr("data-idx") - 1;
+ let doc = this.data[idx % this.grid_pagination.page_length];
+ this.renumber_based_on_dom();
+ this.frm &&
+ this.frm.script_manager.trigger(
+ this.df.fieldname + "_move",
+ this.df.options,
+ doc.name
+ );
+ this.refresh();
+ this.frm && this.frm.dirty();
+ },
+ });
+
+ this.frm && $(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
+ }
+
+ get_data(filter_field) {
+ let data = [];
+ if (filter_field) {
+ data = this.get_filtered_data();
+ } else {
+ data = this.frm
+ ? this.frm.doc[this.df.fieldname] || []
+ : this.df.data || this.get_modal_data();
+ }
+ return data;
+ }
+
+ get_filtered_data() {
+ let all_data = this.frm ? this.frm.doc[this.df.fieldname] : this.df.data;
+
+ if (!all_data) return;
+
+ for (const field in this.filter) {
+ all_data = all_data.filter((data) => {
+ let { df, value } = this.filter[field];
+ return this.get_data_based_on_fieldtype(df, data, value.toLowerCase());
+ });
+ }
+
+ return all_data;
+ }
+
+ get_data_based_on_fieldtype(df, data, value) {
+ let fieldname = df.fieldname;
+ let fieldtype = df.fieldtype;
+ let fieldvalue = data[fieldname];
+
+ if (fieldtype === "Check") {
+ value = frappe.utils.string_to_boolean(value);
+ return Boolean(fieldvalue) === value && data;
+ } else if (fieldtype === "Sr No" && data.idx.toString().includes(value)) {
+ return data;
+ } else if (fieldtype === "Duration" && fieldvalue) {
+ let formatted_duration = frappe.utils.get_formatted_duration(fieldvalue);
+
+ if (formatted_duration.includes(value)) {
+ return data;
+ }
+ } else if (fieldtype === "Barcode" && fieldvalue) {
+ let barcode = fieldvalue.startsWith("