App loaded

This commit is contained in:
Ali 2024-12-18 18:09:47 +04:00
parent 5474d7408d
commit ed3432b568
8 changed files with 3650 additions and 23 deletions

View File

@ -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 <head>
# ------------------
# 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
# }

View File

@ -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 = `
<div class="grid-field">
<label class="control-label">${__(this.df.label || "")}</label>
<span class="help"></span>
<p class="text-muted small grid-description"></p>
<div class="grid-custom-buttons"></div>
<div class="form-grid-container enhanced-grid-container">
<div class="form-grid">
<div class="grid-heading-row"></div>
<div class="grid-body">
<div class="rows"></div>
<div class="grid-empty text-center">
<img
src="/assets/frappe/images/ui-states/grid-empty-state.svg"
alt="Grid Empty State"
class="grid-empty-illustration"
>
${__("No Data")}
</div>
</div>
</div>
<input type="range" min="1" max="100" value="1" class="enhanced-slider">
</div>
<div class="small form-clickable-section grid-footer">
<div class="flex justify-between">
<div class="grid-buttons">
<button type="button" class="btn btn-xs btn-danger grid-remove-rows hidden"
data-action="delete_rows">
${__("Delete")}
</button>
<button type="button" class="btn btn-xs btn-danger grid-remove-all-rows hidden"
data-action="delete_all_rows">
${__("Delete All")}
</button>
<!-- hack to allow firefox include this in tabs -->
<button type="button" class="btn btn-xs btn-secondary grid-add-row">
${__("Add Row")}
</button>
<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary hidden">
${__("Add Multiple")}</a>
</button>
</div>
<div class="grid-pagination">
</div>
<div class="grid-bulk-actions text-right">
<button type="button" class="grid-download btn btn-xs btn-secondary hidden">
${__("Download")}
</button>
<button type="button" class="grid-upload btn btn-xs btn-secondary hidden">
${__("Upload")}
</button>
</div>
</div>
</div>
</div>
`;
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,
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
export default class GridPagination {
constructor(opts) {
$.extend(this, opts);
this.setup_pagination();
}
setup_pagination() {
this.page_length = 50;
this.page_index = 1;
this.total_pages = Math.ceil(this.grid.data.length / this.page_length);
this.render_pagination();
}
render_pagination() {
if (this.grid.data.length <= this.page_length) {
this.wrapper.find(".grid-pagination").html("");
} else {
let $pagination_template = this.get_pagination_html();
this.wrapper.find(".grid-pagination").html($pagination_template);
this.prev_page_button = this.wrapper.find(".prev-page");
this.next_page_button = this.wrapper.find(".next-page");
this.$page_number = this.wrapper.find(".current-page-number");
this.$total_pages = this.wrapper.find(".total-page-number");
this.first_page_button = this.wrapper.find(".first-page");
this.last_page_button = this.wrapper.find(".last-page");
this.bind_pagination_events();
}
}
bind_pagination_events() {
this.prev_page_button.on("click", () => {
this.render_prev_page();
});
this.next_page_button.on("click", () => {
this.render_next_page();
});
this.first_page_button.on("click", () => {
this.go_to_page(1);
});
this.last_page_button.on("click", () => {
this.go_to_page(this.total_pages);
});
this.$page_number.on("keyup", (e) => {
e.currentTarget.style.width = (e.currentTarget.value.length + 1) * 8 + "px";
});
this.$page_number.on("keydown", (e) => {
e = e ? e : window.event;
var charCode = e.which ? e.which : e.keyCode;
let arrow = { up: 38, down: 40 };
switch (charCode) {
case arrow.up:
this.inc_dec_number(true);
break;
case arrow.down:
this.inc_dec_number(false);
break;
}
// only allow numbers from 0-9 and up, down, left, right arrow keys
if (
charCode > 31 &&
(charCode < 48 || charCode > 57) &&
![37, 38, 39, 40].includes(charCode)
) {
return false;
}
return true;
});
this.$page_number.on("focusout", (e) => {
if (this.page_index == e.currentTarget.value) return;
this.page_index = e.currentTarget.value;
if (this.page_index < 1) {
this.page_index = 1;
} else if (this.page_index > this.total_pages) {
this.page_index = this.total_pages;
}
this.go_to_page();
});
}
inc_dec_number(increment) {
let new_value = parseInt(this.$page_number.val());
increment ? new_value++ : new_value--;
if (new_value < 1 || new_value > this.total_pages) return;
this.$page_number.val(new_value);
}
update_page_numbers() {
let total_pages = Math.ceil(this.grid.data.length / this.page_length);
if (this.total_pages !== total_pages) {
this.total_pages = total_pages;
this.render_pagination();
}
}
check_page_number() {
if (this.page_index > this.total_pages && this.page_index > 1) {
this.go_to_page(this.page_index - 1);
}
}
get_pagination_html() {
let page_text_html = `<div class="page-text">
<input class="current-page-number page-number" type="text" value="${__(this.page_index)}"/>
<span>${__("of")}</span>
<span class="total-page-number page-number"> ${__(this.total_pages)} </span>
</div>`;
return $(`<button class="btn btn-secondary btn-xs first-page"">
<span>${__("First")}</span>
</button>
<button class="btn btn-secondary btn-xs prev-page">${frappe.utils.icon("left", "xs")}</button>
${page_text_html}
<button class="btn btn-secondary btn-xs next-page">${frappe.utils.icon("right", "xs")}</button>
<button class="btn btn-secondary btn-xs last-page">
<span>${__("Last")}</span>
</button>`);
}
render_next_page() {
if (this.page_index * this.page_length < this.grid.data.length) {
this.page_index++;
this.go_to_page();
}
}
render_prev_page() {
if (this.page_index > 1) {
this.page_index--;
this.go_to_page();
}
}
go_to_page(index, from_refresh) {
if (!index) {
index = this.page_index;
} else {
this.page_index = index;
}
let $rows = $(this.grid.parent).find(".rows").empty();
this.grid.render_result_rows($rows, true);
if (this.$page_number) {
this.$page_number.val(index);
this.$page_number.css("width", (index.toString().length + 1) * 8 + "px");
}
this.update_page_numbers();
if (!from_refresh) {
this.grid.scroll_to_top();
}
}
go_to_last_page_to_add_row() {
let total_pages = this.total_pages;
let page_length = this.page_length;
if (this.grid.data.length == page_length * total_pages) {
this.go_to_page(total_pages + 1);
frappe.utils.scroll_to(this.wrapper);
} else if (this.page_index == this.total_pages) {
return;
} else {
this.go_to_page(total_pages);
}
}
get_result_length() {
return this.grid.data.length < this.page_index * this.page_length
? this.grid.data.length
: this.page_index * this.page_length;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
export default class GridRowForm {
constructor(opts) {
$.extend(this, opts);
this.wrapper = $('<div class="form-in-grid"></div>').appendTo(this.row.wrapper);
}
render() {
var me = this;
this.make_form();
this.form_area.empty();
frappe.utils.scroll_to(0, false, 0, this.wrapper.find(".grid-form-body"));
this.layout = new frappe.ui.form.Layout({
fields: this.row.docfields,
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
grid: this.row.grid,
grid_row: this.row,
grid_row_form: this,
});
this.layout.make();
this.fields = this.layout.fields;
this.fields_dict = this.layout.fields_dict;
this.layout.refresh(this.row.doc);
// copy get_query to fields
for (var fieldname in this.row.grid.fieldinfo || {}) {
var fi = this.row.grid.fieldinfo[fieldname];
$.extend(me.fields_dict[fieldname], fi);
}
this.toggle_add_delete_button_display(this.wrapper);
this.row.grid.open_grid_row = this;
this.set_focus();
}
make_form() {
if (!this.form_area) {
let template = `<div class="grid-form-heading">
<div class="toolbar grid-header-toolbar">
<span class="panel-title">
${__("Editing Row")} #<span class="grid-form-row-index"></span></span>
<span class="row-actions">
<button class="btn btn-secondary btn-sm pull-right grid-collapse-row">
${frappe.utils.icon("down")}
</button>
<button class="btn btn-secondary btn-sm pull-right grid-move-row hidden-xs">
${__("Move")}</button>
<button class="btn btn-secondary btn-sm pull-right grid-duplicate-row hidden-xs">
${frappe.utils.icon("duplicate")}
${__("Duplicate")}
</button>
<button class="btn btn-secondary btn-sm pull-right grid-insert-row hidden-xs">
${__("Insert Above")}</button>
<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")}
</button>
</span>
</div>
</div>
<div class="grid-form-body">
<div class="form-area"></div>
<div class="grid-footer-toolbar hidden-xs flex justify-between">
<div class="grid-shortcuts">
<span> ${frappe.utils.icon("keyboard", "md")} </span>
<span class="text-medium"> ${__("Shortcuts")}: </span>
<kbd>${__("Ctrl + Up")}</kbd> . <kbd>${__("Ctrl + Down")}</kbd> . <kbd>${__("ESC")}</kbd>
</div>
<button class="btn btn-secondary btn-sm pull-right grid-append-row">
${__("Insert Below")}
</button>
</div>
</div>`;
$(template).appendTo(this.wrapper);
this.form_area = this.wrapper.find(".form-area");
this.row.set_row_index();
this.set_form_events();
}
}
set_form_events() {
var me = this;
this.wrapper.find(".grid-delete-row").on("click", function () {
me.row.remove();
return false;
});
this.wrapper.find(".grid-insert-row").on("click", function () {
me.row.insert(true);
return false;
});
this.wrapper.find(".grid-insert-row-below").on("click", function () {
me.row.insert(true, true);
return false;
});
this.wrapper.find(".grid-duplicate-row").on("click", function () {
me.row.insert(true, true, true);
return false;
});
this.wrapper.find(".grid-move-row").on("click", function () {
me.row.move();
return false;
});
this.wrapper.find(".grid-append-row").on("click", function () {
me.row.toggle_view(false);
me.row.grid.add_new_row(me.row.doc.idx + 1, null, true);
return false;
});
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function () {
me.row.toggle_view();
return false;
});
}
toggle_add_delete_button_display($parent) {
$parent.find(".row-actions, .grid-append-row").toggle(this.row.grid.is_editable());
}
refresh_field(fieldname) {
const field = this.fields_dict[fieldname];
if (!field) return;
field.docname = this.row.doc.name;
field.refresh();
this.layout && this.layout.refresh_dependency();
}
set_focus() {
// wait for animation and then focus on the first row
var me = this;
setTimeout(function () {
if ((me.row.frm && me.row.frm.doc.docstatus === 0) || !me.row.frm) {
var first = me.form_area.find("input:first");
if (
first.length &&
!["Date", "Datetime", "Time"].includes(first.attr("data-fieldtype"))
) {
try {
first.get(0).focus();
} catch (e) {
//
}
}
}
}, 500);
}
}

View File

@ -0,0 +1,135 @@
.form-grid-container.enhanced-grid-container {
// overflow-y: auto;
border: 1px solid var(--table-border-color);
border-radius: var(--border-radius-md);
overflow-x: clip !important;
.form-grid {
border: none;
border-radius: unset;
}
.form-grid.relative-important {
position: relative !important;
}
.grid-heading-row {
height: auto;
.grid-row .static-area.ellipsis {
text-overflow: unset;
white-space: normal;
}
}
.grid-heading-row .grid-static-col,
.grid-heading-row .row-check,
.grid-heading-row .row-index {
height: auto !important;
background-color: var(--subtle-fg);
}
.grid-row {
.data-row.row {
flex-wrap: nowrap;
// display: grid;
// grid-auto-flow: column;
.col {
max-width: 100px !important;
min-width: 100px !important;
border-bottom: 1px solid var(--table-border-color);
padding-bottom: 1px;
.field-area {
border-bottom: 1px solid var(--table-border-color);
}
}
.col.custom-1 {
min-width: 150px !important;
}
.col.custom-2 {
min-width: 200px !important;
}
.col.custom-3 {
min-width: 250px !important;
}
.col.custom-4 {
min-width: 300px !important;
}
.col.custom-5 {
min-width: 350px !important;
}
.col.custom-6 {
min-width: 400px !important;
}
.col.custom-7 {
min-width: 450px !important;
}
.col.custom-8 {
min-width: 500px !important;
}
.col.custom-9 {
min-width: 550px !important;
}
.col.custom-10 {
min-width: 600px !important;
}
.row-check,
.row-index {
max-width: 50px !important;
min-width: 50px !important;
}
}
}
.slidecontainer {
width: 100%;
}
.enhanced-slider {
-webkit-appearance: none;
width: 100%;
height: 8px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
.enhanced-slider:hover {
opacity: 1;
}
.enhanced-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30%;
height: 5px;
background-color: grey;
cursor: pointer;
}
.enhanced-slider::-moz-range-thumb {
width: 30%;
height: 5px;
background-color: grey;
cursor: pointer;
}
}