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.
 
 
 
 
 
 

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