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.
 
 
 
 
 
 

632 rivejä
17 KiB

  1. // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. frappe.provide('frappe.views');
  4. // Renders customized list
  5. // usually based on `in_list_view` property
  6. frappe.views.ListRenderer = Class.extend({
  7. name: 'List',
  8. init: function (opts) {
  9. $.extend(this, opts);
  10. this.meta = frappe.get_meta(this.doctype);
  11. this.init_settings();
  12. this.set_defaults();
  13. this.set_fields();
  14. this.set_columns();
  15. this.setup_cache();
  16. },
  17. set_defaults: function () {
  18. var me = this;
  19. this.page_title = __(this.doctype);
  20. this.set_wrapper();
  21. this.setup_filterable();
  22. this.prepare_render_view();
  23. // flag to enable/disable realtime updates in list_view
  24. this.no_realtime = false;
  25. // set false to render view even if no results
  26. // e.g Calendar
  27. this.show_no_result = true;
  28. // hide sort selector
  29. this.hide_sort_selector = false;
  30. // default settings
  31. this.order_by = this.order_by || 'modified desc';
  32. this.filters = this.filters || [];
  33. this.page_length = this.page_length || 20;
  34. },
  35. setup_cache: function () {
  36. frappe.provide('frappe.views.list_renderers.' + this.doctype);
  37. frappe.views.list_renderers[this.doctype][this.list_view.current_view] = this;
  38. },
  39. init_settings: function () {
  40. this.settings = frappe.listview_settings[this.doctype] || {};
  41. if(!("selectable" in this.settings)) {
  42. this.settings.selectable = true;
  43. }
  44. this.init_user_settings();
  45. this.order_by = this.user_settings.order_by || this.settings.order_by;
  46. this.filters = this.user_settings.filters || this.settings.filters;
  47. this.page_length = this.settings.page_length;
  48. // default filter for submittable doctype
  49. if(frappe.model.is_submittable(this.doctype) && (!this.filters || !this.filters.length)) {
  50. this.filters = [[this.doctype, "docstatus", "!=", 2]];
  51. }
  52. },
  53. init_user_settings: function () {
  54. frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.name);
  55. this.user_settings = frappe.get_user_settings(this.doctype)[this.name];
  56. },
  57. after_refresh: function() {
  58. // called after refresh in list_view
  59. },
  60. before_refresh: function() {
  61. // called before refresh in list_view
  62. },
  63. should_refresh: function() {
  64. return this.list_view.current_view !== this.list_view.last_view;
  65. },
  66. load_last_view: function() {
  67. // this function should handle loading the last view of your list_renderer,
  68. // If you have a last view (for e.g last kanban board in Kanban View),
  69. // load it using frappe.set_route and return true
  70. // else return false
  71. return false;
  72. },
  73. set_wrapper: function () {
  74. this.wrapper = this.list_view.wrapper && this.list_view.wrapper.find('.result-list');
  75. },
  76. set_fields: function () {
  77. var me = this;
  78. var tabDoctype = '`tab' + this.doctype + '`.';
  79. this.fields = [];
  80. this.stats = ['_user_tags'];
  81. var add_field = function (fieldname) {
  82. if (!fieldname.includes('`tab')) {
  83. fieldname = tabDoctype + '`' + fieldname + '`';
  84. }
  85. if (!me.fields.includes(fieldname))
  86. me.fields.push(fieldname);
  87. }
  88. var defaults = [
  89. 'name',
  90. 'owner',
  91. 'docstatus',
  92. '_user_tags',
  93. '_comments',
  94. 'modified',
  95. 'modified_by',
  96. '_assign',
  97. '_liked_by',
  98. '_seen'
  99. ];
  100. defaults.map(add_field);
  101. // add title field
  102. if (this.meta.title_field) {
  103. this.title_field = this.meta.title_field;
  104. add_field(this.meta.title_field);
  105. }
  106. if (this.meta.image_field) {
  107. add_field(this.meta.image_field);
  108. }
  109. // enabled / disabled
  110. if (frappe.meta.has_field(this.doctype, 'enabled')) { add_field('enabled'); }
  111. if (frappe.meta.has_field(this.doctype, 'disabled')) { add_field('disabled'); }
  112. // add workflow field (as priority)
  113. this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype);
  114. if (this.workflow_state_fieldname) {
  115. if (!frappe.workflow.workflows[this.doctype]['override_status']) {
  116. add_field(this.workflow_state_fieldname);
  117. }
  118. this.stats.push(this.workflow_state_fieldname);
  119. }
  120. this.meta.fields.forEach(function (df, i) {
  121. if (df.in_list_view && frappe.perm.has_perm(me.doctype, df.permlevel, 'read')) {
  122. if (df.fieldtype == 'Image' && df.options) {
  123. add_field(df.options);
  124. } else {
  125. add_field(df.fieldname);
  126. }
  127. // currency field for symbol (multi-currency)
  128. if (df.fieldtype == 'Currency' && df.options) {
  129. if (df.options.includes(':')) {
  130. add_field(df.options.split(':')[1]);
  131. } else {
  132. add_field(df.options);
  133. }
  134. }
  135. }
  136. });
  137. // additional fields
  138. if (this.settings.add_fields) {
  139. this.settings.add_fields.forEach(add_field);
  140. }
  141. // kanban column fields
  142. if (me.meta.__kanban_column_fields) {
  143. me.meta.__kanban_column_fields.map(add_field);
  144. }
  145. },
  146. set_columns: function () {
  147. var me = this;
  148. this.columns = [];
  149. var name_column = {
  150. colspan: this.settings.colwidths && this.settings.colwidths.subject || 6,
  151. type: 'Subject',
  152. title: 'Name'
  153. };
  154. if (this.meta.title_field) {
  155. name_column.title = frappe.meta.get_docfield(this.doctype, this.meta.title_field).label;
  156. }
  157. this.columns.push(name_column);
  158. if (frappe.has_indicator(this.doctype)) {
  159. // indicator
  160. this.columns.push({
  161. colspan: this.settings.colwidths && this.settings.colwidths.indicator || 3,
  162. type: 'Indicator',
  163. title: 'Status'
  164. });
  165. }
  166. // total_colspans
  167. this.total_colspans = this.columns.reduce(function (total, curr) {
  168. return total + curr.colspan;
  169. }, 0);
  170. // overridden
  171. var overridden = (this.settings.add_columns || []).map(function (d) {
  172. return d.content;
  173. });
  174. // custom fields in list_view
  175. var docfields_in_list_view =
  176. frappe.get_children('DocType', this.doctype, 'fields', { 'in_list_view': 1 })
  177. .sort(function (a, b) {
  178. return a.idx - b.idx
  179. });
  180. docfields_in_list_view.forEach(function (d) {
  181. if (overridden.includes(d.fieldname) || d.fieldname === me.title_field) {
  182. return;
  183. }
  184. if (me.total_colspans < 12) {
  185. me.add_column(d);
  186. }
  187. });
  188. // additional columns
  189. if (this.settings.add_columns) {
  190. this.settings.add_columns.forEach(function (d) {
  191. if (me.total_colspans < 12) {
  192. if (typeof d === 'string') {
  193. me.add_column(frappe.meta.get_docfield(me.doctype, d));
  194. } else {
  195. me.columns.push(d);
  196. me.total_colspans += parseInt(d.colspan);
  197. }
  198. }
  199. });
  200. }
  201. // distribute remaining columns
  202. var empty_cols = flt(12 - this.total_colspans);
  203. while (empty_cols > 0) {
  204. this.columns = this.columns.map(function (col) {
  205. if (empty_cols > 0) {
  206. col.colspan = cint(col.colspan) + 1;
  207. empty_cols = empty_cols - 1;
  208. }
  209. return col;
  210. });
  211. }
  212. // Remove duplicates
  213. this.columns = this.columns.uniqBy(col => col.title);
  214. // Remove TextEditor field columns
  215. this.columns = this.columns.filter(col => col.fieldtype !== 'Text Editor');
  216. // Remove color field
  217. this.columns = this.columns.filter(col => col.fieldtype !== 'Color');
  218. // Limit number of columns to 4
  219. this.columns = this.columns.slice(0, 4);
  220. },
  221. add_column: function (df) {
  222. // field width
  223. var colspan = 3;
  224. if (in_list(['Int', 'Percent'], df.fieldtype)) {
  225. colspan = 2;
  226. } else if (in_list(['Check', 'Image'], df.fieldtype)) {
  227. colspan = 1;
  228. } else if (in_list(['name', 'subject', 'title'], df.fieldname)) {
  229. // subjects are longer
  230. colspan = 4;
  231. } else if (df.fieldtype == 'Text Editor' || df.fieldtype == 'Text') {
  232. colspan = 4;
  233. }
  234. if (df.columns && df.columns > 0) {
  235. colspan = df.columns;
  236. } else if (this.settings.column_colspan && this.settings.column_colspan[df.fieldname]) {
  237. colspan = this.settings.column_colspan[df.fieldname];
  238. } else {
  239. colspan = 2;
  240. }
  241. this.total_colspans += parseInt(colspan);
  242. var col = {
  243. colspan: colspan,
  244. content: df.fieldname,
  245. type: df.fieldtype,
  246. df: df,
  247. fieldtype: df.fieldtype,
  248. fieldname: df.fieldname,
  249. title: __(df.label)
  250. };
  251. if (this.settings.column_render && this.settings.column_render[df.fieldname]) {
  252. col.render = this.settings.column_render[df.fieldname];
  253. }
  254. this.columns.push(col);
  255. },
  256. setup_filterable: function () {
  257. var me = this;
  258. this.list_view.wrapper &&
  259. this.list_view.wrapper.on('click', '.result-list .filterable', function (e) {
  260. e.stopPropagation();
  261. var filters = $(this).attr('data-filter').split('|');
  262. var added = false;
  263. filters.forEach(function (f) {
  264. f = f.split(',');
  265. if (f[2] === 'Today') {
  266. f[2] = frappe.datetime.get_today();
  267. } else if (f[2] == 'User') {
  268. f[2] = frappe.session.user;
  269. }
  270. var new_filter = me.list_view.filter_list
  271. .add_filter(me.doctype, f[0], f[1], f.slice(2).join(','));
  272. if (new_filter) {
  273. // set it to true if atleast 1 filter is added
  274. added = true;
  275. }
  276. });
  277. if (added) {
  278. me.list_view.refresh(true);
  279. }
  280. });
  281. this.list_view.wrapper &&
  282. this.list_view.wrapper.on('click', '.list-item', function (e) {
  283. // don't open in case of checkbox, like, filterable
  284. if ($(e.target).hasClass('filterable')
  285. || $(e.target).hasClass('octicon-heart')
  286. || $(e.target).is(':checkbox')) {
  287. return;
  288. }
  289. var link = $(this).parent().find('a.list-id').get(0);
  290. window.location.href = link.href;
  291. return false;
  292. });
  293. },
  294. render_view: function (values) {
  295. var me = this;
  296. var $list_items = me.wrapper.find('.list-items');
  297. if($list_items.length === 0) {
  298. $list_items = $(`
  299. <div class="list-items">
  300. </div>
  301. `);
  302. me.wrapper.append($list_items);
  303. }
  304. values.map(value => {
  305. const $item = $(this.get_item_html(value));
  306. const $item_container = $('<div class="list-item-container">').append($item);
  307. $list_items.append($item_container);
  308. if (this.settings.post_render_item) {
  309. this.settings.post_render_item(this, $item_container, value);
  310. }
  311. this.render_tags($item_container, value);
  312. });
  313. this.render_count(values.length);
  314. },
  315. render_count: function(current_count) {
  316. console.log(this)
  317. const $header_right = this.list_view.list_header.find('.list-item__content--activity');
  318. frappe.call({
  319. method: 'frappe.model.db_query.get_count',
  320. args: {
  321. doctype: this.doctype,
  322. filters: this.list_view.get_filters_args()
  323. }
  324. }).then(r => {
  325. const count = r.message;
  326. const $html = $(`<span>${current_count} of ${count}</span>`);
  327. $html.css({
  328. marginRight: '10px'
  329. })
  330. $header_right.addClass('text-muted');
  331. $header_right.html($html);
  332. })
  333. },
  334. // returns html for a data item,
  335. // usually based on a template
  336. get_item_html: function (data) {
  337. var main = this.columns.map(column =>
  338. frappe.render_template('list_item_main', {
  339. data: data,
  340. col: column,
  341. value: data[column.fieldname],
  342. formatters: this.settings.formatters,
  343. subject: this.get_subject_html(data, true),
  344. indicator: this.get_indicator_html(data),
  345. })
  346. ).join("");
  347. return frappe.render_template('list_item_row', {
  348. data: data,
  349. main: main,
  350. settings: this.settings,
  351. meta: this.meta,
  352. indicator_dot: this.get_indicator_dot(data),
  353. })
  354. },
  355. get_header_html: function () {
  356. var main = this.columns.map(column =>
  357. frappe.render_template('list_item_main_head', {
  358. col: column,
  359. _checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable)
  360. && !this.no_delete)
  361. })
  362. ).join("");
  363. return frappe.render_template('list_item_row_head', { main: main, list: this });
  364. },
  365. render_tags: function (element, data) {
  366. var me = this;
  367. var tag_row = $(`<div class='tag-row'>
  368. <div class='list-tag hidden-xs'></div>
  369. <div class='clearfix'></div>
  370. </div>`).appendTo(element);
  371. if (!me.list_view.tags_shown) {
  372. tag_row.addClass('hide');
  373. }
  374. // add tags
  375. var tag_editor = new frappe.ui.TagEditor({
  376. parent: tag_row.find('.list-tag'),
  377. frm: {
  378. doctype: this.doctype,
  379. docname: data.name
  380. },
  381. list_sidebar: me.list_view.list_sidebar,
  382. user_tags: data._user_tags,
  383. on_change: function (user_tags) {
  384. data._user_tags = user_tags;
  385. }
  386. });
  387. tag_editor.wrapper.on('click', '.tagit-label', function () {
  388. me.list_view.set_filter('_user_tags', $(this).text());
  389. });
  390. },
  391. get_subject_html: function (data, without_workflow) {
  392. data._without_workflow = without_workflow;
  393. return frappe.render_template('list_item_subject', data);
  394. },
  395. get_indicator_html: function (doc) {
  396. var indicator = frappe.get_indicator(doc, this.doctype);
  397. if (indicator) {
  398. return `<span class='indicator ${indicator[1]} filterable'
  399. data-filter='${indicator[2]}'>
  400. ${__(indicator[0])}
  401. <span>`;
  402. }
  403. return '';
  404. },
  405. get_indicator_dot: function (doc) {
  406. var indicator = frappe.get_indicator(doc, this.doctype);
  407. if (!indicator) {
  408. return '';
  409. }
  410. return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`;
  411. },
  412. prepare_data: function (data) {
  413. if (data.modified) {
  414. this.prepare_when(data, data.modified);
  415. }
  416. // nulls as strings
  417. for (var key in data) {
  418. if (data[key] == null) {
  419. data[key] = '';
  420. }
  421. }
  422. data.doctype = this.doctype;
  423. data._liked_by = JSON.parse(data._liked_by || '[]');
  424. data._checkbox = (frappe.model.can_delete(this.doctype) || this.settings.selectable) && !this.no_delete
  425. data._doctype_encoded = encodeURIComponent(data.doctype);
  426. data._name = data.name.replace(/'/g, '\'');
  427. data._name_encoded = encodeURIComponent(data.name);
  428. data._submittable = frappe.model.is_submittable(this.doctype);
  429. var title_field = this.meta.title_field || 'name';
  430. data._title = strip_html(data[title_field] || data.name);
  431. // check for duplicates
  432. // add suffix like (1), (2) etc
  433. if (data.name && this.values_map) {
  434. if (this.values_map[data.name]!==undefined) {
  435. if (this.values_map[data.name]===1) {
  436. // update first row!
  437. this.set_title_with_row_number(this.rows_map[data.name], 1);
  438. }
  439. this.values_map[data.name]++;
  440. this.set_title_with_row_number(data, this.values_map[data.name]);
  441. } else {
  442. this.values_map[data.name] = 1;
  443. this.rows_map[data.name] = data;
  444. }
  445. }
  446. data._full_title = data._title;
  447. data._workflow = null;
  448. if (this.workflow_state_fieldname) {
  449. data._workflow = {
  450. fieldname: this.workflow_state_fieldname,
  451. value: data[this.workflow_state_fieldname],
  452. style: frappe.utils.guess_style(data[this.workflow_state_fieldname])
  453. }
  454. }
  455. data._user = frappe.session.user;
  456. if(!data._user_tags) data._user_tags = "";
  457. data._tags = data._user_tags.split(',').filter(function (v) {
  458. // filter falsy values
  459. return v;
  460. });
  461. data.css_seen = '';
  462. if (data._seen) {
  463. var seen = JSON.parse(data._seen);
  464. if (seen && in_list(seen, data._user)) {
  465. data.css_seen = 'seen'
  466. }
  467. }
  468. // whether to hide likes/comments/assignees
  469. data._hide_activity = 0;
  470. data._assign_list = JSON.parse(data._assign || '[]');
  471. // prepare data in settings
  472. if (this.settings.prepare_data)
  473. this.settings.prepare_data(data);
  474. return data;
  475. },
  476. set_title_with_row_number: function (data, id) {
  477. data._title = data._title + ` (${__("Row")} ${id})`;
  478. data._full_title = data._title;
  479. },
  480. prepare_when: function (data, date_str) {
  481. if (!date_str) date_str = data.modified;
  482. // when
  483. data.when = (frappe.datetime.str_to_user(date_str)).split(' ')[0];
  484. var diff = frappe.datetime.get_diff(frappe.datetime.get_today(), date_str.split(' ')[0]);
  485. if (diff === 0) {
  486. data.when = comment_when(date_str);
  487. }
  488. if (diff === 1) {
  489. data.when = __('Yesterday')
  490. }
  491. if (diff === 2) {
  492. data.when = __('2 days ago')
  493. }
  494. },
  495. // for views which require 3rd party libs
  496. required_libs: null,
  497. prepare_render_view: function () {
  498. var me = this;
  499. this._render_view = this.render_view;
  500. var lib_exists = (typeof this.required_libs === 'string' && this.required_libs)
  501. || ($.isArray(this.required_libs) && this.required_libs.length);
  502. this.render_view = function (values) {
  503. me.values_map = {};
  504. me.rows_map = {};
  505. // prepare data before rendering view
  506. values = values.map(me.prepare_data.bind(this));
  507. // remove duplicates
  508. // values = values.uniqBy(value => value.name);
  509. if (lib_exists) {
  510. me.load_lib(function () {
  511. me._render_view(values);
  512. });
  513. } else {
  514. me._render_view(values);
  515. }
  516. }.bind(this);
  517. },
  518. load_lib: function (callback) {
  519. frappe.require(this.required_libs, callback);
  520. },
  521. render_bar_graph: function (parent, data, field, label) {
  522. var args = {
  523. percent: data[field],
  524. label: __(label)
  525. }
  526. $(parent).append(`<span class='progress' style='width: 100 %; float: left; margin: 5px 0px;'> \
  527. <span class='progress- bar' title='${args.percent}% ${args.label}' \
  528. style='width: ${args.percent}%;'></span>\
  529. </span>`);
  530. },
  531. render_icon: function (parent, icon_class, label) {
  532. var icon_html = `<i class='${icon_class}' title='${__(label) || ''}'></i>`;
  533. $(parent).append(icon_html);
  534. },
  535. make_no_result: function () {
  536. var new_button = frappe.boot.user.can_create.includes(this.doctype)
  537. ? (`<p><button class='btn btn-primary btn-sm'
  538. list_view_doc='${this.doctype}'>
  539. ${__('Make a new {0}', [__(this.doctype)])}
  540. </button></p>`)
  541. : '';
  542. var no_result_message =
  543. `<div class='msg-box no-border'>
  544. <p>${__('No {0} found', [__(this.doctype)])}</p>
  545. ${new_button}
  546. </div>`;
  547. return no_result_message;
  548. },
  549. });