You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

710 line
18 KiB

  1. // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. frappe.ui.form.get_open_grid_form = function() {
  4. return $(".grid-row-open").data("grid_row");
  5. }
  6. frappe.ui.form.close_grid_form = function() {
  7. var open_form = frappe.ui.form.get_open_grid_form();
  8. open_form && open_form.hide_form();
  9. // hide editable row too
  10. if(frappe.ui.form.editable_row) {
  11. frappe.ui.form.editable_row.toggle_editable_row(false);
  12. }
  13. }
  14. frappe.ui.form.Grid = Class.extend({
  15. init: function(opts) {
  16. var me = this;
  17. $.extend(this, opts);
  18. this.fieldinfo = {};
  19. this.doctype = this.df.options;
  20. if(this.doctype) {
  21. this.meta = frappe.get_meta(this.doctype);
  22. }
  23. this.fields_map = {};
  24. this.template = null;
  25. this.multiple_set = false;
  26. if(this.frm && this.frm.meta.__form_grid_templates
  27. && this.frm.meta.__form_grid_templates[this.df.fieldname]) {
  28. this.template = this.frm.meta.__form_grid_templates[this.df.fieldname];
  29. }
  30. this.is_grid = true;
  31. },
  32. allow_on_grid_editing: function() {
  33. if(frappe.utils.is_xs()) {
  34. return false;
  35. } else if(this.meta && this.meta.editable_grid || !this.meta) {
  36. return true;
  37. } else {
  38. return false;
  39. }
  40. },
  41. make: function() {
  42. var me = this;
  43. this.wrapper = $(frappe.render_template("grid_body", {}))
  44. .appendTo(this.parent)
  45. .attr("data-fieldname", this.df.fieldname);
  46. this.form_grid = this.wrapper.find('.form-grid');
  47. this.wrapper.find(".grid-add-row").click(function() {
  48. me.add_new_row(null, null, true);
  49. me.set_focus_on_row();
  50. return false;
  51. });
  52. this.custom_buttons = {};
  53. this.grid_buttons = this.wrapper.find('.grid-buttons');
  54. this.remove_rows_button = this.grid_buttons.find('.grid-remove-rows')
  55. this.setup_allow_bulk_edit();
  56. this.setup_check();
  57. if(this.df.on_setup) {
  58. this.df.on_setup(this);
  59. }
  60. },
  61. setup_check: function() {
  62. var me = this;
  63. this.wrapper.on('click', '.grid-row-check', function(e) {
  64. var $check = $(this);
  65. if($check.parents('.grid-heading-row:first').length!==0) {
  66. // select all?
  67. var checked = $check.prop('checked');
  68. $check.parents('.form-grid:first')
  69. .find('.grid-row-check').prop('checked', checked);
  70. // set all
  71. (me.grid_rows || []).forEach(function(row) { row.doc.__checked = checked ? 1 : 0; });
  72. } else {
  73. var docname = $check.parents('.grid-row:first').attr('data-name');
  74. me.grid_rows_by_docname[docname].select($check.prop('checked'));
  75. }
  76. me.refresh_remove_rows_button();
  77. });
  78. this.remove_rows_button.on('click', function() {
  79. var dirty = false;
  80. let tasks = [];
  81. me.get_selected().forEach((docname) => {
  82. tasks.push(() => {
  83. me.grid_rows_by_docname[docname].remove();
  84. dirty = true;
  85. });
  86. tasks.push(() => frappe.timeout(0.1));
  87. });
  88. tasks.push(() => {
  89. if (dirty) me.refresh();
  90. });
  91. frappe.run_serially(tasks);
  92. });
  93. },
  94. select_row: function(name) {
  95. this.grid_rows_by_docname[name].select();
  96. },
  97. remove_all: function() {
  98. this.grid_rows.forEach(row => {
  99. row.remove();
  100. });
  101. },
  102. refresh_remove_rows_button: function() {
  103. this.remove_rows_button.toggleClass('hide',
  104. this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
  105. },
  106. get_selected: function() {
  107. return (this.grid_rows || []).map(function(row) { return row.doc.__checked ? row.doc.name : null; })
  108. .filter(function(d) { return d; });
  109. },
  110. get_selected_children: function() {
  111. return (this.grid_rows || []).map(function(row) { return row.doc.__checked ? row.doc : null; })
  112. .filter(function(d) { return d; });
  113. },
  114. make_head: function() {
  115. // labels
  116. if(!this.header_row) {
  117. this.header_row = new frappe.ui.form.GridRow({
  118. parent: $(this.parent).find(".grid-heading-row"),
  119. parent_df: this.df,
  120. docfields: this.docfields,
  121. frm: this.frm,
  122. grid: this
  123. });
  124. }
  125. },
  126. refresh: function(force) {
  127. !this.wrapper && this.make();
  128. var me = this,
  129. $rows = $(me.parent).find(".rows"),
  130. data = this.get_data();
  131. this.setup_fields();
  132. if(this.frm) {
  133. this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc,
  134. this.perm);
  135. } else {
  136. // not in form
  137. this.display_status = 'Write';
  138. }
  139. if(this.display_status==="None") return;
  140. if(!force && this.data_rows_are_same(data)) {
  141. // soft refresh
  142. this.header_row && this.header_row.refresh();
  143. for(var i in this.grid_rows) {
  144. this.grid_rows[i].refresh();
  145. }
  146. } else {
  147. // redraw
  148. var _scroll_y = $(document).scrollTop();
  149. this.make_head();
  150. // to hide checkbox if grid is not editable
  151. this.header_row && this.header_row.toggle_check();
  152. if(!this.grid_rows) {
  153. this.grid_rows = [];
  154. }
  155. this.truncate_rows(data);
  156. this.grid_rows_by_docname = {};
  157. for(var ri=0; ri < data.length; ri++) {
  158. var d = data[ri];
  159. if(d.idx===undefined) {
  160. d.idx = ri + 1;
  161. }
  162. if(this.grid_rows[ri]) {
  163. var grid_row = this.grid_rows[ri];
  164. grid_row.doc = d;
  165. grid_row.refresh();
  166. } else {
  167. var grid_row = new frappe.ui.form.GridRow({
  168. parent: $rows,
  169. parent_df: this.df,
  170. docfields: this.docfields,
  171. doc: d,
  172. frm: this.frm,
  173. grid: this
  174. });
  175. this.grid_rows.push(grid_row);
  176. }
  177. this.grid_rows_by_docname[d.name] = grid_row;
  178. }
  179. this.wrapper.find(".grid-empty").toggleClass("hide", !!data.length);
  180. // toolbar
  181. this.setup_toolbar();
  182. // sortable
  183. if(this.frm && this.is_sortable() && !this.sortable_setup_done) {
  184. this.make_sortable($rows);
  185. this.sortable_setup_done = true;
  186. }
  187. this.last_display_status = this.display_status;
  188. this.last_docname = this.frm && this.frm.docname;
  189. frappe.utils.scroll_to(_scroll_y);
  190. }
  191. // red if mandatory
  192. this.form_grid.toggleClass('error', !!(this.df.reqd && !(data && data.length)));
  193. this.refresh_remove_rows_button();
  194. this.wrapper.trigger('change');
  195. },
  196. setup_toolbar: function() {
  197. if(this.is_editable()) {
  198. this.wrapper.find(".grid-footer").toggle(true);
  199. // show, hide buttons to add rows
  200. if(this.cannot_add_rows) {
  201. // add 'hide' to buttons
  202. this.wrapper.find(".grid-add-row, .grid-add-multiple-rows")
  203. .addClass('hide');
  204. } else {
  205. // show buttons
  206. this.wrapper.find(".grid-add-row").removeClass('hide');
  207. if(this.multiple_set) {
  208. this.wrapper.find(".grid-add-multiple-rows").removeClass('hide')
  209. }
  210. }
  211. } else {
  212. this.wrapper.find(".grid-footer").toggle(false);
  213. }
  214. },
  215. truncate_rows: function(data) {
  216. if(this.grid_rows.length > data.length) {
  217. // remove extra rows
  218. for(var i=data.length; i < this.grid_rows.length; i++) {
  219. var grid_row = this.grid_rows[i];
  220. grid_row.wrapper.remove();
  221. }
  222. this.grid_rows.splice(data.length);
  223. }
  224. },
  225. setup_fields: function() {
  226. var me = this;
  227. // reset docfield
  228. if (this.frm && this.frm.docname) {
  229. // use doc specific docfield object
  230. this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname,
  231. this.frm.docname);
  232. } else {
  233. // use non-doc specific docfield
  234. if(this.df.options) {
  235. this.df = frappe.meta.get_docfield(this.df.options, this.df.fieldname);
  236. }
  237. }
  238. if(this.doctype) {
  239. this.docfields = frappe.meta.get_docfields(this.doctype, this.frm.docname);
  240. } else {
  241. // fields given in docfield
  242. this.docfields = this.df.fields;
  243. }
  244. this.docfields.forEach(function(df) {
  245. me.fields_map[df.fieldname] = df;
  246. });
  247. },
  248. refresh_row: function(docname) {
  249. this.grid_rows_by_docname[docname] &&
  250. this.grid_rows_by_docname[docname].refresh();
  251. },
  252. data_rows_are_same: function(data) {
  253. if(this.grid_rows) {
  254. var same = data.length==this.grid_rows.length
  255. && this.display_status==this.last_display_status
  256. && (this.frm && this.frm.docname==this.last_docname)
  257. && !$.map(this.grid_rows, function(g, i) {
  258. return (g && g.doc && g.doc.name==data[i].name) ? null : true;
  259. }).length;
  260. return same;
  261. }
  262. },
  263. make_sortable: function($rows) {
  264. var me =this;
  265. if ('ontouchstart' in window) {
  266. return;
  267. }
  268. new Sortable($rows.get(0), {
  269. group: {name: me.df.fieldname},
  270. handle: '.sortable-handle',
  271. draggable: '.grid-row',
  272. filter: 'li, a',
  273. onUpdate: function(event, ui) {
  274. me.frm.doc[me.df.fieldname] = [];
  275. $rows.find(".grid-row").each(function(i, item) {
  276. var doc = locals[me.doctype][$(item).attr('data-name')];
  277. doc.idx = i + 1;
  278. me.frm.doc[me.df.fieldname].push(doc);
  279. });
  280. // re-order grid-rows by name
  281. me.grid_rows = [];
  282. me.frm.doc[me.df.fieldname].forEach(function(d) {
  283. me.grid_rows.push(me.grid_rows_by_docname[d.name]);
  284. });
  285. me.frm.script_manager.trigger(me.df.fieldname + "_move", me.df.options, me.frm.doc[me.df.fieldname][event.newIndex].name);
  286. me.refresh();
  287. me.frm.dirty();
  288. }
  289. });
  290. $(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
  291. },
  292. get_data: function() {
  293. var data = this.frm ?
  294. this.frm.doc[this.df.fieldname] || []
  295. : this.df.get_data();
  296. data.sort(function(a, b) { return a.idx - b.idx});
  297. return data;
  298. },
  299. set_column_disp: function(fieldname, show) {
  300. if($.isArray(fieldname)) {
  301. var me = this;
  302. for(var i=0, l=fieldname.length; i<l; i++) {
  303. var fname = fieldname[i];
  304. me.get_docfield(fname).hidden = show ? 0 : 1;
  305. this.set_editable_grid_column_disp(fname, show);
  306. }
  307. } else {
  308. this.get_docfield(fieldname).hidden = show ? 0 : 1;
  309. this.set_editable_grid_column_disp(fieldname, show);
  310. }
  311. this.refresh(true);
  312. },
  313. set_editable_grid_column_disp: function(fieldname, show) {
  314. //Hide columns for editable grids
  315. if (this.meta.editable_grid && this.grid_rows) {
  316. this.grid_rows.forEach(function(row) {
  317. row.columns_list.forEach(function(column) {
  318. //Hide the column specified
  319. if (column.df.fieldname == fieldname) {
  320. if (show) {
  321. column.df.hidden = false;
  322. //Show the static area and hide field area if it is not the editable row
  323. if (row != frappe.ui.form.editable_row) {
  324. column.static_area.show();
  325. column.field_area && column.field_area.toggle(false);
  326. }
  327. //Hide the static area and show field area if it is the editable row
  328. else {
  329. column.static_area.hide();
  330. column.field_area && column.field_area.toggle(true);
  331. //Format the editable column appropriately if it is now visible
  332. if (column.field) {
  333. column.field.refresh();
  334. if (column.field.$input) column.field.$input.toggleClass('input-sm', true);
  335. }
  336. }
  337. }
  338. else {
  339. column.df.hidden = true;
  340. column.static_area.hide();
  341. }
  342. }
  343. });
  344. });
  345. }
  346. this.refresh();
  347. },
  348. toggle_reqd: function(fieldname, reqd) {
  349. this.get_docfield(fieldname).reqd = reqd;
  350. this.refresh();
  351. },
  352. toggle_enable: function(fieldname, enable) {
  353. this.get_docfield(fieldname).read_only = enable ? 0 : 1;
  354. this.refresh();
  355. },
  356. toggle_display: function(fieldname, show) {
  357. this.get_docfield(fieldname).hidden = show ? 0 : 1;
  358. this.refresh();
  359. },
  360. get_docfield: function(fieldname) {
  361. return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null);
  362. },
  363. get_row: function(key) {
  364. if(typeof key == 'number') {
  365. if(key < 0) {
  366. return this.grid_rows[this.grid_rows.length + key];
  367. } else {
  368. return this.grid_rows[key];
  369. }
  370. } else {
  371. return this.grid_rows_by_docname[key];
  372. }
  373. },
  374. get_grid_row: function(key) {
  375. return this.get_row(key);
  376. },
  377. get_field: function(fieldname) {
  378. // Note: workaround for get_query
  379. if(!this.fieldinfo[fieldname])
  380. this.fieldinfo[fieldname] = {
  381. }
  382. return this.fieldinfo[fieldname];
  383. },
  384. set_value: function(fieldname, value, doc) {
  385. if(this.display_status!=="None" && this.grid_rows_by_docname[doc.name]) {
  386. this.grid_rows_by_docname[doc.name].refresh_field(fieldname, value);
  387. }
  388. },
  389. add_new_row: function(idx, callback, show) {
  390. if(this.is_editable()) {
  391. if(this.frm) {
  392. var d = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname, idx);
  393. d.__unedited = true;
  394. this.frm.script_manager.trigger(this.df.fieldname + "_add", d.doctype, d.name);
  395. this.refresh();
  396. } else {
  397. this.df.data.push({name: "batch " + (this.df.data.length+1), idx: this.df.data.length+1});
  398. this.refresh();
  399. }
  400. if(show) {
  401. if(idx) {
  402. // always open inserted rows
  403. this.wrapper.find("[data-idx='"+idx+"']").data("grid_row")
  404. .toggle_view(true, callback);
  405. } else {
  406. if(!this.allow_on_grid_editing()) {
  407. // open last row only if on-grid-editing is disabled
  408. this.wrapper.find(".grid-row:last").data("grid_row")
  409. .toggle_view(true, callback);
  410. }
  411. }
  412. }
  413. return d;
  414. }
  415. },
  416. set_focus_on_row: function(idx) {
  417. var me = this;
  418. if(!idx) {
  419. idx = me.grid_rows.length - 1;
  420. }
  421. setTimeout(function() {
  422. me.grid_rows[idx].row
  423. .find('input[type="Text"],textarea,select').filter(':visible:first').focus();
  424. }, 100);
  425. },
  426. setup_visible_columns: function() {
  427. var total_colsize = 1,
  428. fields = this.editable_fields || this.docfields;
  429. this.visible_columns = [];
  430. for(var ci in fields) {
  431. var _df = fields[ci];
  432. // get docfield if from fieldname
  433. df = this.fields_map[_df.fieldname];
  434. if(!df.hidden
  435. && (this.editable_fields || df.in_list_view)
  436. && (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm)
  437. && !in_list(frappe.model.layout_fields, df.fieldtype)) {
  438. if(df.columns) {
  439. df.colsize=df.columns;
  440. }
  441. else {
  442. var colsize=2;
  443. switch(df.fieldtype) {
  444. case"Text":
  445. case"Small Text":
  446. colsize=3;
  447. break;
  448. case"Check":
  449. colsize=1
  450. }
  451. df.colsize=colsize;
  452. }
  453. if(df.columns) {
  454. df.colsize=df.columns;
  455. }
  456. else {
  457. var colsize = 2;
  458. switch(df.fieldtype) {
  459. case "Text":
  460. case "Small Text": colsize = 3; break;
  461. case"Check": colsize = 1;
  462. }
  463. df.colsize = colsize;
  464. }
  465. total_colsize += df.colsize;
  466. if(total_colsize > 11)
  467. return false;
  468. this.visible_columns.push([df, df.colsize]);
  469. }
  470. }
  471. // redistribute if total-col size is less than 12
  472. var passes = 0;
  473. while(total_colsize < 11 && passes < 12) {
  474. for(var i in this.visible_columns) {
  475. var df = this.visible_columns[i][0];
  476. var colsize = this.visible_columns[i][1];
  477. if(colsize > 1 && colsize < 11
  478. && !in_list(frappe.model.std_fields_list, df.fieldname)) {
  479. if (passes < 3 && ["Int", "Currency", "Float", "Check", "Percent"].indexOf(df.fieldtype)!==-1) {
  480. // don't increase col size of these fields in first 3 passes
  481. continue;
  482. }
  483. this.visible_columns[i][1] += 1;
  484. total_colsize++;
  485. }
  486. if(total_colsize > 10)
  487. break;
  488. }
  489. passes++;
  490. }
  491. },
  492. is_editable: function() {
  493. return this.display_status=="Write" && !this.static_rows;
  494. },
  495. is_sortable: function() {
  496. return this.sortable_status || this.is_editable();
  497. },
  498. only_sortable: function(status) {
  499. if(status===undefined ? true : status) {
  500. this.sortable_status = true;
  501. this.static_rows = true;
  502. }
  503. },
  504. set_multiple_add: function(link, qty) {
  505. if(this.multiple_set) return;
  506. var me = this;
  507. var link_field = frappe.meta.get_docfield(this.df.options, link);
  508. var btn = $(this.wrapper).find(".grid-add-multiple-rows");
  509. // show button
  510. btn.removeClass('hide');
  511. // open link selector on click
  512. btn.on("click", function() {
  513. new frappe.ui.form.LinkSelector({
  514. doctype: link_field.options,
  515. fieldname: link,
  516. qty_fieldname: qty,
  517. target: me,
  518. txt: ""
  519. });
  520. return false;
  521. });
  522. this.multiple_set = true;
  523. },
  524. setup_allow_bulk_edit: function() {
  525. var me = this;
  526. if(this.frm && this.frm.get_docfield(this.df.fieldname).allow_bulk_edit) {
  527. // download
  528. me.setup_download();
  529. // upload
  530. frappe.flags.no_socketio = true;
  531. $(this.wrapper).find(".grid-upload").removeClass("hide").on("click", function() {
  532. frappe.prompt({fieldtype:"Attach", label:"Upload File"},
  533. function(data) {
  534. var data = frappe.utils.csv_to_array(frappe.upload.get_string(data.upload_file));
  535. // row #2 contains fieldnames;
  536. var fieldnames = data[2];
  537. me.frm.clear_table(me.df.fieldname);
  538. $.each(data, function(i, row) {
  539. if(i > 6) {
  540. var blank_row = true;
  541. $.each(row, function(ci, value) {
  542. if(value) {
  543. blank_row = false;
  544. return false;
  545. }
  546. });
  547. if(!blank_row) {
  548. var d = me.frm.add_child(me.df.fieldname);
  549. $.each(row, function(ci, value) {
  550. var fieldname = fieldnames[ci];
  551. var df = frappe.meta.get_docfield(me.df.options, fieldname);
  552. // convert date formatting
  553. if(df.fieldtype==="Date" && value) {
  554. value = frappe.datetime.user_to_str(value);
  555. }
  556. if(df.fieldtype==="Int" || df.fieldtype==="Check") {
  557. value = cint(value);
  558. }
  559. d[fieldnames[ci]] = value;
  560. });
  561. }
  562. }
  563. });
  564. me.frm.refresh_field(me.df.fieldname);
  565. frappe.msgprint({message:__('Table updated'), title:__('Success'), indicator:'green'})
  566. }, __("Edit via Upload"), __("Update"));
  567. return false;
  568. });
  569. }
  570. },
  571. setup_download: function() {
  572. var me = this;
  573. let title = me.df.label || frappe.model.unscrub(me.df.fieldname);
  574. $(this.wrapper).find(".grid-download").removeClass("hide").on("click", function() {
  575. var data = [];
  576. var docfields = [];
  577. data.push([__("Bulk Edit {0}", [title])]);
  578. data.push([]);
  579. data.push([]);
  580. data.push([]);
  581. data.push([__("The CSV format is case sensitive")]);
  582. data.push([__("Do not edit headers which are preset in the template")]);
  583. data.push(["------"]);
  584. $.each(frappe.get_meta(me.df.options).fields, function(i, df) {
  585. if(frappe.model.is_value_type(df.fieldtype)) {
  586. data[1].push(df.label);
  587. data[2].push(df.fieldname);
  588. data[3].push(df.description || "");
  589. docfields.push(df);
  590. }
  591. });
  592. // add data
  593. $.each(me.frm.doc[me.df.fieldname] || [], function(i, d) {
  594. var row = [];
  595. $.each(data[2], function(i, fieldname) {
  596. var value = d[fieldname];
  597. // format date
  598. if(docfields[i].fieldtype==="Date" && value) {
  599. value = frappe.datetime.str_to_user(value);
  600. }
  601. row.push(value || "");
  602. });
  603. data.push(row);
  604. });
  605. frappe.tools.downloadify(data, null, title);
  606. return false;
  607. });
  608. },
  609. add_custom_button: function(label, click) {
  610. // add / unhide a custom button
  611. var btn = this.custom_buttons[label];
  612. if(!btn) {
  613. btn = $('<button class="btn btn-default btn-xs btn-custom">' + label + '</button>')
  614. .css('margin-right', '4px')
  615. .prependTo(this.grid_buttons)
  616. .on('click', click);
  617. this.custom_buttons[label] = btn;
  618. } else {
  619. btn.removeClass('hidden');
  620. }
  621. },
  622. clear_custom_buttons: function() {
  623. // hide all custom buttons
  624. this.grid_buttons.find('.btn-custom').addClass('hidden');
  625. }
  626. });