diff --git a/.eslintrc b/.eslintrc index fb129c477f..500346a0c3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -120,6 +120,7 @@ "QUnit": true, "JsBarcode": true, "L": true, - "Chart": true + "Chart": true, + "DataTable": true } } diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index d3275e6968..f297bf0836 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -355,7 +355,7 @@ "in_filter": 0, "in_global_search": 0, "in_list_view": 0, - "in_standard_filter": 1, + "in_standard_filter": 0, "label": "Folder", "length": 0, "no_copy": 0, @@ -653,7 +653,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-10-27 13:27:43.882914", + "modified": "2017-12-07 17:01:54.860204", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/file/file_list.js index 61b4a58a0c..e69de29bb2 100644 --- a/frappe/core/doctype/file/file_list.js +++ b/frappe/core/doctype/file/file_list.js @@ -1,239 +0,0 @@ -frappe.provide("frappe.ui"); - -frappe.listview_settings['File'] = { - hide_name_column: true, - use_route: true, - add_fields: ["is_folder", "file_name", "file_url", "folder", "is_private"], - formatters: { - file_size: function(value) { - // formatter for file size - if(value > 1048576) { - value = flt(flt(value) / 1048576, 1) + "M"; - } else if (value > 1024) { - value = flt(flt(value) / 1024, 1) + "K"; - } - return value; - } - }, - prepare_data: function(data) { - // set image icons - var icon = ""; - - if(data.is_folder) { - icon += ' '; - } else if(frappe.utils.is_image_file(data.file_name)) { - icon += ' '; - } else { - icon += ' '; - } - - data._title = icon + (data.file_name ? data.file_name : data.file_url); - - if (data.is_private) { - data._title += ' '; - } - }, - onload: function(doclist) { - doclist.filter_area = doclist.wrapper.find(".show_filters"); - - doclist.breadcrumb = $('') - .insertBefore(doclist.filter_area); - doclist.list_renderer.settings.setup_menu(doclist); - doclist.list_renderer.settings.setup_dragdrop(doclist); - - doclist.$page.on("click", ".list-row-checkbox", function(event) { - doclist.list_renderer.settings.add_menu_item_copy(doclist); - }); - }, - list_view_doc:function(doclist){ - $(doclist.wrapper).on("click", 'button[list_view_doc="'+doclist.doctype+'"]', function() { - frappe.ui.get_upload_dialog({ - "args": { - "folder": doclist.current_folder, - "from_form": 1 - }, - callback: function() { - doclist.refresh(); - } - }); - }); - }, - setup_menu: function(doclist) { - doclist.page.add_menu_item(__("New Folder"), function() { - var d = frappe.prompt(__("Name"), function(values) { - if((values.value.indexOf("/") > -1)){ - frappe.throw(__("Folder name should not include '/' (slash)")); - return; - } - var data = { - "file_name": values.value, - "folder": doclist.current_folder - }; - frappe.call({ - method: "frappe.core.doctype.file.file.create_new_folder", - args: data, - callback: function(r) { } - }); - }, __('Enter folder name'), __("Create")); - }); - - doclist.page.add_menu_item(__("Edit Folder"), function() { - frappe.set_route("Form", "File", doclist.current_folder); - }); - - doclist.page.add_menu_item(__("Import .zip"), function() { - // make upload dialog - frappe.ui.get_upload_dialog({ - args: { - folder: doclist.current_folder, - from_form: 1 - }, - callback: function(attachment, r) { - frappe.call({ - method: "frappe.core.doctype.file.file.unzip_file", - args: { - name: r.message["name"], - }, - callback: function(r) { - if(!r.exc) { - //doclist.refresh(); - } else { - frappe.msgprint(__("Error in uploading files" + r.exc)); - } - } - }); - }, - }); - }); - }, - setup_dragdrop: function(doclist) { - $(doclist.$page).on('dragenter dragover', false) - .on('drop', function (e) { - var dataTransfer = e.originalEvent.dataTransfer; - if (!(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0)) { - return; - } - e.stopPropagation(); - e.preventDefault(); - frappe.upload.multifile_upload(dataTransfer.files, { - "folder": doclist.current_folder, - "from_form": 1 - }, { - confirm_is_private: 1 - }); - }); - }, - add_menu_item_copy: function(doclist){ - if (!doclist.copy) { - var copy_menu = doclist.page.add_menu_item(__("Copy"), function() { - if(doclist.$page.find(".list-row-checkbox:checked").length){ - doclist.selected_files = doclist.get_checked_items(); - doclist.old_parent = doclist.current_folder; - doclist.list_renderer.settings.add_menu_item_paste(doclist); - } - else{ - frappe.throw(__("Please select file to copy")); - } - }); - doclist.copy = true; - } - }, - add_menu_item_paste:function(doclist){ - var paste_menu = doclist.page.add_menu_item(__("Paste"), function(){ - frappe.call({ - method:"frappe.core.doctype.file.file.move_file", - args: { - "file_list": doclist.selected_files, - "new_parent": doclist.current_folder, - "old_parent": doclist.old_parent - }, - callback:function(r){ - doclist.paste = false; - frappe.msgprint(__(r.message)); - doclist.selected_files = []; - $(paste_menu).remove(); - } - }); - }); - }, - before_run: function(doclist) { - var name_filter = doclist.filter_list.get_filter("file_name"); - if(name_filter) { - doclist.filter_area.removeClass("hide"); - doclist.breadcrumb.addClass("hide"); - } else { - doclist.filter_area.addClass("hide"); - doclist.breadcrumb.removeClass("hide"); - } - }, - refresh: function(doclist) { - var name_filter = doclist.filter_list.get_filter("file_name"); - - var folder_filter = doclist.filter_list.get_filter("folder"); - if(folder_filter) { - folder_filter.remove(true); - } - - if(name_filter) return; - - var route = frappe.get_route(); - if(route[2]) { - doclist.current_folder = route.slice(2).join("/"); - doclist.current_folder_name = route.slice(-1)[0]; - } - - if(!doclist.current_folder || doclist.current_folder=="List") { - doclist.current_folder = frappe.boot.home_folder; - doclist.current_folder_name = __("Home"); - } - - doclist.filter_list.add_filter("File", "folder", "=", doclist.current_folder, true); - doclist.dirty = true; - doclist.fresh = false; - - doclist.page.set_title(doclist.current_folder_name); - frappe.utils.set_title(doclist.current_folder_name); - }, - set_primary_action:function(doclist){ - doclist.page.clear_primary_action(); - doclist.page.set_primary_action(__("New"), function() { - frappe.ui.get_upload_dialog({ - "args": { - "folder": doclist.current_folder, - "from_form": 1 - }, - callback: function() { - doclist.refresh(); - } - }); - }, "octicon octicon-plus"); - }, - post_render_item: function(list, row, data) { - if(data.is_folder) { - $(row).find(".list-id").attr("href", "#List/File/" + data.name); - } - }, - set_file_route: function(name) { - frappe.set_route(["List", "File"].concat(decodeURIComponent(name).split("/"))); - }, - post_render: function(doclist) { - frappe.call({ - method: "frappe.core.doctype.file.file.get_breadcrumbs", - args: { - folder: doclist.current_folder - }, - callback: function(r) { - doclist.breadcrumb.empty(); - if(r.message && r.message.length) { - $.each(r.message, function(i, folder) { - $('
  • ' - + folder.file_name+'
  • ') - .appendTo(doclist.breadcrumb); - }); - } - $('
  • '+ doclist.current_folder_name+'
  • ') - .appendTo(doclist.breadcrumb); - } - }); - } -}; diff --git a/frappe/core/doctype/file/test_file.js b/frappe/core/doctype/file/test_file.js new file mode 100644 index 0000000000..efa40b4e98 --- /dev/null +++ b/frappe/core/doctype/file/test_file.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: File", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new File + () => frappe.tests.make('File', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js index bd8a58cf30..9222c86974 100644 --- a/frappe/core/doctype/report/report.js +++ b/frappe/core/doctype/report/report.js @@ -22,7 +22,7 @@ cur_frm.cscript.refresh = function(doc) { cur_frm.add_custom_button("Show Report", function() { switch(doc.report_type) { case "Report Builder": - frappe.set_route("Report", doc.ref_doctype, doc.name); + frappe.set_route('List', doc.ref_doctype, 'Report', doc.name); break; case "Query Report": frappe.set_route("query-report", doc.name); diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index 7b95ba07a1..b015e9222e 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -262,6 +262,7 @@ $.extend(frappe.desktop, { } new Sortable($("#icon-grid").get(0), { + animation: 150, onUpdate: function(event) { var new_order = []; $("#icon-grid .case-wrapper").each(function(i, e) { diff --git a/frappe/desk/doctype/event/test_event.js b/frappe/desk/doctype/event/test_event.js index 50dcd9e9aa..9e6a5ff349 100644 --- a/frappe/desk/doctype/event/test_event.js +++ b/frappe/desk/doctype/event/test_event.js @@ -32,7 +32,7 @@ QUnit.test("test: Event", function (assert) { () => frappe.set_route('List', 'Event', 'Calendar'), () => frappe.timeout(2), () => { - const bg_color = $(`.result-list:visible .fc-day-grid-event:contains("${subject}")`) + const bg_color = $(`.result:visible .fc-day-grid-event:contains("${subject}")`) .css('background-color'); assert.equal(bg_color, rgb, 'Event background color is set correctly'); }, diff --git a/frappe/desk/doctype/kanban_board/kanban_board.js b/frappe/desk/doctype/kanban_board/kanban_board.js index ca49bc308e..ff80a58fa0 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.js +++ b/frappe/desk/doctype/kanban_board/kanban_board.js @@ -32,7 +32,7 @@ frappe.ui.form.on('Kanban Board', { field_name: function(frm) { var field = frappe.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name); frm.doc.columns = []; - field.options && field.options.split('\n').forEach(function(o, i) { + field.options && field.options.split('\n').forEach(function(o) { o = o.trim(); if(!o) return; var d = frm.add_child('columns'); diff --git a/frappe/desk/page/activity/activity.css b/frappe/desk/page/activity/activity.css index 7eb70669fc..87604ff7a5 100644 --- a/frappe/desk/page/activity/activity.css +++ b/frappe/desk/page/activity/activity.css @@ -6,6 +6,7 @@ #page-activity .list-row { border: none; padding: 0px; + height: auto; cursor: pointer; } diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 35bb76ad8c..44134cbe64 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -15,41 +15,10 @@ frappe.pages['activity'].on_page_load = function(wrapper) { me.page.set_title(__("Activity")); frappe.model.with_doctype("Communication", function() { - me.page.list = new frappe.ui.BaseList({ - hide_refresh: true, - page: me.page, - method: 'frappe.desk.page.activity.activity.get_feed', - parent: $("
    ").appendTo(me.page.main), - render_view: function (values) { - var me = this; - wrapper = me.page.main.find(".result-list").get(0) - values.map(function (value) { - var row = $('
    ') - .data("data", value) - .appendTo($(wrapper)).get(0); - new frappe.activity.Feed(row, value); - }); - }, - show_filters: true, - doctype: "Communication", - get_args: function() { - if (frappe.route_options && frappe.route_options.show_likes) { - delete frappe.route_options.show_likes; - return { - show_likes: true - } - } else { - return {} - } - } + me.page.list = new frappe.views.Activity({ + doctype: 'Communication', + parent: wrapper }); - - me.page.list.run(); - - me.page.set_primary_action(__("Refresh"), function() { - me.page.list.filter_list.clear_filters(); - me.page.list.run(); - }, "octicon octicon-sync"); }); frappe.activity.render_heatmap(me.page); @@ -90,7 +59,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { frappe.route_options = { show_likes: true }; - me.page.list.run(); + me.page.list.refresh(); }, 'octicon octicon-heart'); }; @@ -194,4 +163,48 @@ frappe.activity.render_heatmap = function(page) { } } }) -} \ No newline at end of file +} + +frappe.views.Activity = class Activity extends frappe.views.BaseList { + + setup_defaults() { + super.setup_defaults(); + + this.doctype = 'Communication'; + this.method = 'frappe.desk.page.activity.activity.get_feed'; + + } + + setup_filter_area() { + // + } + + setup_sort_selector() { + + } + + get_args() { + return { + start: this.start, + page_length: this.page_length, + show_likes: (frappe.route_options || {}).show_likes || 0 + }; + } + + update_data(r) { + let data = r.message || []; + + if (this.start === 0) { + this.data = data; + } else { + this.data = this.data.concat(data); + } + } + + render() { + this.data.map(value => { + const row = $('
    ').data("data", value).appendTo(this.$result).get(0); + new frappe.activity.Feed(row, value); + }); + } +}; diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 1b7f96b000..4214a6d4f1 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -215,6 +215,8 @@ def delete_items(): il = json.loads(frappe.form_dict.get('items')) doctype = frappe.form_dict.get('doctype') + failed = [] + for i, d in enumerate(il): try: frappe.delete_doc(doctype, d) @@ -223,7 +225,9 @@ def delete_items(): dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype)), user=frappe.session.user) except Exception: - pass + failed.append(d) + + return failed @frappe.whitelist() def get_sidebar_stats(stats, doctype, filters=[]): diff --git a/frappe/model/document.py b/frappe/model/document.py index 50f2170412..aeb8f6113c 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -925,7 +925,12 @@ class Document(BaseDocument): if not self.meta.get("read_only") and not self.meta.get("issingle") and \ not self.meta.get("istable"): - frappe.publish_realtime("list_update", {"doctype": self.doctype}, after_commit=True) + data = { + "doctype": self.doctype, + "name": self.name, + "user": frappe.session.user + } + frappe.publish_realtime("list_update", data, after_commit=True) def db_set(self, fieldname, value=None, update_modified=True, notify=False, commit=False): '''Set a value in the document object, update the timestamp and update the database. diff --git a/frappe/public/build.json b/frappe/public/build.json index ae0a9cbd3c..e8da43feac 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -112,6 +112,7 @@ "public/css/font-awesome.css", "public/css/octicons/octicons.css", "public/css/desk.css", + "public/css/flex.css", "public/css/indicator.css", "public/css/avatar.css", "public/css/navbar.css", @@ -212,6 +213,7 @@ "public/js/frappe/misc/help_links.js", "public/js/frappe/misc/address_and_contact.js", "public/js/frappe/misc/preview_email.js", + "public/js/frappe/misc/file_manager.js", "public/js/frappe/ui/upload.html", "public/js/frappe/upload.js", @@ -299,10 +301,10 @@ "js/list.min.js": [ "public/js/frappe/ui/listing.html", - "public/js/frappe/ui/base_list.js", - "public/js/frappe/model/indicator.js", - "public/js/frappe/ui/filters/filters.js", + "public/js/frappe/ui/filters/filter.js", + "public/js/frappe/ui/filters/filter_list.js", + "public/js/frappe/ui/filters/field_select.js", "public/js/frappe/ui/filters/edit_filter.html", "public/js/frappe/ui/tags.js", "public/js/frappe/ui/tag_editor.js", @@ -310,7 +312,9 @@ "public/js/frappe/ui/liked_by.html", "public/html/print_template.html", + "public/js/frappe/list/base_list.js", "public/js/frappe/list/list_view.js", + "public/js/frappe/list/list_factory.js", "public/js/frappe/list/list_sidebar.js", "public/js/frappe/list/list_sidebar.html", @@ -322,12 +326,12 @@ "public/js/frappe/list/list_item_subject.html", "public/js/frappe/list/list_permission_footer.html", - "public/js/frappe/list/list_renderer.js", "public/js/frappe/views/gantt/gantt_view.js", "public/js/frappe/views/calendar/calendar.js", "public/js/frappe/views/image/image_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", + "public/js/frappe/views/file/file_view.js", "public/js/frappe/list/header_select_all_like_filter.html", "public/js/frappe/list/item_assigned_to_comment_count.html", @@ -336,10 +340,6 @@ "public/js/frappe/views/image/image_view_item_row.html", "public/js/frappe/views/image/photoswipe_dom.html", - "public/js/frappe/views/inbox/inbox_no_result.html", - "public/js/frappe/views/inbox/inbox_view_item_row.html", - "public/js/frappe/views/inbox/inbox_view_item_main_head.html", - "public/js/frappe/views/kanban/kanban_board.html", "public/js/frappe/views/kanban/kanban_column.html", "public/js/frappe/views/kanban/kanban_card.html" @@ -347,13 +347,17 @@ "css/report.min.css": [ "public/css/report.css", "public/css/tree_grid.css", + "public/css/frappe-datatable.css", "public/js/lib/slickgrid/slick.grid.css", "public/js/lib/slickgrid/slick-default-theme.css", "public/css/slickgrid.css" ], "js/report.min.js": [ + "public/js/lib/clusterize.min.js", + "public/js/lib/frappe-datatable.js", "public/js/frappe/views/reports/reportview.js", + "public/js/frappe/views/reports/report_view.js", "public/js/frappe/views/reports/reportview_footer.html", "public/js/frappe/views/reports/query_report.js", "public/js/frappe/views/reports/grid_report.js", diff --git a/frappe/public/css/common.css b/frappe/public/css/common.css index 3aa13bef79..b08be904a6 100644 --- a/frappe/public/css/common.css +++ b/frappe/public/css/common.css @@ -216,6 +216,18 @@ a.no-decoration:active { .margin { margin: 15px; } +.margin-top { + margin-top: 15px; +} +.margin-bottom { + margin-bottom: 15px; +} +.margin-left { + margin-left: 15px; +} +.margin-right { + margin-right: 15px; +} @media (max-width: 767px) { .text-center-xs { text-align: center; diff --git a/frappe/public/css/controls.css b/frappe/public/css/controls.css index ab97425429..fe98963827 100644 --- a/frappe/public/css/controls.css +++ b/frappe/public/css/controls.css @@ -3,6 +3,12 @@ margin-top: 5px; margin-bottom: 5px; } +.unit-checkbox label { + position: relative; +} +.unit-checkbox input[type=checkbox] { + margin-left: 0; +} .unit-checkbox + .checkbox { margin-top: 5px; margin-bottom: 5px; diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 0bd6a0b53b..9d957f2222 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -216,6 +216,18 @@ a.no-decoration:active { .margin { margin: 15px; } +.margin-top { + margin-top: 15px; +} +.margin-bottom { + margin-bottom: 15px; +} +.margin-left { + margin-left: 15px; +} +.margin-right { + margin-right: 15px; +} @media (max-width: 767px) { .text-center-xs { text-align: center; @@ -486,6 +498,9 @@ fieldset[disabled] .form-control { .form-control input { padding: 6px 10px 8px; } +.input-area { + position: relative; +} .link-field.ui-front { z-index: inherit; } @@ -587,10 +602,6 @@ li.user-progress .progress-bar { .intro-area { padding: 15px 30px; } -.footnote-area { - padding: 0px 15px; - border-top: 1px solid #d1d8dd; -} .file-upload .input-group-addon { color: #8D99A6; font-size: 12px; @@ -972,7 +983,7 @@ li.user-progress .progress-bar { } input[type="checkbox"] { position: relative; - height: 16px; + left: -999999px; } input[type="checkbox"]:before { position: absolute; @@ -990,9 +1001,7 @@ input[type="checkbox"]:before { -webkit-transition: 150ms color; -o-transition: 150ms color; transition: 150ms color; - background-color: white; - padding: 1px; - margin: -1px; + left: 999999px; } input[type="checkbox"]:focus:before { color: #8D99A6; diff --git a/frappe/public/css/flex.css b/frappe/public/css/flex.css new file mode 100644 index 0000000000..e680c5592a --- /dev/null +++ b/frappe/public/css/flex.css @@ -0,0 +1,41 @@ +.flex { + display: flex; +} +.justify-center { + justify-content: center; +} +.align-center { + align-items: center; +} +.level { + display: flex; + justify-content: space-between; + align-items: center; +} +.level-left, +.level-right { + display: flex; + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; + align-items: center; +} +.level-left.is-flexible, +.level-right.is-flexible { + flex-grow: initial; + flex-shrink: initial; +} +.level-left { + justify-content: flex-start; +} +.level-right { + justify-content: flex-end; +} +.level-item { + align-items: center; + display: flex; + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; + justify-content: center; +} diff --git a/frappe/public/css/frappe-datatable.css b/frappe/public/css/frappe-datatable.css new file mode 100644 index 0000000000..17d151f935 --- /dev/null +++ b/frappe/public/css/frappe-datatable.css @@ -0,0 +1,58 @@ +.data-table { + margin-left: -1px; + margin-top: -1px; + font-size: 12px; +} +.data-table .data-table-col .edit-cell { + padding: 0; +} +.data-table .data-table-col .edit-cell input { + font-size: inherit; + height: 34px; +} +.data-table .frappe-control { + margin: 0; +} +.data-table .form-group { + margin: 0; +} +.data-table .form-control { + border-radius: 0px; + border: none; +} +.data-table .link-btn { + top: 6px; +} +.data-table select { + height: 34px; +} +.data-table .checkbox { + margin: 7px 0 7px 8px; +} +.data-table [data-fieldtype="Color"] .control-input { + overflow: hidden; +} +.data-table .body-scrollable::-webkit-scrollbar { + display: none; +} +.data-table .data-table-header { + background-color: #F7FAFC; + color: #8D99A6; +} +.data-table .data-table-row.row-update { + animation: 500ms breathe forwards; +} +.data-table .data-table-row.row-highlight { + background-color: #fffdf4; +} +@keyframes breathe { + 0% { + background-color: transparent; + } + 50% { + background-color: #fffdf4; + } + 100% { + background-color: transparent; + } +} diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index b3e8862e2a..6fffea1eff 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -1,43 +1,63 @@ -.no-result { - padding: 150px 15px; - color: #8D99A6; +.result, +.no-result, +.freeze { + min-height: calc(100vh - 284px); +} +.freeze-row .level-left, +.freeze-row .level-right, +.freeze-row .list-row-col { + height: 100%; + width: 100%; } -.result-list { - min-height: 400px; +.freeze-row .list-row-col { + background-color: #d1d8dd; + border-radius: 2px; + animation: 2s breathe infinite; +} +@keyframes breathe { + 0% { + opacity: 0.2; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 0.2; + } } .sort-selector .dropdown:hover { text-decoration: underline; } -.list-filters { +.filter-list { position: relative; } -.list-filters .sort-selector { +.filter-list .sort-selector { position: absolute; top: 15px; right: 15px; } -.show_filters { +.tag-filters-area { padding: 15px 15px 0px; border-bottom: 1px solid #d1d8dd; } -.set-filters { +.active-tag-filters { padding-bottom: 4px; padding-right: 120px; } @media (max-width: 767px) { - .set-filters { + .active-tag-filters { padding-right: 80px; } } -.set-filters .btn { +.active-tag-filters .btn { margin-bottom: 10px; } -.set-filters .btn-group { - margin-right: 10px; +.active-tag-filters .btn-group { + margin-left: 10px; white-space: nowrap; font-size: 0; } -.set-filters .btn-group .btn-default { +.active-tag-filters .btn-group .btn-default { background-color: transparent; border: 1px solid #d1d8dd; color: #8D99A6; @@ -51,138 +71,107 @@ margin-top: 6px; margin-left: 15px; } -.filter-box .filter_field { +.filter-box .filter-field { padding-right: 15px; width: calc(64%); } -.filter-box .filter_field .frappe-control { +.filter-box .filter-field .frappe-control { position: relative; } -@media (min-width: 768px) { +@media (min-width: 767px) { .filter-box .row > div[class*="col-sm-"] { padding-right: 0px; } - .filter_field { + .filter-field { width: 65% !important; } - .filter_field .frappe-control { + .filter-field .frappe-control { position: relative; } } -.list-row { - padding: 9px 15px; +.list-row-container { border-bottom: 1px solid #d1d8dd; + display: flex; + flex-direction: column; +} +.list-row { + padding: 12px 15px; + height: 40px; cursor: pointer; transition: color 0.2s; -webkit-transition: color 0.2s; } -.list-row .h6 { - margin-top: 0px; - margin-bottom: 0px; -} -.list-row-head { +.list-row:hover { background-color: #F7FAFC; - border-bottom: 1px solid #d1d8dd !important; -} -.list-row:hover, -.grid-row:hover { - background-color: #F7FAFC; -} -.no-hover:hover { - background-color: transparent !important; } .list-row:last-child { border-bottom: 0px; } +.list-row .level-left { + flex: 3; +} +.list-row .level-right { + flex: 1; +} .list-row-head { background-color: #F7FAFC; border-bottom: 1px solid #d1d8dd !important; } -.list-row .h6 { - margin-top: 0px; - margin-bottom: 0px; +.list-row-head .list-subject { + font-weight: normal; } -.list-item-col { - white-space: nowrap; - text-overflow: ellipsis; - height: 30px; - padding-top: 3px; +.list-row-head .checkbox-actions { + display: none; } -.list-paging-area { - padding: 10px 15px; - border-top: 1px solid #d1d8dd; +.list-row-col { + flex: 1; + margin-right: 15px; } -.list-value { - display: table; - vertical-align: middle; +.list-subject { + flex: 2; + font-weight: bold; + justify-content: start; +} +.list-subject .level-item { + margin-right: 8px; +} +.list-subject.seen { + font-weight: normal; } -.list-value .list-row-checkbox, -.list-value .liked-by, -.list-value .list-id, -.list-value .list-select-all { - display: table-cell; - vertical-align: middle; +.list-row-activity { + justify-content: flex-end; + min-width: 120px; } -.list-value .list-row-checkbox, -.list-value .list-select-all { +.list-row-activity .avatar:not(.avatar-empty) { margin: 0; - margin-right: 7px; } -.list-value .liked-by { - padding-top: 2px; +.list-row-activity > span { + display: inline-block; + margin-left: 10px; + margin-right: 0; } -.list-value .list-col-title { - vertical-align: middle; +.list-paging-area, +.footnote-area { + padding: 10px 15px; + border-top: 1px solid #d1d8dd; + overflow: auto; } .progress { height: 10px; } -.doclist-row { - font-size: 12px; -} .likes-count { - display: inline-block; - width: 15px; - margin-left: -5px; - color: #8D99A6; - font-size: 12px; + display: none; } -.doclist-row .docstatus .octicon { - font-size: 12px; +.list-liked-by-me { + margin-bottom: 1px; } -.doclist-row .progress { - margin-top: 12px; +input.list-check-all, +input.list-row-checkbox { + margin-top: 0px; } .filterable { cursor: pointer; } -.doclist-row .label { - margin-right: 8px; -} -.list-info-row { - float: left; - margin-top: 1px; -} -.list-row-right .modified { - margin-top: 3px; -} -.list-row-right .list-row-modified { - margin-right: 9px; - margin-top: 3px; -} -.list-row-right { - margin-top: -2px; - margin-bottom: -4px; -} -.list-row-right .indicator { - margin-left: 10px; - margin-right: -5px; -} -.side-panel { - border-bottom: 1px solid #d1d8dd; - margin: 0px -15px; - padding: 5px 15px; -} .listview-main-section .octicon-heart { cursor: pointer; } @@ -208,38 +197,17 @@ .like-action.octicon-heart { color: #ff5858; } -.list-id { - font-weight: bold; -} -.list-id.seen { - font-weight: normal; -} -.list-col { - height: 20px; -} -.list-value { - vertical-align: middle; -} -@media (max-width: 767px) { - .doclist-row { - font-size: 14px; - } - .doclist-row [type='checkbox'] { - display: none; - } - .doclist-row .list-row-right .list-row-modified { - display: none; - } -} .list-comment-count { display: inline-block; width: 37px; text-align: left; } +.result.tags-shown .tag-row { + display: block; +} .tag-row { - padding-left: 55px; - margin-bottom: 0px; - margin-top: -5px; + display: none; + margin-left: 50px; } .taggle_placeholder { top: 0; @@ -300,6 +268,7 @@ padding: 15px; border-bottom: 1px solid #EBEFF2; border-right: 1px solid #EBEFF2; + max-width: 25%; } .image-view-container .image-view-item:nth-child(4n) { border-right: none; @@ -329,6 +298,9 @@ .image-view-container .image-field img { max-height: 100%; } +.image-view-container .image-field.no-image { + background-color: #fafbfc; +} .image-view-container .placeholder-text { font-size: 72px; color: #d1d8dd; @@ -351,6 +323,7 @@ @media (max-width: 991px) { .image-view-container .image-view-item { flex: 0 0 33.33333333%; + max-width: 33.33333333%; } .image-view-container .image-view-item:nth-child(3n) { border-right: none; @@ -381,6 +354,7 @@ } .image-view-container.three-column .image-view-item { flex: 0 0 33.33333333%; + max-width: 33.33333333%; } .image-view-container.three-column .image-view-item:nth-child(3n) { border-right: none; @@ -424,6 +398,10 @@ .pswp__more-item img { max-height: 100%; } +.list-paging-area .gantt-view-mode { + margin-left: 15px; + margin-right: 15px; +} .gantt .details-container .heading { margin-bottom: 10px; font-size: 12px; diff --git a/frappe/public/css/regrid.css b/frappe/public/css/regrid.css new file mode 100644 index 0000000000..92dfbb5ffb --- /dev/null +++ b/frappe/public/css/regrid.css @@ -0,0 +1,33 @@ +.data-table { + font-size: 14px; +} +.data-table .frappe-control { + margin: 0; +} +.data-table .form-group { + margin: 0; +} +.data-table .form-control { + border-radius: 0px; + border: none; +} +.data-table .link-btn { + top: 9px; +} +.data-table select { + height: 36px; +} +.data-table .edit-cell { + border: 2px solid #7679FC; +} +.data-table .checkbox { + margin-top: 8px; + margin-bottom: 8px; + margin-left: 8px; +} +.data-table [data-fieldtype="Color"] .control-input { + overflow: hidden; +} +.data-table .data-table-col.selected .content { + border-color: #7679FC; +} diff --git a/frappe/public/css/report.css b/frappe/public/css/report.css index b95e4fb78f..e1ec264675 100644 --- a/frappe/public/css/report.css +++ b/frappe/public/css/report.css @@ -51,3 +51,9 @@ .column-picker-dialog .add-btn { margin-bottom: 2px; } +.data-table .edit-popup .frappe-control { + padding: 0; +} +.data-table .edit-popup .frappe-control .form-group { + margin: 0; +} diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 5e1d9b2bc8..81a5087b2a 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -216,6 +216,18 @@ a.no-decoration:active { .margin { margin: 15px; } +.margin-top { + margin-top: 15px; +} +.margin-bottom { + margin-bottom: 15px; +} +.margin-left { + margin-left: 15px; +} +.margin-right { + margin-right: 15px; +} @media (max-width: 767px) { .text-center-xs { text-align: center; diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 30303f61b2..037434c2ed 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -56,5 +56,14 @@ frappe.db = { callback && callback(r.message); } }); + }, + get_doc: function(doctype, name, filters = null) { + return new Promise(resolve => { + frappe.call({ + method: "frappe.client.get", + args: { doctype, name, filters }, + callback: r => resolve(r.message) + }); + }); } -} +}; diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 9a4c5ca5b5..98e37ee34c 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -8,9 +8,12 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ set_options() { if (this.df.options) { let options = this.df.options || []; - if(typeof options === 'string') { + if (typeof options === 'string') { options = options.split('\n'); } + if (typeof options[0] === 'string') { + options = options.map(o => ({label: o, value: o})); + } this._data = options; } }, @@ -20,13 +23,14 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ minChars: 0, maxItems: 99, autoFirst: true, - list: this.get_data() + list: this.get_data(), + sort: () => { + return 0; + } }; }, setup_awesomplete() { - var me = this; - this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); $(this.input_area).find('.awesomplete ul').css('min-width', '100%'); diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 6e8880a72a..331180708c 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -79,7 +79,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ if(is_valid) { return value; } - frappe.msgprint(__("{0} is not a valid hex color", [value])); return null; } }); diff --git a/frappe/public/js/frappe/form/footer/assign_to.js b/frappe/public/js/frappe/form/footer/assign_to.js index 80aa38369f..52d134b6ff 100644 --- a/frappe/public/js/frappe/form/footer/assign_to.js +++ b/frappe/public/js/frappe/form/footer/assign_to.js @@ -179,8 +179,8 @@ frappe.ui.form.AssignToDialog = Class.extend({ }); frappe.ui.add_assignment = function(opts, dialog) { - var assign_to = opts.obj.dialog.fields_dict.assign_to.get_value(); - var args = opts.obj.dialog.get_values(); + var assign_to = dialog.fields_dict.assign_to.get_value(); + var args = dialog.get_values(); if(args && assign_to) { return frappe.call({ method: opts.method, diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 2295cafecc..546442c5c1 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -228,6 +228,14 @@ frappe.form.formatters = { }, Email: function(value) { return $("
    ").text(value).html(); + }, + FileSize: function(value) { + if(value > 1048576) { + value = flt(flt(value) / 1048576, 1) + "M"; + } else if (value > 1024) { + value = flt(flt(value) / 1024, 1) + "K"; + } + return value; } } diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js index 4ca2ca568d..7188e4131c 100644 --- a/frappe/public/js/frappe/form/linked_with.js +++ b/frappe/public/js/frappe/form/linked_with.js @@ -22,7 +22,6 @@ frappe.ui.form.LinkedWith = class LinkedWith { } make_dialog() { - var me = this; this.dialog = new frappe.ui.Dialog({ hide_on_page_refresh: true, @@ -38,38 +37,29 @@ frappe.ui.form.LinkedWith = class LinkedWith { .then(() => this.load_doctypes()) .then(() => this.links_not_permitted_or_missing()) .then(() => this.get_linked_docs()) - .then(() => this.make_html()) - } + .then(() => this.make_html()); + }; } make_html() { const linked_docs = this.frm.__linked_docs; - let html; + let html = ''; + + const linked_doctypes = Object.keys(linked_docs); - if(Object.keys(linked_docs).length === 0) { + if (linked_doctypes.length === 0) { html = __("Not Linked to any record"); } else { - html = Object.keys(linked_docs).map(dt => { - const list_renderer = new frappe.views.ListRenderer({ - doctype: dt, - list_view: this - }); - return `
    - ${this.make_doc_head(dt)} - ${linked_docs[dt] - .map(value => { - // prepare data - value = list_renderer.prepare_data(value); - value._checkbox = 0; - value._hide_activity = 1; - - const $item = $(list_renderer.get_item_html(value)); - const $item_container = $('
    ').append($item); - return $item_container[0].outerHTML; - }).join("")} -
    `; - }); + html = linked_doctypes.map(doctype => { + const docs = linked_docs[doctype]; + return ` +
    + ${this.make_doc_head(doctype)} + ${docs.map(doc => this.make_doc_row(doc, doctype)).join('')} +
    + `; + }).join(''); } $(this.dialog.body).html(html); @@ -82,7 +72,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { if (this.frm.__linked_doctypes) { doctypes_to_load = Object.keys(this.frm.__linked_doctypes) - .filter(doctype => !already_loaded.includes(doctype)); + .filter(doctype => !already_loaded.includes(doctype)); } // load all doctypes asynchronously using with_doctype @@ -100,19 +90,17 @@ frappe.ui.form.LinkedWith = class LinkedWith { } links_not_permitted_or_missing() { - var me = this; let links = null; if (this.frm.__linked_doctypes) { links = Object.keys(this.frm.__linked_doctypes) - .filter(frappe.model.can_get_report); + .filter(frappe.model.can_get_report); } let flag; if(!links) { - $(this.dialog.body).html( - `${this.frm.__linked_doctypes + $(this.dialog.body).html(`${this.frm.__linked_doctypes ? __("Not enough permission to see links") : __("Not Linked to any record")}`); flag = true; @@ -126,7 +114,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { } get_linked_doctypes() { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { if (this.frm.__linked_doctypes) { resolve(); } @@ -160,19 +148,20 @@ frappe.ui.form.LinkedWith = class LinkedWith { } make_doc_head(heading) { - return `
    -
    - ${heading} -
    `; + return ` +
    +
    ${__(heading)}
    +
    + `; } make_doc_row(doc, doctype) { - return `
    -
    -
    + return ``; } -} +}; diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js new file mode 100644 index 0000000000..f17d183ffa --- /dev/null +++ b/frappe/public/js/frappe/list/base_list.js @@ -0,0 +1,638 @@ +frappe.provide('frappe.views'); + +frappe.views.BaseList = class BaseList { + constructor(opts) { + Object.assign(this, opts); + this.show(); + } + + show() { + this.init().then(() => this.refresh()); + } + + init() { + + if (this.init_promise) return this.init_promise; + + let tasks = [ + this.setup_defaults, + this.set_stats, + this.setup_fields, + // make view + this.setup_page, + this.setup_page_head, + this.setup_side_bar, + this.setup_list_wrapper, + this.setup_filter_area, + this.setup_sort_selector, + this.setup_result_area, + this.setup_no_result_area, + this.setup_freeze_area, + this.setup_paging_area, + this.setup_footnote_area, + this.setup_view, + ].map(fn => fn.bind(this)); + + this.init_promise = frappe.run_serially(tasks); + return this.init_promise; + } + + setup_defaults() { + this.page_name = frappe.get_route_str(); + this.page_title = this.page_title || __(this.doctype); + this.meta = frappe.get_meta(this.doctype); + this.settings = frappe.listview_settings[this.doctype] || {}; + this.user_settings = frappe.get_user_settings(this.doctype); + + this.start = 0; + this.page_length = 20; + this.data = []; + this.method = 'frappe.desk.reportview.get'; + + this.can_create = frappe.model.can_create(this.doctype); + this.can_delete = frappe.model.can_delete(this.doctype); + this.can_write = frappe.model.can_write(this.doctype); + + this.filters = []; + this.order_by = 'modified desc'; + + // Setup buttons + this.primary_action = null; + this.secondary_action = { + label: __('Refresh'), + action: () => this.refresh() + }; + + this.menu_items = [{ + label: __('Refresh'), + action: () => this.refresh(), + class: 'visible-xs' + }]; + } + + setup_fields() { + this.set_fields(); + this.build_fields(); + } + + set_fields() { + + this._fields = []; + const add_field = f => this._add_field(f); + + // default fields + const std_fields = [ + 'name', + 'owner', + 'docstatus', + '_user_tags', + '_comments', + 'modified', + 'modified_by', + '_assign', + '_liked_by', + '_seen', + 'enabled', + 'disabled', + this.meta.title_field, + this.meta.image_field + ]; + + std_fields.map(add_field); + + // fields in_list_view + const fields = this.get_fields_in_list_view(); + fields.map(add_field); + + // currency fields + fields.filter( + df => df.fieldtype === 'Currency' && df.options + ).map(df => { + if (df.options.includes(':')) { + add_field(df.options.split(':')[1]); + } else { + add_field(df.options); + } + }); + + // image fields + fields.filter( + df => df.fieldtype === 'Image' + ).map(df => { + if (df.options) { + add_field(df.options); + } else { + add_field(df.fieldname); + } + }); + + // fields in listview_settings + (this.settings.add_fields || []).map(add_field); + } + + get_fields_in_list_view() { + return this.meta.fields.filter(df => { + return df.in_list_view + && frappe.perm.has_perm(this.doctype, df.permlevel, 'read') + && frappe.model.is_value_type(df.fieldtype); + }); + } + + build_fields() { + // fill in missing doctype + this._fields = this._fields.map(f => { + if (typeof f === 'string') { + f = [f, this.doctype]; + } + return f; + }); + //de-dup + this._fields = this._fields.uniqBy(f => f[0] + f[1]); + // build this.fields + this.fields = this._fields.map(f => frappe.model.get_full_column_name(f[0], f[1])); + } + + _add_field(fieldname) { + if (!fieldname) return; + let doctype = this.doctype; + + if (typeof fieldname === 'object') { + // df is passed + const df = fieldname; + fieldname = df.fieldname; + doctype = df.parent; + } + + const is_valid_field = frappe.model.std_fields_list.includes(fieldname) + || frappe.meta.has_field(doctype, fieldname); + + if (!is_valid_field) { + return; + } + + this._fields.push([fieldname, doctype]); + } + + set_stats() { + this.stats = ['_user_tags']; + // add workflow field (as priority) + this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype); + if (this.workflow_state_fieldname) { + if (!frappe.workflow.workflows[this.doctype]['override_status']) { + this._add_field(this.workflow_state_fieldname); + } + this.stats.push(this.workflow_state_fieldname); + } + } + + setup_page() { + this.parent.list_view = this; + this.page = this.parent.page; + this.$page = $(this.parent); + this.page.page_form.removeClass('row').addClass('flex'); + } + + setup_page_head() { + this.page.set_title(this.page_title); + this.set_menu_items(); + this.set_breadcrumbs(); + } + + set_menu_items() { + const $secondary_action = this.page.set_secondary_action( + this.secondary_action.label, + this.secondary_action.action, + this.secondary_action.icon + ); + if (!this.secondary_action.icon) { + $secondary_action.addClass('hidden-xs'); + } else { + $secondary_action.addClass('visible-xs'); + } + + this.menu_items.map(item => { + const $item = this.page.add_menu_item(item.label, item.action, item.standard); + if (item.class) { + $item.addClass(item.class); + } + }); + } + + set_breadcrumbs() { + frappe.breadcrumbs.add(this.meta.module, this.doctype); + } + + setup_side_bar() { + this.list_sidebar = new frappe.views.ListSidebar({ + doctype: this.doctype, + stats: this.stats, + parent: this.$page.find('.layout-side-section'), + // set_filter: this.set_filter.bind(this), + page: this.page, + list_view: this + }); + } + + setup_main_section() { + this.setup_list_wrapper(); + this.setup_filter_area(); + this.setup_sort_selector(); + this.setup_result_area(); + this.setup_no_result_area(); + this.setup_freeze_area(); + this.setup_paging_area(); + this.setup_footnote_area(); + } + + setup_list_wrapper() { + this.$frappe_list = $('
    ').appendTo(this.page.main); + } + + setup_filter_area() { + this.filter_area = new FilterArea(this); + + if (this.filters.length > 0) { + return this.filter_area.set(this.filters); + } + } + + setup_sort_selector() { + this.sort_selector = new frappe.ui.SortSelector({ + parent: this.filter_area.$filter_list_wrapper, + doctype: this.doctype, + args: this.order_by, + onchange: () => this.refresh(true) + }); + } + + setup_result_area() { + this.$result = $(`
    `).hide(); + this.$frappe_list.append(this.$result); + } + + setup_no_result_area() { + this.$no_result = $(` +
    + ${this.get_no_result_message()} +
    + `).hide(); + this.$frappe_list.append(this.$no_result); + } + + setup_freeze_area() { + this.$freeze = $('
    ').hide(); + this.$frappe_list.append(this.$freeze); + } + + get_no_result_message() { + return __('Nothing to show'); + } + + setup_paging_area() { + const paging_values = [20, 100, 500]; + this.$paging_area = $( + `
    +
    +
    + ${paging_values.map(value => ` + + `).join('')} +
    +
    +
    + +
    +
    ` + ).hide(); + this.$frappe_list.append(this.$paging_area); + + // set default paging btn active + this.$paging_area + .find(`.btn-paging[data-value="${this.page_length}"]`) + .addClass('btn-info'); + + this.$paging_area.on('click', '.btn-paging, .btn-more', e => { + const $this = $(e.currentTarget); + + if ($this.is('.btn-paging')) { + // set active button + this.$paging_area.find('.btn-paging').removeClass('btn-info'); + $this.addClass('btn-info'); + + this.start = 0; + this.page_length = $this.data().value; + this.refresh(); + } else if ($this.is('.btn-more')) { + this.start = this.start + this.page_length; + this.refresh(); + } + }); + } + + setup_footnote_area() { + this.$footnote_area = null; + } + + get_fields() { + return this.fields; + } + + setup_view() { + // for child classes + } + + get_args() { + // filters might have a fifth param called hidden, + // we don't want to pass that server side + const filters = this.filter_area.get().map(filter => filter.slice(0, 4)); + return { + doctype: this.doctype, + fields: this.get_fields(), + filters: filters, + order_by: this.sort_selector.get_sql_string(), + start: this.start, + page_length: this.page_length + }; + } + + refresh() { + this.freeze(true); + // fetch data from server + const args = this.get_args(); + return frappe.call({ + method: this.method, + type: 'GET', + args: args + }).then(r => { + // render + this.freeze(false); + + this.update_data(r); + + this.toggle_result_area(); + this.before_render(); + this.render(); + }); + } + + update_data(r) { + let data = r.message || {}; + data = frappe.utils.dict(data.keys, data.values); + data = data.uniqBy(d => d.name); + + if (this.start === 0) { + this.data = data; + } else { + this.data = this.data.concat(data); + } + } + + freeze() { + // show a freeze message while data is loading + } + + before_render() { + + } + + render() { + // for child classes + } + + toggle_result_area() { + this.$result.toggle(this.data.length > 0); + this.$paging_area.toggle(this.data.length > 0); + this.$no_result.toggle(this.data.length == 0); + + const show_more = (this.start + this.page_length) <= this.data.length; + this.$paging_area.find('.btn-more') + .toggle(show_more); + } + + call_for_selected_items(method, args = {}) { + args.names = this.get_checked_items(true); + + frappe.call({ + method: method, + args: args, + freeze: true, + callback: r => { + if (!r.exc) { + this.refresh(); + } + } + }); + } +}; + +class FilterArea { + constructor(list_view) { + this.list_view = list_view; + this.standard_filters_wrapper = this.list_view.page.page_form; + this.$filter_list_wrapper = $('
    ').appendTo(this.list_view.$frappe_list); + this.trigger_refresh = true; + this.setup(); + } + + setup() { + this.make_standard_filters(); + this.make_filter_list(); + } + + get() { + let filters = this.filter_list.get_filters(); + let standard_filters = this.get_standard_filters(); + + return filters + .concat(standard_filters) + .uniqBy(JSON.stringify); + } + + set(filters) { + // use to method to set filters without triggering refresh + this.trigger_refresh = false; + return this.add(filters, false) + .then(() => { + this.trigger_refresh = true; + }); + } + + add(filters, refresh = true) { + if (!filters || Array.isArray(filters) && filters.length === 0) + return Promise.resolve(); + + if (typeof filters[0] === 'string') { + // passed in the format of doctype, field, condition, value + const filter = Array.from(arguments); + filters = [filter]; + } + + const { non_standard_filters, promise } = this.set_standard_filter(filters); + if (non_standard_filters.length === 0) { + return promise; + } + + return promise + .then(() => this.filter_list.add_filters(non_standard_filters)) + .then(() => { + if (refresh) return this.list_view.refresh(); + }); + } + + refresh_list_view() { + if (this.trigger_refresh) { + this.list_view.refresh(); + } + } + + set_standard_filter(filters) { + const fields_dict = this.list_view.page.fields_dict; + + let out = filters.reduce((out, filter) => { + // eslint-disable-next-line + const [dt, fieldname, condition, value] = filter; + out.promise = out.promise || Promise.resolve(); + out.non_standard_filters = out.non_standard_filters || []; + + if (fields_dict[fieldname] && condition === '=') { + // standard filter + out.promise = out.promise.then( + () => fields_dict[fieldname].set_value(value) + ); + } else { + // filter out non standard filters + out.non_standard_filters.push(filter); + } + return out; + }, {}); + + return out; + } + + remove(fieldname) { + const fields_dict = this.list_view.page.fields_dict; + + if (fieldname in fields_dict) { + fields_dict[fieldname].set_value(''); + return; + } + this.filter_list.get_filter(fieldname).remove(); + } + + clear() { + this.filter_list.clear_filters(); + + const fields_dict = this.list_view.page.fields_dict; + for (let key in fields_dict) { + const field = this.list_view.page.fields_dict[key]; + field.set_value(''); + } + } + + make_standard_filters() { + $( + `
    + +
    ` + ) + .css({ + height: '30px', + width: '20px', + marginRight: '-2px', + marginLeft: '10px' + }) + .prependTo(this.standard_filters_wrapper); + + let fields = [ + { + fieldtype: 'Data', + label: 'ID', + condition: 'like', + fieldname: 'name', + onchange: () => this.refresh_list_view() + } + ]; + + const doctype_fields = this.list_view.meta.fields; + + fields = fields.concat(doctype_fields.filter( + df => df.in_standard_filter && + frappe.model.is_value_type(df.fieldtype) + ).map(df => { + let options = df.options; + let condition = '='; + let fieldtype = df.fieldtype; + if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) { + fieldtype = 'Data'; + condition = 'like'; + } + if (df.fieldtype == "Select" && df.options) { + options = df.options.split("\n"); + if (options.length > 0 && options[0] != "") { + options.unshift(""); + options = options.join("\n"); + } + } + return { + fieldtype: fieldtype, + label: __(df.label), + options: options, + fieldname: df.fieldname, + condition: condition, + onchange: () => this.refresh_list_view() + }; + })); + + if (fields.length > 3) { + fields = fields.map((df, i) => { + if (i >= 3) { + df.input_class = 'hidden-sm hidden-xs'; + } + return df; + }); + } + + fields.map(df => this.list_view.page.add_field(df)); + } + + get_standard_filters() { + const filters = []; + const fields_dict = this.list_view.page.fields_dict; + for (let key in fields_dict) { + let field = fields_dict[key]; + let value = field.get_value(); + if (value) { + if (field.df.condition === 'like' && !value.includes('%')) { + value = '%' + value + '%'; + } + filters.push([ + this.list_view.doctype, + field.df.fieldname, + field.df.condition || '=', + value + ]); + } + } + + return filters; + } + + make_filter_list() { + this.filter_list = new frappe.ui.FilterGroup({ + base_list: this.list_view, + parent: this.$filter_list_wrapper, + doctype: this.list_view.doctype, + default_filters: [], + on_change: () => this.refresh_list_view() + }); + } +} + +// utility function to validate view modes +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report']; +frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_factory.js b/frappe/public/js/frappe/list/list_factory.js new file mode 100644 index 0000000000..921be5c568 --- /dev/null +++ b/frappe/public/js/frappe/list/list_factory.js @@ -0,0 +1,95 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.provide('frappe.views.list_view'); + +cur_list = null; +frappe.views.ListFactory = frappe.views.Factory.extend({ + make: function (route) { + var me = this; + var doctype = route[1]; + + frappe.model.with_doctype(doctype, function () { + if (locals['DocType'][doctype].issingle) { + frappe.set_re_route('Form', doctype); + } else { + // List / Gantt / Kanban / etc + // File is a special view + const view_name = doctype !== 'File' ? route[2] : 'File'; + let view_class = frappe.views[view_name + 'View']; + if (!view_class) view_class = frappe.views.ListView; + + if (view_class && view_class.load_last_view && view_class.load_last_view()) { + // view can have custom routing logic + return; + } + + frappe.provide('frappe.views.list_view.' + doctype); + const page_name = frappe.get_route_str(); + + if (!frappe.views.list_view[page_name]) { + frappe.views.list_view[page_name] = new view_class({ + doctype: doctype, + parent: me.make_page(true, page_name) + }); + } else { + frappe.container.change_to(page_name); + } + me.set_cur_list(); + } + }); + }, + show: function () { + if(this.re_route_to_view()) { + return; + } + this.set_module_breadcrumb(); + this._super(); + this.set_cur_list(); + cur_list && cur_list.show(); + }, + re_route_to_view: function() { + var route = frappe.get_route(); + var doctype = route[1]; + var last_route = frappe.route_history.slice(-2)[0]; + if (route[0] === 'List' && route.length === 2 && frappe.views.list_view[doctype]) { + if(last_route && last_route[0]==='List' && last_route[1]===doctype) { + // last route same as this route, so going back. + // this happens because #List/Item will redirect to #List/Item/List + // while coming from back button, the last 2 routes will be same, so + // we know user is coming in the reverse direction (via back button) + + // example: + // Step 1: #List/Item redirects to #List/Item/List + // Step 2: User hits "back" comes back to #List/Item + // Step 3: Now we cannot send the user back to #List/Item/List so go back one more step + window.history.go(-1); + return true; + } else { + return false; + } + } + }, + set_module_breadcrumb: function () { + if (frappe.route_history.length > 1) { + var prev_route = frappe.route_history[frappe.route_history.length - 2]; + if (prev_route[0] === 'modules') { + var doctype = frappe.get_route()[1], + module = prev_route[1]; + if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) { + // save the last page from the breadcrumb was accessed + frappe.breadcrumbs.set_doctype_module(doctype, module); + } + } + } + }, + set_cur_list: function () { + var route = frappe.get_route(); + var page_name = frappe.get_route_str(); + cur_list = frappe.views.list_view[page_name]; + if (cur_list && cur_list.doctype !== route[1]) { + // changing... + cur_list = null; + } + } +}); diff --git a/frappe/public/js/frappe/list/list_permission_footer.html b/frappe/public/js/frappe/list/list_permission_footer.html index b61d09031f..3b0b42e9d7 100644 --- a/frappe/public/js/frappe/list/list_permission_footer.html +++ b/frappe/public/js/frappe/list/list_permission_footer.html @@ -1,8 +1,8 @@ -
    - +
    + {% for(var i=0; i < condition_list.length; i++) { var conditions = condition_list[i]; %} -
    +
    {% if (i > 0) { %}{{ __("Or") }}{% } %} {% for(key in conditions) { %} diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index 4ee18116f1..4e2127a090 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -10,7 +10,7 @@ {{ __("Reports") }}
    diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 5f6419042a..54970dd449 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -17,7 +17,7 @@ frappe.views.ListSidebar = Class.extend({ this.cat_tags = []; }, make: function() { - var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.list_view.doctype}); + var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.doctype}); this.sidebar = $('') .html(sidebar_content) @@ -96,7 +96,8 @@ frappe.views.ListSidebar = Class.extend({ $.each(reports, function(name, r) { if(!r.ref_doctype || r.ref_doctype==me.doctype) { var report_type = r.report_type==='Report Builder' - ? 'Report/' + r.ref_doctype : 'query-report'; + ? `List/${r.ref_doctype}/Report` : 'query-report'; + var route = r.route || report_type + '/' + (r.title || r.name); if(added.indexOf(route)===-1) { @@ -113,11 +114,11 @@ frappe.views.ListSidebar = Class.extend({ } } }); - } + }; // from reference doctype - if(this.list_view.list_renderer.settings.reports) { - add_reports(this.list_view.list_renderer.settings.reports) + if(this.list_view.settings.reports) { + add_reports(this.list_view.settings.reports); } // from specially tagged reports @@ -175,7 +176,7 @@ frappe.views.ListSidebar = Class.extend({ fieldname: 'custom_column', label: __('Custom Column'), default: 0, - onchange: function(e) { + onchange: function() { var checked = d.get_value('custom_column'); if(checked) { $(d.body).find('.frappe-control[data-fieldname="field_name"]').hide(); @@ -202,19 +203,19 @@ frappe.views.ListSidebar = Class.extend({ var custom_column = values.custom_column !== undefined ? values.custom_column : 1; - + var field_name; if(custom_column) { - var field_name = 'kanban_column'; + field_name = 'kanban_column'; } else { - var field_name = + field_name = select_fields .find(df => df.label === values.field_name) .fieldname; } me.add_custom_column_field(custom_column) - .then(function(custom_column) { - return me.make_kanban_board(values.board_name, field_name) + .then(function() { + return me.make_kanban_board(values.board_name, field_name); }) .then(function() { d.hide(); @@ -321,7 +322,7 @@ frappe.views.ListSidebar = Class.extend({ if(has_common(frappe.user_roles, ["System Manager", "Administrator"])) { $(``) - .appendTo($dropdown) + .appendTo($dropdown); } let accounts = frappe.boot.email_accounts; @@ -334,7 +335,7 @@ frappe.views.ListSidebar = Class.extend({ } $(`
  • ${account.email_id}
  • `).appendTo($dropdown); if(account.email_id === "Sent Mail") - divider = false + divider = false; }); $dropdown.find('.new-email-account').click(function() { @@ -354,7 +355,7 @@ frappe.views.ListSidebar = Class.extend({ // if account is holding one user free plan or // if account's expiry date within range of 30 days from today's date - let upgrade_date = frappe.datetime.add_days(get_today(), 30); + let upgrade_date = frappe.datetime.add_days(frappe.datetime.get_today(), 30); if (frappe.boot.limits.users === 1 || upgrade_date >= frappe.boot.limits.expiry) { let upgrade_box = $(`
    List/{doctype}/{last_view} or List + const user_settings = frappe.get_user_settings(doctype); + frappe.set_route('List', doctype, user_settings.last_view || 'List'); + return true; + } + return false; + } + + get view_name() { + // ListView -> List + return this.constructor.name.split('View')[0]; + } - frappe.model.with_doctype(doctype, function () { - if (locals['DocType'][doctype].issingle) { - frappe.set_re_route('Form', doctype); + show() { + this.init().then(() => { + if (frappe.route_options) { + this.set_filters_from_route_options(); + return; } else { - if (!frappe.views.list_view[doctype]) { - frappe.views.list_view[doctype] = new frappe.views.ListView({ - doctype: doctype, - parent: me.make_page(true, 'List/' + doctype) - }); - } else { - frappe.container.change_to(frappe.views.list_view[doctype].page_name); - } - me.set_cur_list(); + this.refresh(); } }); - }, - show: function () { - if(this.re_route_to_view()) { - return; - } - this.set_module_breadcrumb(); - this._super(); - this.set_cur_list(); - cur_list && cur_list.refresh(); - }, - re_route_to_view: function() { - var route = frappe.get_route(); - var doctype = route[1]; - var last_route = frappe.route_history.slice(-2)[0]; - if (route[0] === 'List' && route.length === 2 && frappe.views.list_view[doctype]) { - if(last_route && last_route[0]==='List' && last_route[1]===doctype) { - // last route same as this route, so going back. - // this happens because #List/Item will redirect to #List/Item/List - // while coming from back button, the last 2 routes will be same, so - // we know user is coming in the reverse direction (via back button) - - // example: - // Step 1: #List/Item redirects to #List/Item/List - // Step 2: User hits "back" comes back to #List/Item - // Step 3: Now we cannot send the user back to #List/Item/List so go back one more step - window.history.go(-1); - } else { - frappe.views.list_view[doctype].load_last_view(); - } - return true; - } - }, - set_module_breadcrumb: function () { - if (frappe.route_history.length > 1) { - var prev_route = frappe.route_history[frappe.route_history.length - 2]; - if (prev_route[0] === 'modules') { - var doctype = frappe.get_route()[1], - module = prev_route[1]; - if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) { - // save the last page from the breadcrumb was accessed - frappe.breadcrumbs.set_doctype_module(doctype, module); + } + + get view_user_settings() { + return this.user_settings[this.view_name] || {}; + } + + setup_defaults() { + super.setup_defaults(); + // initialize with saved filters + const saved_filters = this.view_user_settings.filters; + if (saved_filters) { + this.filters = saved_filters; + } else { + // filters in listview_settings + const filters = (this.settings.filters || []).map(f => { + if (f.length === 3) { + f = [this.doctype, f[0], f[1], f[2]]; } - } - } - }, - set_cur_list: function () { - var route = frappe.get_route(); - cur_list = frappe.container.page && frappe.container.page.list_view; - if (cur_list && cur_list.doctype !== route[1]) { - // changing... - cur_list = null; + return f; + }); + + this.filters = filters; } - } -}); + // initialize with saved order by + this.order_by = this.view_user_settings.order_by || 'modified desc'; + // buld menu items) + this.menu_items = this.menu_items.concat(this.get_menu_items()); -$(document).on('save', function (event, doc) { - frappe.views.set_list_as_dirty(doc.doctype); -}); + this.patch_refresh_and_load_lib(); + } -frappe.views.set_list_as_dirty = function (doctype) { - if (frappe.views.trees[doctype]) { - frappe.views.trees[doctype].tree.refresh(); + patch_refresh_and_load_lib() { + // throttle refresh for 1s + this.refresh = this.refresh.bind(this); + this.refresh = frappe.utils.throttle(this.refresh, 1000); + this.load_lib = new Promise(resolve => { + if (this.required_libs) { + frappe.require(this.required_libs, resolve); + } else { + resolve(); + } + }); + // call refresh every 5 minutes + const interval = 5 * 60 * 1000; + setInterval(this.refresh, interval); } - var route = frappe.get_route(); - var current_view = route[2] || 'List'; + set_fields() { + // get from user_settings + if (this.view_user_settings.fields) { + this._fields = this.view_user_settings.fields; + return; + } + // build from meta + super.set_fields(); + } - var list_renderer = frappe.views.list_renderers[doctype]; - if (list_renderer - && list_renderer[current_view] - && list_renderer[current_view].no_realtime) { - return; + setup_page_head() { + super.setup_page_head(); + this.set_primary_action(); } - var list_page = 'List/' + doctype; - if (frappe.pages[list_page]) { - if (frappe.pages[list_page].list_view) { - if (frappe.pages[list_page].list_view.dirty) { - // already refreshing... - return; - } - frappe.pages[list_page].list_view.dirty = true; + set_primary_action() { + if (this.can_create) { + this.page.set_primary_action(__('New'), () => { + this.make_new_doc(); + }, 'octicon octicon-plus'); + } else { + this.page.clear_primary_action(); } } - if (route[0] === 'List' && route[1] === doctype) { - setTimeout(function () { - frappe.pages[list_page].list_view.refresh(); - }, 100); + + make_new_doc() { + const doctype = this.doctype; + const options = {}; + this.filter_area.get().forEach(f => { + if (f[2] === "=" && frappe.model.is_non_std_field(f[1])) { + options[f[1]] = f[3]; + } + }); + frappe.new_doc(doctype, options); } -} -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox']; + setup_view() { + this.setup_columns(); + this.setup_events(); + this.settings.onload && this.settings.onload(this); + } -frappe.views.ListView = frappe.ui.BaseList.extend({ - init: function (opts) { - $.extend(this, opts); + setup_footnote_area() { + const match_rules_list = frappe.perm.get_match_rules(this.doctype); - if (!frappe.boot.user.all_read.includes(this.doctype)) { - frappe.show_not_permitted(frappe.get_route_str()); - return; + if (match_rules_list.length) { + this.$footnote_area = + frappe.utils.set_footnote(this.$footnote_area, this.$frappe_list, + frappe.render_template('list_permission_footer', { + condition_list: match_rules_list + })); } + } - this.is_list_view = true; - this.page_name = 'List/' + this.doctype; - this.dirty = true; - this.tags_shown = false; - - this.page_title = __(this.doctype); - this.page_title = - (this.page_title.toLowerCase().substr(-4) == 'list') && __(this.page_title) - || __(this.page_title) + ' ' + __('List'); - - this.make_page(); - this.setup(); - - // refresh on init - this.refresh(); - }, - - make_page: function () { - this.parent.list_view = this; - this.page = this.parent.page; - - this.$page = $(this.parent).css({ 'min-height': '400px' }); - - $(`
    `) - .appendTo(this.page.main); - - this.page.main.addClass('listview-main-section'); - var module = locals.DocType[this.doctype].module; - - frappe.breadcrumbs.add(module, this.doctype); - }, - - setup: function () { - this.can_delete = frappe.model.can_delete(this.doctype); - this.meta = frappe.get_meta(this.doctype); - this.wrapper = this.$page.find('.frappe-list-area').empty(); - this.allow_delete = true; - - this.load_last_view(); - this.setup_view_variables(); - - this.setup_list_renderer(); - this.init_base_list(false); - this.list_renderer.set_wrapper(); - this.list_renderer_onload(); - - this.show_match_help(); - this.init_menu(); - this.init_sort_selector(); - this.init_filters(); - this.set_title(); - this.init_headers(); - }, - - refresh_surroundings: function() { - this.init_sort_selector(); - this.init_filters(); - this.set_title(); - this.init_headers(); - this.no_result_message = this.list_renderer.make_no_result() - }, - - setup_list_renderer: function () { - frappe.provide('frappe.views.list_renderers.' + this.doctype); - - var list_renderer = frappe.views.list_renderers[this.doctype][this.current_view]; - if (list_renderer) { - this.list_renderer = list_renderer; - this.list_renderer.init_settings(); - return; - } + setup_columns() { + // setup columns for list view + this.columns = []; + + const get_df = frappe.meta.get_docfield.bind(null, this.doctype); - var opts = { - doctype: this.doctype, - list_view: this + // 1st column: title_field or name + if (this.meta.title_field) { + this.columns.push({ + type: 'Subject', + df: get_df(this.meta.title_field) + }); + } else { + this.columns.push({ + type: 'Subject', + df: { + label: __('Name'), + fieldname: 'name' + } + }); } - if (this.current_view === 'List') { - this.list_renderer = new frappe.views.ListRenderer(opts); - } else if (this.current_view === 'Gantt') { - this.list_renderer = new frappe.views.GanttView(opts); - } else if (this.current_view === 'Calendar') { - this.list_renderer = new frappe.views.CalendarView(opts); - } else if (this.current_view === 'Image') { - this.list_renderer = new frappe.views.ImageView(opts); - } else if (this.current_view === 'Kanban') { - this.list_renderer = new frappe.views.KanbanView(opts); - } else if (this.current_view === 'Inbox') { - this.list_renderer = new frappe.views.InboxView(opts) + // 2nd column: Status indicator + if (frappe.has_indicator(this.doctype)) { + // indicator + this.columns.push({ + type: 'Status' + }); } - }, - render_view: function (values) { - this.list_renderer.render_view(values); - }, + const fields_in_list_view = this.get_fields_in_list_view(); + // Add rest from in_list_view docfields + this.columns = this.columns.concat( + fields_in_list_view + .filter(df => df.fieldname !== 'status') + .map(df => ({ + type: 'Field', + df + })) + ); - set_title: function () { - if (this.list_renderer.page_title) { - this.page.set_title(this.list_renderer.page_title); - } else { - this.page.set_title(this.page_title); - } - }, + // limit to 4 columns + this.columns = this.columns.slice(0, 4); + } - load_last_view: function () { - var us = frappe.get_user_settings(this.doctype); - var route = ['List', this.doctype]; + get_no_result_message() { + const new_button = this.can_create ? + `

    ` : ''; - if (us.last_view && frappe.views.view_modes.includes(us.last_view)) { - route.push(us.last_view); + return `
    +

    ${__('No {0} found', [__(this.doctype)])}

    + ${new_button} +
    `; + } - if (us.last_view === 'Kanban') { - route.push(us['Kanban'].last_kanban_board); - } + get_args() { + const args = super.get_args(); - if (us.last_view === 'Inbox') { - route.push(us['Inbox'].last_email_account) - } - } else { - route.push('List'); - } + return Object.assign(args, { + with_comment_count: true + }); + } - frappe.set_route(route); - }, + // freeze(toggle) { + // if (this.view_name !== 'List') return; + + // this.$freeze.toggle(toggle); + // this.$result.toggle(!toggle); + + // const columns = this.columns; + // if (toggle) { + // if (this.$freeze.find('.freeze-row').length > 0) return; + + // const html = ` + // ${this.get_header_html()} + // ${Array.from(new Array(10)).map(loading_row).join('')} + // `; + // this.$freeze.html(html); + // } + + // function loading_row() { + // return ` + //
    + //
    + //
    + // ${columns.slice(1).map(c => `
    `).join('')} + //
    + //
    + //
    + // `; + // } + // } + + toggle_result_area() { + super.toggle_result_area(); + this.toggle_delete_button( + this.$result.find('.list-row-check:checked').length > 0 + ); + } - init_headers: function () { - this.page.main.find('.list-headers > .list-item--head').hide(); - this.list_header = this.page.main.find('.list-headers > ' - + '.list-item--head[data-list-renderer="' - + this.list_renderer.name +'"]'); + before_render() { + this.settings.before_render && this.settings.before_render(); + frappe.model.user_settings.save(this.doctype, 'last_view', this.view_name); + this.save_view_user_settings({ + fields: this._fields, + filters: this.filter_area.get(), + order_by: this.sort_selector.get_sql_string() + }); + } - if(this.list_header.length > 0) { - this.list_header.show(); - return; - } + render() { + if (!this.start === 0) { + // append new rows + } - var html = this.list_renderer.get_header_html(); - if(!html) { - this.list_header = $(); - return; + if (this.data.length > 0) { + const html = ` + ${this.get_header_html()} + ${this.data.map(doc => this.get_list_row_html(doc)).join('')} + `; + this.$result.html(html); } + this.render_count(); + this.render_tags(); + } - this.list_header = $(html).appendTo(this.page.main.find('.list-headers')); + render_count() { + this.get_count_html() + .then(html => { + this.$result.find('.list-count').html(html); + }); + } - this.setup_like(); - this.setup_select_all(); - this.setup_delete(); - }, + render_tags() { + const $list_rows = this.$result.find('.list-row-container'); + + this.data.forEach((d, i) => { + let tag_html = $(`
    + +
    `).appendTo($list_rows.get(i)); + + // add tags + let tag_editor = new frappe.ui.TagEditor({ + parent: tag_html.find('.list-tag'), + frm: { + doctype: this.doctype, + docname: d.name + }, + list_sidebar: this.list_sidebar, + user_tags: d._user_tags, + on_change: function (user_tags) { + d._user_tags = user_tags; + } + }); - list_renderer_onload: function () { - if (this.list_renderer.settings.onload) { - this.list_renderer.settings.onload(this); - } - }, - - set_sidebar_height: function () { - var h_main = this.page.sidebar.height(); - var h_side = this.$page.find('.layout-side-section').height(); - if (h_side > h_main) - this.$page.find('.layout-main-section').css({ 'min-height': h_side }); - }, - - init_filters: function () { - this.make_standard_filters(); - this.filter_list = new frappe.ui.FilterList({ - base_list: this, - parent: this.wrapper.find('.list-filters').show(), - doctype: this.doctype, - default_filters: [] - }); - this.set_filters(this.list_renderer.filters); - }, - - set_filters: function (filters) { - var me = this; - $.each(filters, function (i, f) { - var hidden = false - if (f.length === 3) { - f = [me.doctype, f[0], f[1], f[2]] - } else if (f.length === 5) { - hidden = f.pop(4) || false - } - me.filter_list.add_filter(f[0], f[1], f[2], f[3], hidden); + tag_editor.wrapper.on('click', '.tagit-label', (e) => { + const $this = $(e.currentTarget); + this.filter_area.add(this.doctype, '_user_tags', '=', $this.text()); + }); }); - }, + } - init_sort_selector: function () { - var me = this; - var order_by = this.list_renderer.order_by; + get_header_html() { + const subject_field = this.columns[0].df; + let subject_html = ` + + + + + ${__(subject_field.label)} + `; + const $columns = this.columns.map(col => { + let classes = [ + 'list-row-col ellipsis', + col.type == 'Subject' ? 'list-subject level' : 'hidden-xs', + frappe.model.is_numeric_field(col.df) ? 'text-right' : '' + ].join(' '); + + return ` +
    + ${col.type === 'Subject' ? subject_html : ` + ${__(col.df && col.df.label || col.type)}`} +
    + `; + }).join(''); + + return this.get_header_html_skeleton($columns, ''); + } - this.sort_selector = new frappe.ui.SortSelector({ - parent: this.wrapper.find('.list-filters'), - doctype: this.doctype, - args: order_by, - change: function () { me.run(); } - }); - }, + get_header_html_skeleton(left = '', right = '') { + return ` +
    +
    + ${left} +
    +
    +
    + + +
    +
    +
    + ${right} +
    +
    + `; + } - show_match_help: function () { - var me = this; - var match_rules_list = frappe.perm.get_match_rules(this.doctype); - var perm = frappe.perm.get_perm(this.doctype); + get_left_html(doc) { + return this.columns.map(col => this.get_column_html(col, doc)).join(''); + } - if (match_rules_list.length) { - this.footnote_area = - frappe.utils.set_footnote(this.footnote_area, this.$page.find('.layout-main-section'), - frappe.render_template('list_permission_footer', { - condition_list: match_rules_list - })); - $(this.footnote_area).css({ 'margin-top': '0px' }); - } - }, - - setup_view_variables: function () { - var route = frappe.get_route(); - this.last_view = this.current_view || ''; - this.current_view = route[2] || 'List'; - }, - - init_base_list: function (auto_run) { - var me = this; - // init list - this.make({ - method: 'frappe.desk.reportview.get', - save_user_settings: true, - get_args: this.get_args, - parent: this.wrapper, - freeze: true, - start: 0, - page_length: this.list_renderer.page_length, - show_filters: false, - new_doctype: this.doctype, - no_result_message: this.list_renderer.make_no_result(), - show_no_result: function() { - return me.list_renderer.show_no_result; - } - }); + get_right_html(doc) { + return this.get_meta_html(doc); + } - this.setup_make_new_doc(); + get_list_row_html(doc) { + return this.get_list_row_html_skeleton(this.get_left_html(doc), this.get_right_html(doc)); + } - if (auto_run !== false && auto_run !== 0) - this.refresh(); - }, - - setup_make_new_doc: function () { - var me = this; - // make_new_doc can be overridden so that default values can be prefilled - // for example - communication list in customer - if (this.list_renderer.settings.list_view_doc) { - this.list_renderer.settings.list_view_doc(this); - } else { - var doctype = this.list_renderer.no_result_doctype? this.list_renderer.no_result_doctype: this.doctype - $(this.wrapper).on('click', `button[list_view_doc='${doctype}']`, function () { - if (me.list_renderer.make_new_doc) - me.list_renderer.make_new_doc() - else - me.make_new_doc.apply(me, [me.doctype]); - }); + get_list_row_html_skeleton(left = '', right = '') { + return ` +
    +
    +
    + ${left} +
    +
    + ${right} +
    +
    +
    + `; + } + + get_column_html(col, doc) { + if (col.type === 'Status') { + return ` + + `; } - }, - refresh: function (dirty) { - var me = this; + const df = col.df || {}; + const label = df.label; + const fieldname = df.fieldname; + const value = doc[fieldname] || ''; + + // listview_setting formatter + const formatters = this.settings.formatters; + + const format = () => { + if (formatters && formatters[fieldname]) { + return formatters[fieldname](value, df, doc); + } else if (df.fieldtype === 'Code') { + return value; + } else { + return frappe.format(value, df, null, doc); + } + }; + + const field_html = () => { + let html; + if (df.fieldtype === 'Image') { + html = df.options ? + `` : + `
    + +
    `; + } else if (df.fieldtype === 'Select') { + html = ` + ${__(value)} + `; + } else if (df.fieldtype === 'Link') { + html = ` + ${value} + `; + } else { + html = ` + ${format()} + `; + } - if (dirty !== undefined) this.dirty = dirty; + return ` + ${html} + `; + }; + + const class_map = { + Subject: 'list-subject level', + Field: 'hidden-xs' + }; + const css_class = [ + 'list-row-col ellipsis', + class_map[col.type], + frappe.model.is_numeric_field(df) ? 'text-right' : '' + ].join(' '); + + const html_map = { + Subject: this.get_subject_html(doc), + Field: field_html() + }; + const column_html = html_map[col.type]; + + return ` +
    + ${column_html} +
    + `; + } - this.refresh_sidebar(); - this.setup_view_variables(); + get_meta_html(doc) { + let html = ''; + if (doc[this.meta.title_field || ''] !== doc.name) { + html += ` + + `; + } + const modified = comment_when(doc.modified, true); + + const last_assignee = JSON.parse(doc._assign || '[]').slice(-1)[0]; + const assigned_to = last_assignee ? + ` + ${frappe.avatar(last_assignee)} + ` : + ``; + + const comment_count = + ` + + ${doc._comment_count > 99 ? "99+" : doc._comment_count} + `; + + html += ` + +
    + ${this.get_indicator_dot(doc)} +
    + `; + + return html; + } - if (this.list_renderer.should_refresh()) { - this.setup_list_renderer(); + get_count_html() { + const current_count = this.data.length; - if (this.list_renderer.load_last_view && this.list_renderer.load_last_view()) { - // let the list_renderer load the last view for the current view - // for e.g last kanban board for kanban view - return; + return frappe.call({ + method: 'frappe.model.db_query.get_count', + args: { + doctype: this.doctype, + filters: this.filter_area.get() } + }).then(r => { + const count = r.message || current_count; + const str = __('{0} of {1}', [current_count, count]); + const html = `${str}`; + return html; + }); + } - this.refresh_surroundings(); - this.dirty = true; - } + get_form_link(doc) { + return '#Form/' + this.doctype + '/' + doc.name; + } - if (this.list_renderer.settings.refresh) { - this.list_renderer.settings.refresh(this); + get_subject_html(doc) { + let user = frappe.session.user; + let subject_field = this.columns[0].df; + let subject = strip_html(doc[subject_field.fieldname]); + + const liked_by = JSON.parse(doc._liked_by || '[]'); + let heart_class = liked_by.includes(user) ? + 'liked-by' : 'text-extra-muted not-liked'; + + const seen = JSON.parse(doc._seen || '[]') + .includes(user) ? 'seen' : ''; + + let subject_html = ` + + + + + + + + ${subject} + + + `; + + return subject_html; + } + + get_indicator_html(doc) { + const indicator = frappe.get_indicator(doc, this.doctype); + if (indicator) { + return ` + ${__(indicator[0])} + `; } + return ''; + } - this.set_filters_before_run(); - this.execute_run(); - }, + get_indicator_dot(doc) { + const indicator = frappe.get_indicator(doc, this.doctype); + if (!indicator) return ''; + return ``; + } - execute_run: function () { - if (this.dirty) { - this.run(); - } else { - if (new Date() - (this.last_updated_on || 0) > 30000) { - // older than 5 mins, refresh - this.run(); + setup_events() { + // filterable events + this.$result.on('click', '.filterable', e => { + if (e.metaKey || e.ctrlKey) return; + e.stopPropagation(); + const $this = $(e.currentTarget); + const filters = $this.attr('data-filter').split('|'); + + const filters_to_apply = filters.map(f => { + f = f.split(','); + if (f[2] === 'Today') { + f[2] = frappe.datetime.get_today(); + } else if (f[2] == 'User') { + f[2] = frappe.session.user; + } + return [this.doctype, f[0], f[1], f.slice(2).join(',')]; + }); + this.filter_area.add(filters_to_apply); + }); + + this.$result.on('click', '.list-row', (e) => { + const $target = $(e.target); + + // tick checkbox if Ctrl/Meta key is pressed + if (e.ctrlKey || e.metaKey && !$target.is('a')) { + const $list_row = $(e.currentTarget); + const $check = $list_row.find('.list-row-checkbox'); + $check.prop('checked', !$check.prop('checked')); + e.preventDefault(); + this.on_row_checked(); + return; } - } - }, - save_user_settings_locally: function (args) { + // don't open form when checkbox, like, filterable are clicked + if ($target.hasClass('filterable') || + $target.hasClass('octicon-heart') || + $target.is(':checkbox') || + $target.is('a') + ) { + return; + } - frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.list_renderer.name); - var user_settings_common = frappe.model.user_settings[this.doctype]; - var user_settings = frappe.model.user_settings[this.doctype][this.list_renderer.name]; + // open form + const link = $(this).find('.list-subject a').get(0); + if (link) { + window.location.href = link.href; + return false; + } + }); - if (!user_settings) return; + // toggle tags + this.list_sidebar.parent.on('click', '.list-tag-preview', () => { + this.toggle_tags(); + }); - var different = false; - if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) { - // settings are dirty if filters change - user_settings.filters = args.filters; - different = true; - } + this.$no_result.find('.btn-new-doc').click(() => this.make_new_doc()); - if (user_settings.order_by !== args.order_by) { - user_settings.order_by = args.order_by; - different = true; - } + this.setup_check_events(); + this.setup_like(); + } - // never save page_length in user_settings - // if (user_settings.page_length !== args.page_length) { - // user_settings.page_length = args.page_length || 20 - // different = true; - // } + setup_check_events() { + this.$result.on('change', 'input[type=checkbox]', e => { + const $target = $(e.currentTarget); - // save fields in list settings - if (args.save_user_settings_fields) { - user_settings.fields = args.fields; - } + if ($target.is('.list-header-subject .list-check-all')) { + const $check = this.$result.find('.checkbox-actions .list-check-all'); + $check.prop('checked', $target.prop('checked')); + $check.trigger('change'); + } else if ($target.is('.checkbox-actions .list-check-all')) { + const $check = this.$result.find('.list-header-subject .list-check-all'); + $check.prop('checked', $target.prop('checked')); - // save last view - if (user_settings_common.last_view !== this.current_view - && frappe.views.view_modes.includes(this.current_view)) { - user_settings_common.last_view = this.current_view; - different = true; - } + this.$result.find('.list-row-checkbox') + .prop('checked', $target.prop('checked')); + } - if (different) { - user_settings_common.updated_on = moment().toString(); - } - }, + this.on_row_checked(); + }); - set_filters_before_run: function () { - // set filters from frappe.route_options - // before switching pages, frappe.route_options can have pre-set filters - // for the list view - var me = this; + this.$result.on('click', '.list-row-checkbox', e => { + const $target = $(e.currentTarget); - if (frappe.route_options) { - this.set_filters_from_route_options(); - this.dirty = true; - } - }, + // shift select checkboxes + if (e.shiftKey && this.$checkbox_cursor && !$target.is(this.$checkbox_cursor)) { + const name_1 = this.$checkbox_cursor.data().name; + const name_2 = $target.data().name; + const index_1 = this.data.findIndex(d => d.name === name_1); + const index_2 = this.data.findIndex(d => d.name === name_2); + let [min_index, max_index] = [index_1, index_2]; - run: function (more) { - // set filter from route - var me = this; + if (min_index > max_index) { + [min_index, max_index] = [max_index, min_index]; + } - if (this.fresh && !more) { - return; - } + let docnames = this.data.slice(min_index + 1, max_index).map(d => d.name); + const selector = docnames.map(name => `.list-row-checkbox[data-name="${name}"]`).join(','); + this.$result.find(selector).prop('checked', true); + } - if (this.list_renderer.settings.before_run) { - this.list_renderer.settings.before_run(this); - } + this.$checkbox_cursor = $target; + }); + } - if (!this.list_renderer.settings.use_route) { - var route = frappe.get_route(); - if (route[2] && !frappe.views.view_modes.includes(route[2])) { - $.each(frappe.utils.get_args_dict_from_url(route[2]), function (key, val) { - me.set_filter(key, val, true); - }); + setup_like() { + this.$result.on('click', '.like-action', frappe.ui.click_toggle_like); + this.$result.on('click', '.list-liked-by-me', e => { + const $this = $(e.currentTarget); + $this.toggleClass('active'); + + if ($this.hasClass('active')) { + this.filter_area.add(this.doctype, '_liked_by', 'like', '%' + frappe.session.user + '%'); + } else { + this.filter_area.remove('_liked_by'); } - } + }); - if(this.list_header) { - this.list_header.find('.list-liked-by-me') - .toggleClass('text-extra-muted not-liked', !this.is_star_filtered()); - } + frappe.ui.setup_like_popover(this.$result, '.liked-by'); + } - this.last_updated_on = new Date(); - this.dirty = false; - // set a fresh so that multiple refreshes do not happen - // at the same time. This is true when deleting. - // AJAX response will try to refresh and list_update notification - // via async will also try to update. - // It is not possible to guess which will reach first - // (most probably async will) but this is a forced way - // to prevent instant refreshes on mutilple triggers - // in a loosly coupled way. - this.fresh = true; - setTimeout(function () { - me.fresh = false; - }, 1000); - - this._super(more); - - if (this.list_renderer.settings.post_render) { - this.list_renderer.settings.post_render(this); - } + on_row_checked() { + this.$list_head_subject = this.$list_head_subject || this.$result.find('header .list-header-subject'); + this.$checkbox_actions = this.$checkbox_actions || this.$result.find('header .checkbox-actions'); - this.wrapper.on('render-complete', function() { - me.list_renderer.after_refresh(); - }) - }, - - get_args: function () { - var args = { - doctype: this.doctype, - fields: this.list_renderer.fields, - filters: this.get_filters_args(), - order_by: this.get_order_by_args(), - with_comment_count: true - } - return args; - }, - get_filters_args: function() { - var filters = []; - if(this.filter_list) { - // get filters from filter_list - filters = this.filter_list.get_filters(); - } else { - filters = this.list_renderer.filters; - } - // remove duplicates - var uniq = filters.uniqBy(JSON.stringify); - return uniq; - }, - get_order_by_args: function() { - var order_by = ''; - if(this.sort_selector) { - // get order_by from sort_selector - order_by = $.format('`tab{0}`.`{1}` {2}', - [this.doctype, this.sort_selector.sort_by, this.sort_selector.sort_order]); + this.$checks = this.$result.find('.list-row-checkbox:checked'); + + this.$list_head_subject.toggle(this.$checks.length === 0); + this.$checkbox_actions.toggle(this.$checks.length > 0); + + if (this.$checks.length === 0) { + this.$list_head_subject.find('.list-select-all').prop('checked', false); } else { - order_by = this.list_renderer.order_by; - } - return order_by; - }, - assigned_to_me: function () { - this.filter_list.add_filter(this.doctype, '_assign', 'like', '%' + frappe.session.user + '%'); - this.run(); - }, - liked_by_me: function () { - this.filter_list.add_filter(this.doctype, '_liked_by', 'like', '%' + frappe.session.user + '%'); - this.run(); - }, - remove_liked_by_me: function () { - this.filter_list.get_filter('_liked_by').remove(); - }, - is_star_filtered: function () { - return this.filter_list.filter_exists(this.doctype, '_liked_by', 'like', '%' + frappe.session.user + '%'); - }, - init_menu: function () { - var me = this; - this.$page.on('click', '.list-tag-preview', function () { me.toggle_tags(); }); - - // Refresh button for large screens - this.page.set_secondary_action(__('Refresh'), function () { - me.refresh(true); - }, 'octicon octicon-sync') - .addClass('hidden-xs'); - - // Refresh button as menu item in small screens - this.page.add_menu_item(__('Refresh'), function () { - me.refresh(true); - }, 'octicon octicon-sync') - .addClass('visible-xs'); - - if (frappe.model.can_import(this.doctype)) { - this.page.add_menu_item(__('Import'), function () { - frappe.set_route('List', 'Data Import', { - reference_doctype: me.doctype - }); - }, true); - } - if (frappe.model.can_set_user_permissions(this.doctype)) { - this.page.add_menu_item(__('User Permissions'), function () { - frappe.set_route('List', 'User Permission', { - allow: me.doctype - }); - }, true); - } - if (frappe.user_roles.includes('System Manager')) { - this.page.add_menu_item(__('Role Permissions Manager'), function () { - frappe.set_route('permission-manager', { - doctype: me.doctype - }); - }, true); - this.page.add_menu_item(__('Customize'), function () { - frappe.set_route('Form', 'Customize Form', { - doc_type: me.doctype - }) - }, true); + this.$checkbox_actions.find('.list-header-meta').html( + __('{0} items selected', [this.$checks.length]) + ); + this.$checkbox_actions.show(); + this.$list_head_subject.hide(); } - this.make_bulk_assignment(); - if(frappe.model.can_print(this.doctype)) { - this.make_bulk_printing(); + if (this.can_delete) { + this.toggle_delete_button(this.$checks.length > 0); } + } - // add to desktop - this.page.add_menu_item(__('Add to Desktop'), function () { - frappe.add_to_desktop(me.doctype, me.doctype); - }, true); - - if (frappe.user_roles.includes('System Manager') && frappe.boot.developer_mode === 1) { - // edit doctype - this.page.add_menu_item(__('Edit DocType'), function () { - frappe.set_route('Form', 'DocType', me.doctype); - }, true); + toggle_delete_button(toggle) { + if (toggle) { + this.page.set_primary_action(__('Delete'), + () => this.delete_items(), + 'octicon octicon-trashcan' + ).addClass('btn-danger'); + } else { + this.page.btn_primary.removeClass('btn-danger'); + this.set_primary_action(); } + } - }, - make_bulk_assignment: function () { - - var me = this; + toggle_tags() { + this.$result.toggleClass('tags-shown'); + } - //bulk assignment - me.page.add_menu_item(__('Assign To'), function () { + delete_items() { + const docnames = this.get_checked_items(true); - var docnames = me.get_checked_items().map(function (doc) { - return doc.name; - }); + frappe.confirm(__('Delete {0} items permanently?', [docnames.length]), + () => { + frappe.call({ + method: 'frappe.desk.reportview.delete_items', + freeze: true, + args: { + items: docnames, + doctype: this.doctype + } + }).then((r) => { + let failed = r.message; + if (!failed) failed = []; - if (docnames.length >= 1) { - me.dialog = new frappe.ui.form.AssignToDialog({ - obj: me, - method: 'frappe.desk.form.assign_to.add_multiple', - doctype: me.doctype, - docname: docnames, - bulk_assign: true, - re_assign: true, - callback: function () { - me.refresh(true); + if (failed.length && !r._server_messages) { + frappe.throw(__('Cannot delete {0}', [failed.map(f => f.bold()).join(', ')])); + } + if (failed.length < docnames.length) { + frappe.utils.play_sound('delete'); + this.refresh(true); } }); - me.dialog.clear(); - me.dialog.show(); - } - else { - frappe.msgprint(__('Select records for assignment')) - } - }, true); - - }, - make_bulk_printing: function () { - var me = this; - var print_settings = frappe.model.get_doc(':Print Settings', 'Print Settings') - var allow_print_for_draft = cint(print_settings.allow_print_for_draft) - var is_submittable = frappe.model.is_submittable(me.doctype) - var allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled) - - //bulk priting - me.page.add_menu_item(__('Print'), function () { - var items = me.get_checked_items(); - - var valid_docs = - items.filter(function (doc) { - return !is_submittable || doc.docstatus === 1 || - (allow_print_for_cancelled && doc.docstatus == 2) || - (allow_print_for_draft && doc.docstatus == 0) || - frappe.user_roles.includes('Administrator') - }).map(function (doc) { - return doc.name - }); - - var invalid_docs = items.filter(function (doc) { - return !valid_docs.includes(doc.name); - }); - - if (invalid_docs.length >= 1) { - frappe.msgprint(__('You selected Draft or Cancelled documents')) - return; } + ); + } - if (valid_docs.length >= 1) { - - var dialog = new frappe.ui.Dialog({ - title: 'Print Documents', - fields: [ - { 'fieldtype': 'Check', 'label': __('With Letterhead'), 'fieldname': 'with_letterhead' }, - { 'fieldtype': 'Select', 'label': __('Print Format'), 'fieldname': 'print_sel' }, - ] - }); + get_checked_items(only_docnames) { + const docnames = Array.from(this.$checks || []) + .map(check => $(check).data().name); - dialog.set_primary_action(__('Print'), function () { - var args = dialog.get_values(); - if (!args) return; - var default_print_format = locals.DocType[me.doctype].default_print_format; - var with_letterhead = args.with_letterhead ? 1 : 0; - var print_format = args.print_sel ? args.print_sel : default_print_format; - - var json_string = JSON.stringify(valid_docs); - var w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' - + 'doctype=' + encodeURIComponent(me.doctype) - + '&name=' + encodeURIComponent(json_string) - + '&format=' + encodeURIComponent(print_format) - + '&no_letterhead=' + (with_letterhead ? '0' : '1')); - if (!w) { - frappe.msgprint(__('Please enable pop-ups')); return; - } - }); + if (only_docnames) return docnames; - var print_formats = frappe.meta.get_print_formats(me.doctype); - dialog.fields_dict.print_sel.$input.empty().add_options(print_formats); + return this.data.filter(d => docnames.includes(d.name)); + } - dialog.show(); - } - else { - frappe.msgprint(__('Select atleast 1 record for printing')) - } - }, true); - }, - - setup_like: function () { - var me = this; - this.$page.find('.result-list').on('click', '.like-action', frappe.ui.click_toggle_like); - this.list_header.find('.list-liked-by-me').on('click', function () { - if (me.is_star_filtered()) { - me.remove_liked_by_me(); - } else { - me.liked_by_me(); - } - }); + save_view_user_settings(obj) { + return frappe.model.user_settings.save(this.doctype, this.view_name, obj); + } - if (!frappe.dom.is_touchscreen()) { - frappe.ui.setup_like_popover(this.$page.find('.result-list'), '.liked-by'); + on_update(data) { + if (data.doctype === this.doctype) { + this.refresh(); } - }, - - setup_select_all: function () { - var me = this; + } - if (this.can_delete || this.list_renderer.settings.selectable) { - this.list_header.find('.list-select-all').on('click', function () { - me.$page.find('.list-row-checkbox').prop('checked', $(this).prop('checked')); + get_menu_items() { + const doctype = this.doctype; + const items = []; + + if (frappe.model.can_import(doctype)) { + items.push({ + label: __('Import'), + action: () => frappe.set_route('data-import-tool', { + doctype + }), + standard: true + }); + } + if (frappe.model.can_set_user_permissions(doctype)) { + items.push({ + label: __('User Permissions'), + action: () => frappe.set_route('List', 'User Permission', { + allow: doctype + }), + standard: true + }); + } + if (frappe.user_roles.includes('System Manager')) { + items.push({ + label: __('Role Permissions Manager'), + action: () => frappe.set_route('permission-manager', { + doctype + }), + standard: true }); - this.$page.on('click', '.list-row-checkbox', function (event) { - // multi-select using shift key - var $this = $(this); - if (event.shiftKey && $this.prop('checked')) { - var $end_row = $this.parents('.list-item-container'); - var $start_row = $end_row.prevAll('.list-item-container') - .find('.list-row-checkbox:checked').last().parents('.list-item-container'); - if ($start_row) { - $start_row.nextUntil($end_row).find('.list-row-checkbox').prop('checked', true); - } - } + items.push({ + label: __('Customize'), + action: () => frappe.set_route('Form', 'Customize Form', { + doc_type: doctype + }), + standard: true }); } - }, - setup_delete: function () { - var me = this; - if (!this.can_delete) { - return; + // utility + const bulk_assignment = () => { + return { + label: __('Assign To'), + action: () => { + const docnames = this.get_checked_items(true); + if (docnames.length > 0) { + const dialog = new frappe.ui.form.AssignToDialog({ + obj: this, + method: 'frappe.desk.form.assign_to.add_multiple', + doctype: this.doctype, + docname: docnames, + bulk_assign: true, + re_assign: true, + callback: () => this.refresh(true) + }); + dialog.clear(); + dialog.show(); + } else { + frappe.msgprint(__('Select records for assignment')); + } + }, + standard: true + }; + }; + + const bulk_printing = () => { + const print_settings = frappe.model.get_doc(':Print Settings', 'Print Settings'); + const allow_print_for_draft = cint(print_settings.allow_print_for_draft); + const is_submittable = frappe.model.is_submittable(this.doctype); + const allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); + + return { + label: __('Print'), + action: () => { + const items = this.get_checked_items(); + + const valid_docs = items.filter(doc => { + return !is_submittable || doc.docstatus === 1 || + (allow_print_for_cancelled && doc.docstatus == 2) || + (allow_print_for_draft && doc.docstatus == 0) || + frappe.user.has_role('Administrator'); + }).map(doc => doc.name); + + var invalid_docs = items.filter(doc => !valid_docs.includes(doc.name)); + + if (invalid_docs.length > 0) { + frappe.msgprint(__('You selected Draft or Cancelled documents')); + return; + } + + if (valid_docs.length > 0) { + const dialog = new frappe.ui.Dialog({ + title: __('Print Documents'), + fields: [{ + 'fieldtype': 'Check', + 'label': __('With Letterhead'), + 'fieldname': 'with_letterhead' + }, + { + 'fieldtype': 'Select', + 'label': __('Print Format'), + 'fieldname': 'print_sel', + options: frappe.meta.get_print_formats(this.doctype) + }] + }); + + dialog.set_primary_action(__('Print'), args => { + if (!args) return; + const default_print_format = frappe.get_meta(this.doctype).default_print_format; + const with_letterhead = args.with_letterhead ? 1 : 0; + const print_format = args.print_sel ? args.print_sel : default_print_format; + const json_string = JSON.stringify(valid_docs); + + const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + + 'doctype=' + encodeURIComponent(this.doctype) + + '&name=' + encodeURIComponent(json_string) + + '&format=' + encodeURIComponent(print_format) + + '&no_letterhead=' + (with_letterhead ? '0' : '1')); + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); + return; + } + }); + + dialog.show(); + } else { + frappe.msgprint(__('Select atleast 1 record for printing')); + } + }, + standard: true + }; + }; + + // bulk assignment + items.push(bulk_assignment()); + + if (frappe.model.can_print(doctype)) { + items.push(bulk_printing()); } - this.$page.on('change', '.list-row-checkbox, .list-select-all', function() { - me.toggle_delete(); - }); - // after delete, hide delete button - this.wrapper.on('render-complete', function () { - me.toggle_delete(); + + // add to desktop + items.push({ + label: __('Add to Desktop'), + action: () => frappe.add_to_desktop(doctype, doctype), + standard: true }); - }, - - toggle_delete: function () { - var me = this; - var checked_items = this.get_checked_items(); - var checked_items_status = this.$page.find('.checked-items-status'); - - if (checked_items.length > 0) { - this.page.set_primary_action(__('Delete'), function () { - me.delete_items() - }, 'octicon octicon-trashcan') - .addClass('btn-danger'); - - checked_items_status.text( - checked_items.length == 1 - ? __('1 item selected') - : __('{0} items selected', [checked_items.length]) - ) - checked_items_status.removeClass('hide'); - } else { - this.page.btn_primary.removeClass('btn-danger'); - this.set_primary_action(); - checked_items_status.addClass('hide'); - } - }, - toggle_tags: function () { - if (this.tags_shown) { - $('.tag-row').addClass('hide'); - this.tags_shown = false; - } else { - $('.tag-row').removeClass('hide'); - this.tags_shown = true; + if (frappe.user.has_role('System Manager') && frappe.boot.developer_mode === 1) { + // edit doctype + items.push({ + label: __('Edit DocType'), + action: () => frappe.set_route('Form', 'DocType', doctype), + standard: true + }); } - }, - get_checked_items: function () { - var names = this.$page.find('.list-row-checkbox:checked').map(function (i, item) { - return cstr($(item).data().name); - }).toArray(); + return items; + } - return this.data.filter(function (doc) { - return names.includes(doc.name); - }); - }, + set_filters_from_route_options() { + const filters = []; + for (let field in frappe.route_options) { + var value = frappe.route_options[field]; + var doctype = null; - set_primary_action: function () { - if (this.list_renderer.settings.set_primary_action) { - this.list_renderer.settings.set_primary_action(this); - } else { - this._super(); + // if `Child DocType.fieldname` + if (field.includes('.')) { + doctype = field.split('.')[0]; + field = field.split('.')[1]; + } + + // find the table in which the key exists + // for example the filter could be {"item_code": "X"} + // where item_code is in the child table. + + // we can search all tables for mapping the doctype + if (!doctype) { + doctype = frappe.meta.get_doctype_for_field(this.doctype, field); + } + + if (doctype) { + if ($.isArray(value)) { + filters.push([doctype, field, value[0], value[1]]); + } else { + filters.push([doctype, field, "=", value]); + } + } } - }, + frappe.route_options = null; + + this.filter_area.add(filters); + } + + static trigger_list_update(data) { + const doctype = data.doctype; + if (!doctype) return; + frappe.provide('frappe.views.trees'); - delete_items: function () { - var me = this; - var to_delete = this.get_checked_items(); - if (!to_delete.length) + // refresh tree view + if (frappe.views.trees[doctype]) { + frappe.views.trees[doctype].tree.refresh(); return; + } - var docnames = to_delete.map(function (doc) { - return doc.name; - }); + // refresh list view + const page_name = frappe.get_route_str(); + const list_view = frappe.views.list_view[page_name]; + list_view && list_view.on_update(data); + } +}; - frappe.confirm(__('Delete {0} items permanently?', [to_delete.length]), - function () { - return frappe.call({ - method: 'frappe.desk.reportview.delete_items', - freeze: true, - args: { - items: docnames, - doctype: me.doctype - }, - callback: function () { - me.$page.find('.list-select-all').prop('checked', false); - frappe.utils.play_sound('delete'); - me.refresh(true); - } - }) - } - ); - }, - refresh_sidebar: function () { - //TODO: refresh if already exist - this.list_sidebar = new frappe.views.ListSidebar({ - doctype: this.doctype, - stats: this.list_renderer.stats, - parent: this.$page.find('.layout-side-section'), - set_filter: this.set_filter.bind(this), - default_filters: this.list_renderer.filters, - page: this.page, - list_view: this - }) - } -}); +$(document).on('save', function (event, doc) { + frappe.views.ListView.trigger_list_update(doc); +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/misc/file_manager.js b/frappe/public/js/frappe/misc/file_manager.js new file mode 100644 index 0000000000..f57cb8200a --- /dev/null +++ b/frappe/public/js/frappe/misc/file_manager.js @@ -0,0 +1,56 @@ +frappe.provide('frappe.file_manager'); + +frappe.file_manager = function() { + let files_to_move = []; + let old_folder = null; + let new_folder = null; + + function cut(files, old_folder_) { + files_to_move = files; + old_folder = old_folder_; + } + + function paste(new_folder_) { + return new Promise((resolve, reject) => { + if (files_to_move.length === 0 || !old_folder) { + reset(); + resolve(); + return; + } + new_folder = new_folder_; + + frappe.call({ + method:"frappe.core.doctype.file.file.move_file", + args: { + file_list: files_to_move, + new_parent: new_folder, + old_parent: old_folder + }, + callback: r => { + reset(); + resolve(r); + } + }).fail(reject); + }); + } + + function reset() { + files_to_move = []; + old_folder = null; + new_folder = null; + } + + return { + cut, + paste, + get can_paste() { + return Boolean(files_to_move.length > 0 && old_folder); + }, + get old_folder() { + return old_folder; + }, + get files_to_move() { + return files_to_move; + } + }; +}(); diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js index 8fcc17a9c6..8d47a7ccd6 100644 --- a/frappe/public/js/frappe/misc/utils.js +++ b/frappe/public/js/frappe/misc/utils.js @@ -195,13 +195,11 @@ frappe.utils = { }, set_footnote: function(footnote_area, wrapper, txt) { if(!footnote_area) { - footnote_area = $('
    ') + footnote_area = $('
    ') .appendTo(wrapper); } if(txt) { - if(!txt.includes('

    ')) - txt = '

    ' + txt + '

    '; footnote_area.html(txt); } else { footnote_area.remove(); @@ -591,7 +589,47 @@ frappe.utils = { catch (err) { return false; } - }() + }(), + throttle: function (func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + + let later = function () { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + return function () { + var now = Date.now(); + if (!previous && options.leading === false) previous = now; + let remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + get_form_link: function(doctype, name, html = false) { + const route = ['#Form', doctype, name].join('/'); + if (html) { + return `${name}`; + } + return route; + } }; // String.prototype.includes polyfill diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index f902216764..c6267b206a 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -27,6 +27,8 @@ $.extend(frappe.model, { {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, ], + numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], + std_fields_table: [ {fieldname:'parent', fieldtype:'Data', label:__('Parent')}, ], @@ -39,8 +41,9 @@ $.extend(frappe.model, { // setup refresh if the document is updated somewhere else frappe.realtime.on("doc_update", function(data) { // set list dirty - frappe.views.set_list_as_dirty(data.doctype); + frappe.views.ListView.trigger_list_update(data); var doc = locals[data.doctype] && locals[data.doctype][data.name]; + if(doc) { // current document is dirty, show message if its not me if(frappe.get_route()[0]==="Form" && cur_frm.doc.doctype===doc.doctype && cur_frm.doc.name===doc.name) { @@ -61,7 +64,7 @@ $.extend(frappe.model, { }); frappe.realtime.on("list_update", function(data) { - frappe.views.set_list_as_dirty(data.doctype); + frappe.views.ListView.trigger_list_update(data); }); }, @@ -74,6 +77,10 @@ $.extend(frappe.model, { return frappe.model.no_value_type.indexOf(fieldtype)===-1; }, + is_non_std_field: function(fieldname) { + return !frappe.model.std_fields_list.includes(fieldname); + }, + get_std_field: function(fieldname) { var docfield = $.map([].concat(frappe.model.std_fields).concat(frappe.model.std_fields_table), function(d) { @@ -576,6 +583,19 @@ $.extend(frappe.model, { } return all; }, + + get_full_column_name: function(fieldname, doctype) { + if (fieldname.includes('`tab')) return fieldname; + return '`tab' + doctype + '`.`' + fieldname + '`'; + }, + + is_numeric_field: function(fieldtype) { + if (!fieldtype) return; + if (typeof fieldtype === 'object') { + fieldtype = fieldtype.fieldtype; + } + return frappe.model.numeric_fieldtypes.includes(fieldtype); + } }); // legacy diff --git a/frappe/public/js/frappe/model/user_settings.js b/frappe/public/js/frappe/model/user_settings.js index 66b50375c3..52c160ea77 100644 --- a/frappe/public/js/frappe/model/user_settings.js +++ b/frappe/public/js/frappe/model/user_settings.js @@ -5,6 +5,7 @@ $.extend(frappe.model.user_settings, { var user_settings = frappe.model.user_settings[doctype] || {}; if ($.isPlainObject(value)) { + user_settings[key] = user_settings[key] || {}; $.extend(user_settings[key], value); } else { user_settings[key] = value; diff --git a/frappe/public/js/frappe/ui/base_list.js b/frappe/public/js/frappe/ui/base_list.js deleted file mode 100644 index 0d3b1957e8..0000000000 --- a/frappe/public/js/frappe/ui/base_list.js +++ /dev/null @@ -1,532 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -// new re-re-factored Listing object -// now called BaseList -// -// opts: -// parent - -// method (method to call on server) -// args (additional args to method) -// get_args (method to return args as dict) - -// show_filters [false] -// doctype -// filter_fields (if given, this list is rendered, else built from doctype) - -// query or get_query (will be deprecated) -// query_max -// buttons_in_frame - -// no_result_message ("No result") - -// page_length (20) -// hide_refresh (False) -// no_toolbar -// new_doctype -// [function] render_row(parent, data) -// [function] onrun -// no_loading (no ajax indicator) - -frappe.provide('frappe.ui'); - -frappe.ui.BaseList = Class.extend({ - init: function (opts) { - this.opts = opts || {}; - this.set_defaults(); - if (opts) { - this.make(); - } - }, - set_defaults: function () { - this.page_length = 20; - this.start = 0; - this.data = []; - }, - make: function (opts) { - if (opts) { - this.opts = opts; - } - this.prepare_opts(); - - $.extend(this, this.opts); - - // make dom - this.wrapper = $(frappe.render_template('listing', this.opts)); - this.parent.append(this.wrapper); - - this.set_events(); - - if (this.page) { - this.wrapper.find('.list-toolbar-wrapper').hide(); - } - - if (this.show_filters) { - this.make_filters(); - } - }, - prepare_opts: function () { - if (this.opts.new_doctype) { - if (!frappe.boot.user.can_create.includes(this.opts.new_doctype)) { - this.opts.new_doctype = null; - } - } - if (!this.opts.no_result_message) { - this.opts.no_result_message = __('Nothing to show'); - } - if (!this.opts.page_length) { - this.opts.page_length = this.user_settings && this.user_settings.limit || 20; - } - this.opts._more = __('More'); - }, - add_button: function (label, click, icon) { - if (this.page) { - return this.page.add_menu_item(label, click, icon) - } else { - this.wrapper.find('.list-toolbar-wrapper').removeClass('hide'); - return $('') - .appendTo(this.wrapper.find('.list-toolbar')) - .html((icon ? (' ') : '') + label) - .click(click); - } - }, - set_events: function () { - var me = this; - - // next page - this.wrapper.find('.btn-more').click(function () { - me.run(true); - }); - - this.wrapper.find(".btn-group-paging").on('click', '.btn', function () { - me.page_length = cint($(this).attr("data-value")); - - me.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info"); - $(this).addClass("btn-info"); - - // always reset when changing list page length - me.run(); - }); - - // select the correct page length - if (this.opts.page_length !== 20) { - this.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info"); - this.wrapper - .find(".btn-group-paging .btn[data-value='" + this.opts.page_length + "']") - .addClass('btn-info'); - } - - // title - if (this.title) { - this.wrapper.find('h3').html(this.title).show(); - } - - // new - this.set_primary_action(); - - if (me.no_toolbar || me.hide_toolbar) { - me.wrapper.find('.list-toolbar-wrapper').hide(); - } - }, - - set_primary_action: function () { - var me = this; - if (this.new_doctype) { - this.page.set_primary_action( - __("New"), - me.make_new_doc.bind(me, me.new_doctype), - "octicon octicon-plus" - ); - } else { - this.page.clear_primary_action(); - } - }, - - make_new_doc: function (doctype) { - var me = this; - frappe.model.with_doctype(doctype, function () { - if (me.custom_new_doc) { - me.custom_new_doc(doctype); - } else { - if (me.filter_list) { - frappe.route_options = {}; - me.filter_list.get_filters().forEach(function (f, i) { - if (f[2] === "=" && !frappe.model.std_fields_list.includes(f[1])) { - frappe.route_options[f[1]] = f[3]; - } - }); - } - frappe.new_doc(doctype, true); - } - }); - }, - - make_filters: function () { - this.make_standard_filters(); - - this.filter_list = new frappe.ui.FilterList({ - base_list: this, - parent: this.wrapper.find('.list-filters').show(), - doctype: this.doctype, - filter_fields: this.filter_fields, - default_filters: this.default_filters || [] - }); - // default filter for submittable doctype - if (frappe.model.is_submittable(this.doctype)) { - this.filter_list.add_filter(this.doctype, "docstatus", "!=", 2); - } - }, - - make_standard_filters: function() { - var me = this; - if (this.standard_filters_added) { - return; - } - - if (this.meta) { - var filter_count = 1; - if(this.is_list_view) { - $(``) - .prependTo(this.page.page_form); - } - this.page.add_field({ - fieldtype: 'Data', - label: 'ID', - condition: 'like', - fieldname: 'name', - onchange: () => { me.refresh(true); } - }); - - this.meta.fields.forEach(function(df, i) { - if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) { - let options = df.options; - let condition = '='; - let fieldtype = df.fieldtype; - if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) { - fieldtype = 'Data'; - condition = 'like'; - } - if(df.fieldtype == "Select" && df.options) { - options = df.options.split("\n"); - if(options.length > 0 && options[0] != "") { - options.unshift(""); - options = options.join("\n"); - } - } - let f = me.page.add_field({ - fieldtype: fieldtype, - label: __(df.label), - options: options, - fieldname: df.fieldname, - condition: condition, - onchange: () => {me.refresh(true);} - }); - filter_count ++; - if (filter_count > 3) { - $(f.wrapper).addClass('hidden-sm').addClass('hidden-xs'); - } - if (filter_count > 5) { - return false; - } - } - }); - } - - this.standard_filters_added = true; - }, - - update_standard_filters: function(filters) { - let me = this; - for(let key in this.page.fields_dict) { - let field = this.page.fields_dict[key]; - let value = field.get_value(); - if (value) { - if (field.df.condition==='like' && !value.includes('%')) { - value = '%' + value + '%'; - } - filters.push([ - me.doctype, - field.df.fieldname, - field.df.condition || '=', - value - ]); - } - } - }, - - - clear: function () { - this.data = []; - this.wrapper.find('.result-list').empty(); - this.wrapper.find('.result').show(); - this.wrapper.find('.no-result').hide(); - this.start = 0; - this.onreset && this.onreset(); - }, - - set_filters_from_route_options: function ({clear_filters=true} = {}) { - var me = this; - if(this.filter_list && clear_filters) { - this.filter_list.clear_filters(); - } - - for(var field in frappe.route_options) { - var value = frappe.route_options[field]; - var doctype = null; - - // if `Child DocType.fieldname` - if (field.includes(".")) { - doctype = field.split(".")[0]; - field = field.split(".")[1]; - } - - // find the table in which the key exists - // for example the filter could be {"item_code": "X"} - // where item_code is in the child table. - - // we can search all tables for mapping the doctype - if (!doctype) { - doctype = frappe.meta.get_doctype_for_field(me.doctype, field); - } - - if (doctype && me.filter_list) { - if ($.isArray(value)) { - me.filter_list.add_filter(doctype, field, value[0], value[1]); - } else { - me.filter_list.add_filter(doctype, field, "=", value); - } - } - } - frappe.route_options = null; - }, - - run: function(more) { - setTimeout(() => this._run(more), 100); - }, - - _run: function (more) { - var me = this; - if (!more) { - this.start = 0; - this.onreset && this.onreset(); - } - - var args = this.get_call_args(); - this.save_user_settings_locally(args); - - // user_settings are saved by db_query.py when dirty - $.extend(args, { - user_settings: frappe.model.user_settings[this.doctype] - }); - - return frappe.call({ - method: this.opts.method || 'frappe.desk.query_builder.runquery', - type: "GET", - freeze: this.opts.freeze !== undefined ? this.opts.freeze : true, - args: args, - callback: function (r) { - me.dirty = false; - me.render_results(r); - }, - no_spinner: this.opts.no_loading - }); - }, - save_user_settings_locally: function (args) { - if (this.opts.save_user_settings && this.doctype && !this.docname) { - // save list settings locally - var user_settings = frappe.model.user_settings[this.doctype]; - var different = false; - - if (!user_settings) { - return; - } - - if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) { - // settings are dirty if filters change - user_settings.filters = args.filters; - different = true; - } - - if (user_settings.order_by !== args.order_by) { - user_settings.order_by = args.order_by; - different = true; - } - - if (user_settings.limit !== args.limit_page_length) { - user_settings.limit = args.limit_page_length || 20 - different = true; - } - - // save fields in list settings - if (args.save_user_settings_fields) { - user_settings.fields = args.fields; - } - - if (different) { - user_settings.updated_on = moment().toString(); - } - } - }, - get_call_args: function () { - // load query - if (!this.method) { - var query = this.get_query && this.get_query() || this.query; - query = this.add_limits(query); - var args = { - query_max: this.query_max, - as_dict: 1 - } - args.simple_query = query; - } else { - var args = { - start: this.start, - page_length: this.page_length - } - } - - // append user-defined arguments - if (this.args) - $.extend(args, this.args) - - if (this.get_args) { - $.extend(args, this.get_args()); - } - return args; - }, - render_results: function (r) { - if (this.start === 0) - this.clear(); - - this.wrapper.find('.btn-more, .list-loading').hide(); - - var values = []; - - if (r.message) { - values = this.get_values_from_response(r.message); - } - - var show_results = true; - if(this.show_no_result) { - if($.isFunction(this.show_no_result)) { - show_results = !this.show_no_result() - } else { - show_results = !this.show_no_result; - } - } - - // render result view when - // length > 0 OR - // explicitly set by flag - if (values.length || show_results) { - this.data = this.data.concat(values); - this.render_view(values); - this.update_paging(values); - } else if (this.start === 0) { - // show no result message - this.wrapper.find('.result').hide(); - - var msg = ''; - var no_result_message = this.no_result_message; - if(no_result_message && $.isFunction(no_result_message)) { - msg = no_result_message(); - } else if(typeof no_result_message === 'string') { - msg = no_result_message; - } else { - msg = __('No Results') - } - - this.wrapper.find('.no-result').html(msg).show(); - } - - this.wrapper.find('.list-paging-area') - .toggle(values.length > 0|| this.start > 0); - - // callbacks - if (this.onrun) this.onrun(); - if (this.callback) this.callback(r); - this.wrapper.trigger("render-complete"); - }, - - get_values_from_response: function (data) { - // make dictionaries from keys and values - if (data.keys && $.isArray(data.keys)) { - return frappe.utils.dict(data.keys, data.values); - } else { - return data; - } - }, - - render_view: function (values) { - // override this method in derived class - }, - - update_paging: function (values) { - if (values.length >= this.page_length) { - this.wrapper.find('.btn-more').show(); - this.start += this.page_length; - } - }, - - refresh: function () { - this.run(); - }, - add_limits: function (query) { - return query + ' LIMIT ' + this.start + ',' + (this.page_length + 1); - }, - set_filter: function (fieldname, label, no_run, no_duplicate) { - var filter = this.filter_list.get_filter(fieldname); - if (filter) { - var value = cstr(filter.field.get_value()); - if (value.includes(label)) { - // already set - return false - - } else if (no_duplicate) { - filter.set_values(this.doctype, fieldname, "=", label); - } else { - // second filter set for this field - if (fieldname == '_user_tags' || fieldname == "_liked_by") { - // and for tags - this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%'); - } else { - // or for rest using "in" - filter.set_values(this.doctype, fieldname, 'in', value + ', ' + label); - } - } - } else { - // no filter for this item, - // setup one - if (['_user_tags', '_comments', '_assign', '_liked_by'].includes(fieldname)) { - this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%'); - } else { - this.filter_list.add_filter(this.doctype, fieldname, '=', label); - } - } - if (!no_run) - this.run(); - }, - init_user_settings: function () { - this.user_settings = frappe.model.user_settings[this.doctype] || {}; - }, - call_for_selected_items: function (method, args) { - var me = this; - args.names = this.get_checked_items().map(function (item) { - return item.name; - }); - - frappe.call({ - method: method, - args: args, - freeze: true, - callback: function (r) { - if (!r.exc) { - if (me.list_header) { - me.list_header.find(".list-select-all").prop("checked", false); - } - me.refresh(true); - } - } - }); - } -}); diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html index 49944a459e..984bd00aab 100644 --- a/frappe/public/js/frappe/ui/filters/edit_filter.html +++ b/frappe/public/js/frappe/ui/filters/edit_filter.html @@ -1,6 +1,6 @@
    -
    +
    -
    +
    diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js new file mode 100644 index 0000000000..04f0fcff57 --- /dev/null +++ b/frappe/public/js/frappe/ui/filters/field_select.js @@ -0,0 +1,155 @@ +// ') + .appendTo(this.parent) + .on("click", function () { $(this).select(); }); + this.select_input = this.$input.get(0); + this.awesomplete = new Awesomplete(this.select_input, { + minChars: 0, + maxItems: 99, + autoFirst: true, + list: me.options, + item(item) { + return $(repl('
  • %(label)s

  • ', item)) + .data("item.autocomplete", item) + .get(0); + } + }); + this.$input.on("awesomplete-select", function(e) { + var o = e.originalEvent; + var value = o.text.value; + var item = me.awesomplete.get_item(value); + me.selected_doctype = item.doctype; + me.selected_fieldname = item.fieldname; + if(me.select) me.select(item.doctype, item.fieldname); + }); + this.$input.on("awesomplete-selectcomplete", function(e) { + var o = e.originalEvent; + var value = o.text.value; + var item = me.awesomplete.get_item(value); + me.$input.val(item.label); + }); + + if(this.filter_fields) { + for(var i in this.filter_fields) + this.add_field_option(this.filter_fields[i]); + } else { + this.build_options(); + } + this.set_value(this.doctype, "name"); + }, + get_value() { + return this.selected_doctype ? this.selected_doctype + "." + this.selected_fieldname : null; + }, + val(value) { + if(value===undefined) { + return this.get_value(); + } else { + this.set_value(value); + } + }, + clear() { + this.selected_doctype = null; + this.selected_fieldname = null; + this.$input.val(""); + }, + set_value(doctype, fieldname) { + var me = this; + this.clear(); + if(!doctype) return; + + // old style + if(doctype.indexOf(".")!==-1) { + var parts = doctype.split("."); + doctype = parts[0]; + fieldname = parts[1]; + } + + $.each(this.options, function(i, v) { + if(v.doctype===doctype && v.fieldname===fieldname) { + me.selected_doctype = doctype; + me.selected_fieldname = fieldname; + me.$input.val(v.label); + return false; + } + }); + }, + build_options() { + var me = this; + me.table_fields = []; + var std_filters = $.map(frappe.model.std_fields, function(d) { + var opts = {parent: me.doctype}; + if(d.fieldname=="name") opts.options = me.doctype; + return $.extend(copy_dict(d), opts); + }); + + // add parenttype column + var doctype_obj = locals['DocType'][me.doctype]; + if(doctype_obj && cint(doctype_obj.istable)) { + std_filters = std_filters.concat([{ + fieldname: 'parent', + fieldtype: 'Data', + label: 'Parent', + parent: me.doctype, + }]); + } + + // blank + if(this.with_blank) { + this.options.push({ + label:"", + value:"", + }); + } + + // main table + var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); + $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { + // show fields where user has read access and if report hide flag is not set + if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) + me.add_field_option(df); + }); + + // child tables + $.each(me.table_fields, function(i, table_df) { + if(table_df.options) { + var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]); + $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { + // show fields where user has read access and if report hide flag is not set + if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) + me.add_field_option(df); + }); + } + }); + }, + + add_field_option(df) { + var me = this; + var label, table; + if(me.doctype && df.parent==me.doctype) { + label = __(df.label); + table = me.doctype; + if(df.fieldtype=='Table') me.table_fields.push(df); + } else { + label = __(df.label) + ' (' + __(df.parent) + ')'; + table = df.parent; + } + if(frappe.model.no_value_type.indexOf(df.fieldtype) == -1 && + !(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])) { + this.options.push({ + label: label, + value: table + "." + df.fieldname, + fieldname: df.fieldname, + doctype: df.parent + }); + if(!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {}; + me.fields_by_name[df.parent][df.fieldname] = df; + } + }, +}); diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js new file mode 100644 index 0000000000..e981692461 --- /dev/null +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -0,0 +1,335 @@ +frappe.ui.Filter = class { + constructor(opts) { + $.extend(this, opts); + + this.utils = frappe.ui.filter_utils; + this.make(); + this.make_select(); + this.set_events(); + this.setup(); + } + + make() { + this.filter_edit_area = $(frappe.render_template("edit_filter", {})) + .appendTo(this.parent.find('.filter-edit-area')); + } + + make_select() { + this.fieldselect = new frappe.ui.FieldSelect({ + parent: this.filter_edit_area.find('.fieldname-select-area'), + doctype: this.doctype, + filter_fields: this.filter_fields, + select: (doctype, fieldname) => { + this.set_field(doctype, fieldname); + } + }); + + if(this.fieldname) { + this.fieldselect.set_value(this.doctype, this.fieldname); + } + } + + set_events() { + this.filter_edit_area.find("a.remove-filter").on("click", () => { + this.remove(); + }); + + this.filter_edit_area.find(".set-filter-and-run").on("click", () => { + this.filter_edit_area.removeClass("new-filter"); + this.on_change(); + }); + + this.filter_edit_area.find('.condition').change(() => { + if(!this.field) return; + + let condition = this.get_condition(); + let fieldtype = null; + + if(["in", "like", "not in", "not like"].includes(condition)) { + fieldtype = 'Data'; + this.add_condition_help(condition); + } + this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition); + }); + } + + setup() { + const fieldname = this.fieldname || 'name'; + // set the field + return this.set_values(this.doctype, fieldname, this.condition, this.value); + } + + setup_state(is_new) { + let promise = Promise.resolve(); + if (is_new) { + this.filter_edit_area.addClass("new-filter"); + } else { + promise = this.update_filter_tag(); + } + + if(this.hidden) { + promise.then(() => this.$filter_tag.hide()); + } + } + + freeze() { + this.update_filter_tag(); + } + + update_filter_tag() { + return this._filter_value_set.then(() => { + !this.$filter_tag ? this.make_tag() : this.set_filter_button_text(); + this.filter_edit_area.hide(); + }); + } + + remove() { + this.filter_edit_area.remove(); + this.$filter_tag && this.$filter_tag.remove(); + this.field = null; + this.on_change(true); + } + + set_values(doctype, fieldname, condition, value) { + // presents given (could be via tags!) + this.set_field(doctype, fieldname); + + if(this.field.df.original_type==='Check') { + value = (value==1) ? 'Yes' : 'No'; + } + if(condition) this.set_condition(condition, true); + + // set value can be asynchronous, so update_filter_tag should happen after field is set + this._filter_value_set = Promise.resolve(); + if(value) { + this._filter_value_set = this.field.set_value(value); + } + return this._filter_value_set; + } + + set_field(doctype, fieldname, fieldtype, condition) { + // set in fieldname (again) + let cur = {}; + if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k]; + + let original_docfield = this.fieldselect.fields_by_name[doctype][fieldname]; + if(!original_docfield) { + frappe.msgprint(__("Field {0} is not selectable.", [fieldname])); + return; + } + + let df = copy_dict(original_docfield); + + // filter field shouldn't be read only or hidden + df.read_only = 0; df.hidden = 0; + + let c = condition ? condition : this.utils.get_default_condition(df); + this.set_condition(c); + + this.utils.set_fieldtype(df, fieldtype, this.get_condition()); + + // called when condition is changed, + // don't change if all is well + if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && + df.parent == cur.parent) { + return; + } + + // clear field area and make field + this.fieldselect.selected_doctype = doctype; + this.fieldselect.selected_fieldname = fieldname; + + this.make_field(df, cur.fieldtype); + } + + make_field(df, old_fieldtype) { + let old_text = this.field ? this.field.get_value() : null; + + let field_area = this.filter_edit_area.find('.filter-field').empty().get(0); + let f = frappe.ui.form.make_control({ + df: df, + parent: field_area, + only_input: true, + }); + f.refresh(); + + this.field = f; + if(old_text && f.fieldtype===old_fieldtype) { + this.field.set_value(old_text); + } + + // run on enter + $(this.field.wrapper).find(':input').keydown(e => { + if(e.which==13) { + this.on_change(); + } + }); + } + + get_value() { + return [ + this.fieldselect.selected_doctype, + this.field.df.fieldname, + this.get_condition(), + this.get_selected_value(), + this.hidden + ]; + } + + get_selected_value() { + return this.utils.get_selected_value(this.field, this.get_condition()); + } + + get_condition() { + return this.filter_edit_area.find('.condition').val(); + } + + set_condition(condition, trigger_change=false) { + let $condition_field = this.filter_edit_area.find('.condition'); + $condition_field.val(condition); + if(trigger_change) $condition_field.change(); + } + + make_tag() { + this.$filter_tag = this.get_filter_tag_element() + .insertAfter(this.parent.find(".active-tag-filters .add-filter")); + this.set_filter_button_text(); + this.bind_tag(); + } + + bind_tag() { + this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this)); + + let filter_button = this.$filter_tag.find(".toggle-filter"); + filter_button.on("click", () => { + filter_button.closest('.tag-filters-area').find('.filter-edit-area').show(); + this.filter_edit_area.toggle(); + }); + } + + set_filter_button_text() { + this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text()); + } + + get_filter_button_text() { + let value = this.utils.get_formatted_value(this.field, this.get_selected_value()); + return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`; + } + + get_filter_tag_element() { + return $(`
    + + +
    `); + } + + add_condition_help(condition) { + let $desc = this.field.desc_area; + if(!$desc) { + $desc = $('
    ').appendTo(this.field.wrapper); + } + // set description + $desc.html((in_list(["in", "not in"], condition)==="in" + ? __("values separated by commas") + : __("use % as wildcard"))+'
    '); + } +}; + +frappe.ui.filter_utils = { + get_formatted_value(field, value) { + if(field.df.fieldname==="docstatus") { + value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; + } else if(field.df.original_type==="Check") { + value = {0:"No", 1:"Yes"}[cint(value)]; + } + return frappe.format(value, field.df, {only_value: 1}); + }, + + get_selected_value(field, condition) { + let val = field.get_value(); + + if(typeof val==='string') { + val = strip(val); + } + + if(field.df.original_type == 'Check') { + val = (val=='Yes' ? 1 :0); + } + + if(condition.indexOf('like', 'not like')!==-1) { + // automatically append wildcards + if(val) { + if(val.slice(0,1) !== "%") { + val = "%" + val; + } + if(val.slice(-1) !== "%") { + val = val + "%"; + } + } + } else if(in_list(["in", "not in"], condition)) { + if(val) { + val = $.map(val.split(","), function(v) { return strip(v); }); + } + } if(val === '%') { + val = ""; + } + + return val; + }, + + get_default_condition(df) { + if (df.fieldtype == 'Data') { + return 'like'; + } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ + return 'Between'; + } else { + return '='; + } + }, + + set_fieldtype(df, fieldtype, condition) { + // reset + if(df.original_type) + df.fieldtype = df.original_type; + else + df.original_type = df.fieldtype; + + df.description = ''; df.reqd = 0; + df.ignore_link_validation = true; + + // given + if(fieldtype) { + df.fieldtype = fieldtype; + return; + } + + // scrub + if(df.fieldname=="docstatus") { + df.fieldtype="Select", + df.options=[ + {value:0, label:__("Draft")}, + {value:1, label:__("Submitted")}, + {value:2, label:__("Cancelled")} + ]; + } else if(df.fieldtype=='Check') { + df.fieldtype='Select'; + df.options='No\nYes'; + } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', + 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { + df.fieldtype = 'Data'; + } else if(df.fieldtype=='Link' && ['=', '!='].indexOf(condition)==-1) { + df.fieldtype = 'Data'; + } + if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { + df.options = null; + } + if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ + df.fieldtype = 'DateRange'; + } + } +}; diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js new file mode 100644 index 0000000000..164278b07d --- /dev/null +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -0,0 +1,141 @@ +frappe.ui.FilterGroup = class { + constructor(opts) { + $.extend(this, opts); + this.wrapper = this.parent; + this.filters = []; + this.make(); + } + + make() { + this.wrapper.append(this.get_container_template()); + this.set_events(); + } + + set_events() { + this.wrapper.find('.add-filter').on('click', () => { + this.add_filter(this.doctype, 'name'); + }); + } + + add_filters(filters) { + let promises = []; + + for (const filter of filters) { + promises.push(this.add_filter(...filter)); + } + + return Promise.all(promises); + } + + add_filter(doctype, fieldname, condition, value, hidden) { + if (!fieldname) return Promise.resolve(); + // adds a new filter, returns true if filter has been added + + // {}: Add in page filter by fieldname if exists ('=' => 'like') + + if(!this.validate_args(doctype, fieldname)) return false; + + const is_new_filter = arguments.length < 2; + if (is_new_filter && this.wrapper.find(".new-filter:visible").length) { + // only allow 1 new filter at a time! + return Promise.resolve(); + } else { + let args = [doctype, fieldname, condition, value, hidden]; + const promise = this.push_new_filter(args, is_new_filter); + return (promise && promise.then) ? promise : Promise.resolve(); + } + } + + validate_args(doctype, fieldname) { + if(doctype && fieldname + && !frappe.meta.has_field(doctype, fieldname) + && !frappe.model.std_fields_list.includes(fieldname)) { + + frappe.throw(__(`Invalid filter: "${[fieldname.bold()]}"`)); + return false; + } + return true; + } + + push_new_filter(args, is_new_filter=false) { + // args: [doctype, fieldname, condition, value] + if(this.filter_exists(args)) return; + + // {}: Clear page filter fieldname field + + let filter = this._push_new_filter(...args); + + if (filter && filter.value) { + filter.setup_state(is_new_filter); + return filter._filter_value_set; // internal promise + } + } + + _push_new_filter(doctype, fieldname, condition, value, hidden = false) { + let args = { + parent: this.wrapper, + doctype: doctype, + fieldname: fieldname, + condition: condition, + value: value, + hidden: hidden, + on_change: (update) => { + if(update) this.update_filters(); + this.on_change(); + } + }; + let filter = new frappe.ui.Filter(args); + this.filters.push(filter); + return filter; + } + + filter_exists(filter_value) { + // filter_value of form: [doctype, fieldname, condition, value] + let exists = false; + this.filters.filter(f => f.field).map(f => { + let f_value = f.get_value(); + let value = filter_value[3]; + let equal = frappe.utils.arrays_equal; + + if(equal(f_value, filter_value) || (Array.isArray(value) && equal(value, f_value[3]))) { + exists = true; + } + }); + return exists; + } + + get_filters() { + return this.filters.filter(f => f.field).map(f => { + f.freeze(); + return f.get_value(); + }); + // {}: this.list.update_standard_filters(values); + } + + update_filters() { + this.filters = this.filters.filter(f => f.field); // remove hidden filters + } + + clear_filters() { + this.filters.map(f => { f.remove(true); }); + // {}: Clear page filters, .date-range-picker (called list run()) + this.filters = []; + } + + get_filter(fieldname) { + return this.filters.filter(f => { + return (f.field && f.field.df.fieldname==fieldname); + })[0]; + } + + get_container_template() { + return $(`
    +
    + +
    +
    +
    `); + } +}; diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js deleted file mode 100644 index 2ff8b51d5a..0000000000 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ /dev/null @@ -1,679 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.FilterList = Class.extend({ - init: function(opts) { - $.extend(this, opts); - this.filters = []; - this.wrapper = this.parent; - this.stats = []; - this.make(); - this.set_events(); - }, - make: function() { - this.wrapper.find('.show_filters, .filter_area').remove(); - this.wrapper.append(` -
    -
    - -
    -
    -
    `); - }, - set_events: function() { - var me = this; - // show filters - this.wrapper.find('.new-filter').bind('click', function() { - me.add_filter(); - }); - - this.wrapper.find('.clear-filters').bind('click', function() { - me.clear_filters(); - $('.date-range-picker').val('') - me.base_list.run(); - $(this).addClass("hide"); - }); - }, - - show_filters: function() { - this.wrapper.find('.show_filters').toggle(); - if(!this.filters.length) { - this.add_filter(this.doctype, 'name'); - this.filters[0].wrapper.find(".filter_field input").focus(); - } - }, - - clear_filters: function() { - $.each(this.filters, function(i, f) { f.remove(true); }); - if(this.base_list.page.fields_dict) { - $.each(this.base_list.page.fields_dict, (key, value) => { - value.set_input(''); - }); - } - this.filters = []; - }, - - add_filter: function(doctype, fieldname, condition, value, hidden) { - // adds a new filter, returns true if filter has been added - - // allow equal to be used as like - let base_filter = this.base_list.page.fields_dict[fieldname]; - if (base_filter - && (base_filter.df.condition==condition - || (condition==='=' && base_filter.df.condition==='like'))) { - // if filter exists in base_list, then exit - this.base_list.page.fields_dict[fieldname].set_input(value); - - return true; - } - - if(doctype && fieldname - && !frappe.meta.has_field(doctype, fieldname) - && !in_list(frappe.model.std_fields_list, fieldname)) { - frappe.msgprint({ - message: __('Filter {0} missing', [fieldname.bold()]), - title: 'Invalid Filter', - indicator: 'red' - }); - return false; - } - - this.wrapper.find('.show_filters').toggle(true); - var is_new_filter = arguments.length===0; - - if (is_new_filter && this.wrapper.find(".is-new-filter:visible").length) { - // only allow 1 new filter at a time! - return false; - } - - var filter = this.push_new_filter(doctype, fieldname, condition, value); - if (!filter) return; - - if(this.wrapper.find('.clear-filters').hasClass("hide")) { - this.wrapper.find('.clear-filters').removeClass("hide"); - } - - if (filter && is_new_filter) { - filter.wrapper.addClass("is-new-filter"); - } else { - filter.freeze(); - } - - if (hidden) { - filter.$btn_group.addClass("hide"); - } - - return true; - }, - push_new_filter: function(doctype, fieldname, condition, value) { - if(this.filter_exists(doctype, fieldname, condition, value)) { - return; - } - - // if standard filter exists, then clear it. - if(this.base_list.page.fields_dict[fieldname]) { - this.base_list.page.fields_dict[fieldname].set_input(''); - } - - var filter = new frappe.ui.Filter({ - flist: this, - _doctype: doctype, - fieldname: fieldname, - condition: condition, - value: value - }); - - this.filters.push(filter); - - return filter; - }, - - remove: function(filter) { - // remove `filter` from flist - for (var i in this.filters) { - if (this.filters[i] === filter) { - break; - } - } - if (i!==undefined) { - // remove index - this.filters.splice(i, 1); - } - }, - - filter_exists: function(doctype, fieldname, condition, value) { - var flag = false; - for(var i in this.filters) { - if(this.filters[i].field) { - var f = this.filters[i].get_value(); - - if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) { - flag = true; - } else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) { - flag = true; - } - } - } - return flag; - }, - - get_filters: function() { - // get filter values as dict - var values = []; - $.each(this.filters, function(i, filter) { - if(filter.field) { - filter.freeze(); - values.push(filter.get_value()); - } - }); - this.base_list.update_standard_filters(values); - - return values; - }, - - // remove hidden filters - update_filters: function() { - var fl = []; - $.each(this.filters, function(i, f) { - if(f.field) fl.push(f); - }) - this.filters = fl; - if(this.filters.length === 0) { - this.wrapper.find('.clear-filters').addClass("hide"); - } - }, - - get_filter: function(fieldname) { - for(var i in this.filters) { - if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname) - return this.filters[i]; - } - }, - - get_formatted_value: function(field, val){ - var value = val; - - if(field.df.fieldname==="docstatus") { - value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; - } else if(field.df.original_type==="Check") { - value = {0:"No", 1:"Yes"}[cint(value)]; - } - - value = frappe.format(value, field.df, {only_value: 1}); - return value; - } -}); - -frappe.ui.Filter = Class.extend({ - init: function(opts) { - $.extend(this, opts); - - this.doctype = this.flist.doctype; - this.make(); - this.make_select(); - this.set_events(); - }, - make: function() { - this.wrapper = $(frappe.render_template("edit_filter", {})) - .appendTo(this.flist.wrapper.find('.filter_area')); - }, - make_select: function() { - var me = this; - this.fieldselect = new frappe.ui.FieldSelect({ - parent: this.wrapper.find('.fieldname_select_area'), - doctype: this.doctype, - filter_fields: this.filter_fields, - select: function(doctype, fieldname) { - me.set_field(doctype, fieldname); - } - }); - if(this.fieldname) { - this.fieldselect.set_value(this._doctype || this.doctype, this.fieldname); - } - }, - set_events: function() { - var me = this; - - this.wrapper.find("a.remove-filter").on("click", function() { - me.remove(); - }); - - this.wrapper.find(".set-filter-and-run").on("click", function() { - me.wrapper.removeClass("is-new-filter"); - me.flist.base_list.run(); - me.apply(); - }); - - // add help for "in" codition - me.wrapper.find('.condition').change(function() { - if(!me.field) return; - var condition = $(this).val(); - if(in_list(["in", "like", "not in", "not like"], condition)) { - me.set_field(me.field.df.parent, me.field.df.fieldname, 'Data', condition); - if(!me.field.desc_area) { - me.field.desc_area = $('
    ').appendTo(me.field.wrapper); - } - // set description - me.field.desc_area.html((in_list(["in", "not in"], condition)==="in" - ? __("values separated by commas") - : __("use % as wildcard"))+'
    '); - } else { - //if condition selected after refresh - me.set_field(me.field.df.parent, me.field.df.fieldname, null, condition); - } - }); - - // set the field - if(me.fieldname) { - // pre-sets given (could be via tags!) - return this.set_values(me._doctype, me.fieldname, me.condition, me.value); - } else { - me.set_field(me.doctype, 'name'); - } - }, - - apply: function() { - var f = this.get_value(); - - this.flist.remove(this); - this.flist.push_new_filter(f[0], f[1], f[2], f[3]); - this.wrapper.remove(); - this.flist.update_filters(); - }, - - remove: function(dont_run) { - this.wrapper.remove(); - this.$btn_group && this.$btn_group.remove(); - this.field = null; - this.flist.update_filters(); - - if(!dont_run) { - this.flist.base_list.refresh(true); - } - }, - - set_values: function(doctype, fieldname, condition, value) { - // presents given (could be via tags!) - this.set_field(doctype, fieldname); - - // change 0,1 to Yes, No for check field type - if(this.field.df.original_type==='Check') { - if(value==0) value = 'No'; - else if(value==1) value = 'Yes'; - } - - if(condition) { - this.wrapper.find('.condition').val(condition).change(); - } - if(value!=null) { - return this.field.set_value(value); - } - }, - - set_field: function(doctype, fieldname, fieldtype, condition) { - var me = this; - - // set in fieldname (again) - var cur = me.field ? { - fieldname: me.field.df.fieldname, - fieldtype: me.field.df.fieldtype, - parent: me.field.df.parent, - } : {}; - - var original_docfield = me.fieldselect.fields_by_name[doctype][fieldname]; - if(!original_docfield) { - frappe.msgprint(__("Field {0} is not selectable.", [fieldname])); - return; - } - - var df = copy_dict(me.fieldselect.fields_by_name[doctype][fieldname]); - - // filter field shouldn't be read only or hidden - df.read_only = 0; - df.hidden = 0; - - if(!condition) this.set_default_condition(df, fieldtype); - this.set_fieldtype(df, fieldtype); - - // called when condition is changed, - // don't change if all is well - if(me.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && - df.parent == cur.parent) { - return; - } - - // clear field area and make field - me.fieldselect.selected_doctype = doctype; - me.fieldselect.selected_fieldname = fieldname; - - // save old text - var old_text = null; - if(me.field) { - old_text = me.field.get_value(); - } - - var field_area = me.wrapper.find('.filter_field').empty().get(0); - var f = frappe.ui.form.make_control({ - df: df, - parent: field_area, - only_input: true, - }) - f.refresh(); - - me.field = f; - if(old_text && me.field.df.fieldtype===cur.fieldtype) { - me.field.set_value(old_text); - } - - // run on enter - $(me.field.wrapper).find(':input').keydown(function(ev) { - if(ev.which==13) { - me.flist.base_list.run(); - } - }) - }, - - set_fieldtype: function(df, fieldtype) { - // reset - if(df.original_type) - df.fieldtype = df.original_type; - else - df.original_type = df.fieldtype; - - df.description = ''; df.reqd = 0; - df.ignore_link_validation = true; - - // given - if(fieldtype) { - df.fieldtype = fieldtype; - return; - } - - // scrub - if(df.fieldname=="docstatus") { - df.fieldtype="Select", - df.options=[ - {value:0, label:__("Draft")}, - {value:1, label:__("Submitted")}, - {value:2, label:__("Cancelled")} - ] - } else if(df.fieldtype=='Check') { - df.fieldtype='Select'; - df.options='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', - 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { - df.fieldtype = 'Data'; - } else if(df.fieldtype=='Link' && ['=', '!='].indexOf(this.wrapper.find('.condition').val())==-1) { - df.fieldtype = 'Data'; - } - if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { - df.options = null; - } - if(this.wrapper.find('.condition').val()== "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ - df.fieldtype = 'DateRange'; - } - }, - - set_default_condition: function(df, fieldtype) { - if(!fieldtype) { - // set as "like" for data fields - if (df.fieldtype == 'Data') { - this.wrapper.find('.condition').val('like'); - } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ - this.wrapper.find('.condition').val('Between'); - }else{ - this.wrapper.find('.condition').val('='); - } - } - }, - - get_value: function() { - return [this.fieldselect.selected_doctype, - this.field.df.fieldname, this.get_condition(), this.get_selected_value()]; - }, - - get_selected_value: function() { - var val = this.field.get_value(); - - if(typeof val==='string') { - val = strip(val); - } - - if(this.field.df.original_type == 'Check') { - val = (val=='Yes' ? 1 :0); - } - - if(this.get_condition().indexOf('like', 'not like')!==-1) { - // automatically append wildcards - if(val) { - if(val.slice(0,1) !== "%") { - val = "%" + val; - } - if(val.slice(-1) !== "%") { - val = val + "%"; - } - } - } else if(in_list(["in", "not in"], this.get_condition())) { - if(val) { - val = $.map(val.split(","), function(v) { return strip(v); }); - } - } if(val === '%') { - val = ""; - } - - return val; - }, - - get_condition: function() { - return this.wrapper.find('.condition').val(); - }, - - freeze: function() { - if(this.$btn_group) { - // already made, just hide the condition setter - this.set_filter_button_text(); - this.wrapper.toggle(false); - return; - } - - var me = this; - - // add a button for new filter if missing - this.$btn_group = $(`
    - -
    `) - .insertAfter(this.flist.wrapper.find(".set-filters .new-filter")); - - this.set_filter_button_text(); - - this.$btn_group.find(".remove-filter").on("click", function() { - me.remove(); - }); - - this.$btn_group.find(".toggle-filter").on("click", function() { - $(this).closest('.show_filters').find('.filter_area').show() - me.wrapper.toggle(); - }) - this.wrapper.toggle(false); - }, - - set_filter_button_text: function() { - var value = this.get_selected_value(); - value = this.flist.get_formatted_value(this.field, value); - - // for translations - // __("like"), __("not like"), __("in") - - this.$btn_group.find(".toggle-filter") - .html(repl('%(label)s %(condition)s "%(value)s"', { - label: __(this.field.df.label), - condition: __(this.get_condition()), - value: __(value), - })); - } - -}); - -// ') - .appendTo(this.parent) - .on("click", function () { $(this).select(); }); - this.select_input = this.$select.get(0); - this.awesomplete = new Awesomplete(this.select_input, { - minChars: 0, - maxItems: 99, - autoFirst: true, - list: me.options, - item: function(item, input) { - return $(repl('
  • %(label)s

  • ', item)) - .data("item.autocomplete", item) - .get(0); - } - }); - this.$select.on("awesomplete-select", function(e) { - var o = e.originalEvent; - var value = o.text.value; - var item = me.awesomplete.get_item(value); - me.selected_doctype = item.doctype; - me.selected_fieldname = item.fieldname; - if(me.select) me.select(item.doctype, item.fieldname); - }); - this.$select.on("awesomplete-selectcomplete", function(e) { - var o = e.originalEvent; - var value = o.text.value; - var item = me.awesomplete.get_item(value); - me.$select.val(item.label); - }); - - if(this.filter_fields) { - for(var i in this.filter_fields) - this.add_field_option(this.filter_fields[i]) - } else { - this.build_options(); - } - this.set_value(this.doctype, "name"); - window.last_filter = this; - }, - get_value: function() { - return this.selected_doctype ? this.selected_doctype + "." + this.selected_fieldname : null; - }, - val: function(value) { - if(value===undefined) { - return this.get_value(); - } else { - this.set_value(value); - } - }, - clear: function() { - this.selected_doctype = null; - this.selected_fieldname = null; - this.$select.val(""); - }, - set_value: function(doctype, fieldname) { - var me = this; - this.clear(); - if(!doctype) return; - - // old style - if(doctype.indexOf(".")!==-1) { - var parts = doctype.split("."); - doctype = parts[0]; - fieldname = parts[1]; - } - - $.each(this.options, function(i, v) { - if(v.doctype===doctype && v.fieldname===fieldname) { - me.selected_doctype = doctype; - me.selected_fieldname = fieldname; - me.$select.val(v.label); - return false; - } - }); - }, - build_options: function() { - var me = this; - me.table_fields = []; - var std_filters = $.map(frappe.model.std_fields, function(d) { - var opts = {parent: me.doctype} - if(d.fieldname=="name") opts.options = me.doctype; - return $.extend(copy_dict(d), opts); - }); - - // add parenttype column - var doctype_obj = locals['DocType'][me.doctype]; - if(doctype_obj && cint(doctype_obj.istable)) { - std_filters = std_filters.concat([{ - fieldname: 'parent', - fieldtype: 'Data', - label: 'Parent', - parent: me.doctype, - }]); - } - - // blank - if(this.with_blank) { - this.options.push({ - label:"", - value:"", - }) - } - - // main table - var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); - $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { - // show fields where user has read access and if report hide flag is not set - if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) - me.add_field_option(df); - }); - - // child tables - $.each(me.table_fields, function(i, table_df) { - if(table_df.options) { - var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]); - $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { - // show fields where user has read access and if report hide flag is not set - if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) - me.add_field_option(df); - }); - } - }); - }, - - add_field_option: function(df) { - var me = this; - if(me.doctype && df.parent==me.doctype) { - var label = __(df.label); - var table = me.doctype; - if(df.fieldtype=='Table') me.table_fields.push(df); - } else { - var label = __(df.label) + ' (' + __(df.parent) + ')'; - var table = df.parent; - } - if(frappe.model.no_value_type.indexOf(df.fieldtype) == -1 && - !(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])) { - this.options.push({ - label: label, - value: table + "." + df.fieldname, - fieldname: df.fieldname, - doctype: df.parent - }); - if(!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {}; - me.fields_by_name[df.parent][df.fieldname] = df; - } - }, -}) diff --git a/frappe/public/js/frappe/ui/like.js b/frappe/public/js/frappe/ui/like.js index 0dbdac72ad..840d4c9fda 100644 --- a/frappe/public/js/frappe/ui/like.js +++ b/frappe/public/js/frappe/ui/like.js @@ -97,7 +97,10 @@ frappe.ui.setup_like_popover = function($parent, selector) { animation: true, placement: "right", content: function() { - var liked_by = JSON.parse($wrapper.attr('data-liked-by') || "[]"); + var liked_by = $wrapper.attr('data-liked-by'); + liked_by = liked_by ? decodeURI(liked_by) : '[]'; + liked_by = JSON.parse(liked_by); + var user = frappe.session.user; // hack if ($wrapper.find(".not-liked").length) { diff --git a/frappe/public/js/frappe/ui/sort_selector.js b/frappe/public/js/frappe/ui/sort_selector.js index 73076665c5..d28cf32065 100644 --- a/frappe/public/js/frappe/ui/sort_selector.js +++ b/frappe/public/js/frappe/ui/sort_selector.js @@ -184,5 +184,9 @@ frappe.ui.SortSelector = Class.extend({ return this.labels[fieldname] || frappe.meta.get_label(this.doctype, fieldname); } + }, + get_sql_string: function() { + // build string like `tabTask`.`subject` desc + return '`tab' + this.doctype + '`.`' + this.sort_by + '` ' + this.sort_order; } }) diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index c5ed49f29c..744e5db70a 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -377,7 +377,7 @@ frappe.upload = { frappe.throw(__("File size exceeded the maximum allowed size of {0} MB", [max_file_size / 1048576])); } }, - multifile_upload:function(fileobjs, args, opts) { + multifile_upload:function(fileobjs, args, opts={}) { //loop through filenames and checkboxes then append to list var fields = []; for (var i =0,j = fileobjs.length;i
    ${breadcrumbs.label}`; + $breadcrumbs.append(html); + $("body").removeClass("no-breadcrumbs"); + return; + } + // get preferred module for breadcrumbs, based on sent via module var from_module = frappe.breadcrumbs.get_doctype_module(breadcrumbs.doctype); diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 45c15990bf..1f75db72c2 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -4,82 +4,63 @@ frappe.provide("frappe.views.calendar"); frappe.provide("frappe.views.calendars"); -frappe.views.CalendarView = frappe.views.ListRenderer.extend({ - name: 'Calendar', - render_view: function() { - var me = this; - this.get_calendar_options() - .then(options => { - this.calendar = new frappe.views.Calendar(options); - }); - }, - load_last_view: function() { +frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { + static load_last_view() { const route = frappe.get_route(); - - if (!route[3]) { - // routed to Calendar view, check last calendar_view - let calendar_view = this.user_settings.last_calendar_view; - - if (calendar_view) { - frappe.set_route('List', this.doctype, 'Calendar', calendar_view); - return true; - } + if (route.length === 3) { + const doctype = route[1]; + const user_settings = frappe.get_user_settings(doctype)['Calendar'] || {}; + route.push(user_settings.last_calendar_view || 'Default'); + frappe.set_route(route); + return true; + } else { + return false; } + } - return false; - }, - set_defaults: function() { - this._super(); + setup_defaults() { + super.setup_defaults(); this.page_title = this.page_title + ' ' + __('Calendar'); - this.no_realtime = true; - this.show_no_result = false; - this.hide_sort_selector = true; - }, - get_header_html: function() { - return null; - }, - should_refresh: function() { - var should_refresh = this._super(); - if(!should_refresh) { - this.last_calendar_view = this.current_calendar_view || ''; - this.current_calendar_view = this.get_calendar_view(); - - if (this.current_calendar_view !== 'Default') { - this.page_title = __(this.current_calendar_view); - } else { - this.page_title = this.doctype + ' ' + __('Calendar'); - } + this.calendar_settings = frappe.views.calendar[this.doctype] || {}; + this.calendar_name = frappe.get_route()[3]; + } + + before_render() { + super.before_render(); + this.save_view_user_settings({ + last_calendar: this.calendar_name + }); + } - should_refresh = this.current_calendar_view !== this.last_calendar_view; + render() { + if (this.calendar) { + this.calendar.refresh(); + return; } - return should_refresh; - }, - get_calendar_view: function() { - return frappe.get_route()[3]; - }, - get_calendar_options: function() { - const calendar_view = frappe.get_route()[3] || 'Default'; - // save in user_settings - frappe.model.user_settings.save(this.doctype, 'Calendar', { - last_calendar_view: calendar_view - }); + this.load_lib + .then(() => this.get_calendar_options()) + .then(options => { + this.calendar = new frappe.views.Calendar(options); + }); + } + get_calendar_options() { const options = { doctype: this.doctype, - parent: this.wrapper, - page: this.list_view.page, - list_view: this.list_view - } + parent: this.$result, + page: this.page, + list_view: this + }; + const calendar_name = this.calendar_name; return new Promise(resolve => { - if (calendar_view === 'Default') { + if (calendar_name === 'Default') { Object.assign(options, frappe.views.calendar[this.doctype]); resolve(options); } else { - - frappe.model.with_doc('Calendar View', calendar_view, () => { - const doc = frappe.get_doc('Calendar View', calendar_view); + frappe.model.with_doc('Calendar View', calendar_name, () => { + const doc = frappe.get_doc('Calendar View', calendar_name); Object.assign(options, { field_map: { id: "name", @@ -88,18 +69,20 @@ frappe.views.CalendarView = frappe.views.ListRenderer.extend({ title: doc.subject_field } }); - resolve(options); }); } - }) - }, - required_libs: [ - 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css', - 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js', - 'assets/frappe/js/lib/fullcalendar/locale-all.js' - ] -}) + }); + } + + get required_libs() { + return [ + 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css', + 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js', + 'assets/frappe/js/lib/fullcalendar/locale-all.js' + ]; + } +}; frappe.views.Calendar = Class.extend({ init: function(options) { @@ -123,11 +106,10 @@ frappe.views.Calendar = Class.extend({ $(this.parent).on("show", function() { me.$cal.fullCalendar("refetchEvents"); - }) + }); }, make: function() { - var me = this; this.$wrapper = this.parent; this.$cal = $("
    ").appendTo(this.$wrapper); this.footnote_area = frappe.utils.set_footnote(this.footnote_area, this.$wrapper, @@ -146,9 +128,9 @@ frappe.views.Calendar = Class.extend({ this.$wrapper.find(".fc-button-group").addClass("btn-group"); this.$wrapper.find('.fc-prev-button span') - .attr('class', '').addClass('fa fa-chevron-left') + .attr('class', '').addClass('fa fa-chevron-left'); this.$wrapper.find('.fc-next-button span') - .attr('class', '').addClass('fa fa-chevron-right') + .attr('class', '').addClass('fa fa-chevron-right'); var btn_group = this.$wrapper.find(".fc-button-group"); btn_group.find(".fc-state-active").addClass("active"); @@ -197,22 +179,22 @@ frappe.views.Calendar = Class.extend({ events = me.prepare_events(events); callback(events); } - }) + }); }, eventRender: function(event, element) { element.attr('title', event.tooltip); }, - eventClick: function(event, jsEvent, view) { + eventClick: function(event) { // edit event description or delete var doctype = event.doctype || me.doctype; if(frappe.model.can_read(doctype)) { frappe.set_route("Form", doctype, event.name); } }, - eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) { + eventDrop: function(event, delta, revertFunc) { me.update_event(event, revertFunc); }, - eventResize: function(event, delta, revertFunc, jsEvent, ui, view) { + eventResize: function(event, delta, revertFunc) { me.update_event(event, revertFunc); }, select: function(startDate, endDate, jsEvent, view) { @@ -269,7 +251,7 @@ frappe.views.Calendar = Class.extend({ doctype: this.doctype, start: this.get_system_datetime(start), end: this.get_system_datetime(end), - filters: this.list_view.filter_list.get_filters(), + filters: this.list_view.filter_area.get(), field_map: this.field_map }; return args; @@ -384,4 +366,4 @@ frappe.views.Calendar = Class.extend({ event.end = event.end ? $.fullCalendar.moment(event.end).add(1, "day").stripTime() : null; } } -}) +}); diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 19fd967897..98c8286afe 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -418,8 +418,7 @@ frappe.views.CommunicationComposer = Class.extend({ if(!form_values) return; var selected_attachments = - $.map($(me.dialog.wrapper) - .find("[data-file-name]:checked"), function (element) { + $.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) { return $(element).attr("data-file-name"); }); diff --git a/frappe/public/js/frappe/views/factory.js b/frappe/public/js/frappe/views/factory.js index a0a5e1e07c..a160a4ff92 100644 --- a/frappe/public/js/frappe/views/factory.js +++ b/frappe/public/js/frappe/views/factory.js @@ -11,12 +11,9 @@ frappe.views.Factory = Class.extend({ show: function() { var page_name = frappe.get_route_str(), me = this; - if(page_name.substr(0, 4) === 'List') { - page_name = frappe.get_route().slice(0, 2).join('/'); - } if(frappe.pages[page_name] && !page_name.includes("Form/")) { - frappe.container.change_to(frappe.pages[page_name]); + frappe.container.change_to(page_name); if(me.on_show) { me.on_show(); } diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js new file mode 100644 index 0000000000..8f91dead98 --- /dev/null +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -0,0 +1,266 @@ +frappe.provide('frappe.views'); + +frappe.views.FileView = class FileView extends frappe.views.ListView { + static load_last_view() { + const route = frappe.get_route(); + if (route.length === 2) { + const view_user_settings = frappe.get_user_settings('File', 'File'); + frappe.set_route('List', 'File', view_user_settings.last_folder || frappe.boot.home_folder); + return true; + } + return false; + } + + setup_view() { + this.setup_events(); + } + + set_breadcrumbs() { + const route = frappe.get_route(); + route.splice(-1); + const last_folder = route[route.length - 1]; + if (last_folder === 'File') return; + + const last_folder_route = '#' + route.join('/'); + frappe.breadcrumbs.add({ + type: 'Custom', + label: last_folder, + route: last_folder_route + }); + } + + setup_defaults() { + super.setup_defaults(); + this.page_title = __('File Manager'); + + const route = frappe.get_route(); + this.current_folder = route.slice(2).join('/'); + this.filters = [['File', 'folder', '=', this.current_folder, true]]; + this.order_by = this.view_user_settings.order_by || 'file_name asc'; + + this.menu_items = this.menu_items.concat(this.file_menu_items()); + } + + file_menu_items() { + const items = [ + { + label: __('Cut'), + action: () => { + frappe.file_manager.cut(this.get_checked_items(), this.current_folder); + }, + class: 'cut-menu-button hide' + }, + { + label: __('Paste'), + action: () => { + frappe.file_manager.paste(this.current_folder); + }, + class: 'paste-menu-button hide' + }, + { + label: __('New Folder'), + action: () => { + frappe.prompt(__('Name'), (values) => { + if((values.value.indexOf("/") > -1)) { + frappe.throw(__("Folder name should not include '/' (slash)")); + } + const data = { + file_name: values.value, + folder: this.current_folder + }; + frappe.call({ + method: "frappe.core.doctype.file.file.create_new_folder", + args: data + }); + }, __('Enter folder name'), __('Create')); + } + }, + { + label: __('Import Zip'), + action: () => { + // make upload dialog + frappe.ui.get_upload_dialog({ + args: { + folder: this.current_folder, + from_form: 1 + }, + callback: (attachment, r) => { + frappe.call({ + method: 'frappe.core.doctype.file.file.unzip_file', + args: { + name: r.message.name, + }, + callback: function (r) { + if(r.exc) { + frappe.msgprint(__('Error in uploading files' + r.exc)); + } + } + }); + }, + }); + } + } + ]; + + return items; + } + + set_fields() { + this._fields = this.meta.fields + .filter(df => frappe.model.is_value_type(df.fieldtype) && !df.hidden) + .map(df => df.fieldname) + .concat(['name', 'modified']); + } + + update_data(data) { + super.update_data(data); + + this.data = this.data.map(d => { + let icon_class = ''; + if (d.is_folder) { + icon_class = "octicon octicon-file-directory"; + } else if (frappe.utils.is_image_file(d.file_name)) { + icon_class = "octicon octicon-file-media"; + } else { + icon_class = 'octicon octicon-file-text'; + } + + let title = d.file_name || d.file_url; + title = title.slice(0, 60); + + d._title = ` + + ${title} + ${d.is_private ? '' : ''} + `; + return d; + }); + + // Bring folders to the top + const { sort_by } = this.sort_selector; + if (sort_by === 'file_name') { + this.data.sort((a, b) => { + if (a.is_folder && !b.is_folder) { + return -1; + } + if (!a.is_folder &&b.is_folder) { + return 1; + } + return 0; + }); + } + } + + before_render() { + super.before_render(); + this.save_view_user_settings({ + last_folder: this.current_folder + }); + } + + get_header_html() { + let subject_html = ` +
    + + ${__('File Name')} +
    + + `; + + return this.get_header_html_skeleton(subject_html, ''); + } + + get_left_html(file) { + const file_size = frappe.form.formatters.FileSize(file.file_size); + const route_url = file.is_folder ? '#List/File/' + file.name : this.get_form_link(file); + + return ` + + + `; + } + + get_right_html(file) { + return ` +
    + ${comment_when(file.modified)} +
    + `; + } + + make_new_doc() { + frappe.ui.get_upload_dialog({ + "args": { + "folder": this.current_folder, + "from_form": 1 + }, + callback:() => this.refresh() + }); + } + + setup_events() { + super.setup_events(); + this.setup_drag_drop(); + } + + setup_drag_drop() { + this.$result.on('dragenter dragover', false) + .on('drop', e => { + var dataTransfer = e.originalEvent.dataTransfer; + if (!(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0)) { + return; + } + e.stopPropagation(); + e.preventDefault(); + frappe.upload.make({ + files: dataTransfer.files, + "args": { + "folder": this.current_folder, + "from_form": 1 + } + }); + }); + } + + toggle_result_area() { + super.toggle_result_area(); + this.toggle_cut_paste_buttons(); + } + + on_row_checked() { + super.on_row_checked(); + this.toggle_cut_paste_buttons(); + } + + toggle_cut_paste_buttons() { + // paste btn + const $paste_btn = this.page.menu_btn_group.find('.paste-menu-button'); + const hide = !frappe.file_manager.can_paste || + frappe.file_manager.old_folder === this.current_folder; + + if (hide) { + $paste_btn.addClass('hide'); + } else { + $paste_btn.removeClass('hide'); + } + + // cut btn + const $cut_btn = this.page.menu_btn_group.find('.cut-menu-button'); + if (this.$checks && this.$checks.length > 0) { + $cut_btn.removeClass('hide'); + } else { + $cut_btn.addClass('hide'); + } + } +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/gantt/gantt_view.js b/frappe/public/js/frappe/views/gantt/gantt_view.js index 885c7ddcc3..8529976b03 100644 --- a/frappe/public/js/frappe/views/gantt/gantt_view.js +++ b/frappe/public/js/frappe/views/gantt/gantt_view.js @@ -1,73 +1,117 @@ frappe.provide('frappe.views'); -frappe.views.GanttView = frappe.views.ListRenderer.extend({ - name: 'Gantt', - prepare: function(values) { - this.items = values; +frappe.views.GanttView = class GanttView extends frappe.views.ListView { + + setup_defaults() { + super.setup_defaults(); + this.page_title = this.page_title + ' ' + __('Gantt'); + this.calendar_settings = frappe.views.calendar[this.doctype] || {}; + this.order_by = this.view_user_settings.order_by || this.calendar_settings.field_map.start + ' asc'; + } + + setup_view() { + this.$result + .css('overflow', 'auto') + .append(''); + } + + update_data(data) { + super.update_data(data); this.prepare_tasks(); - this.prepare_dom(); - }, + } - render_view: function(values) { + prepare_tasks() { var me = this; - this.prepare(values); - this.render_gantt(); - }, + var meta = this.meta; + var field_map = this.calendar_settings.field_map; - set_defaults: function() { - this._super(); - this.no_realtime = true; - this.page_title = this.page_title + ' ' + __('Gantt'); - }, + this.tasks = this.data.map(function (item) { + // set progress + var progress = 0; + if (field_map.progress && $.isFunction(field_map.progress)) { + progress = field_map.progress(item); + } else if (field_map.progress) { + progress = item[field_map.progress]; + } - init_settings: function() { - this._super(); - this.field_map = frappe.views.calendar[this.doctype].field_map; - this.order_by = this.order_by || this.field_map.start + ' asc'; - }, + // title + var label; + if (meta.title_field) { + label = $.format("{0} ({1})", [item[meta.title_field], item.name]); + } else { + label = item[field_map.title]; + } - prepare_dom: function() { - this.wrapper.css('overflow', 'auto') - .append('') - }, + var r = { + start: item[field_map.start], + end: item[field_map.end], + name: label, + id: item[field_map.id || 'name'], + doctype: me.doctype, + progress: progress, + dependencies: item.depends_on_tasks || "" + }; - render_gantt: function(tasks) { - var me = this; - this.gantt_view_mode = this.user_settings.gantt_view_mode || 'Day'; - var field_map = frappe.views.calendar[this.doctype].field_map; + if (item.color && frappe.ui.color.validate_hex(item.color)) { + r['custom_class'] = 'color-' + item.color.substr(1); + } + + if (item.is_milestone) { + r['custom_class'] = 'bar-milestone'; + } + + return r; + }); + } + + render() { + this.load_lib.then(() => { + this.render_gantt(); + }); + } + + render_gantt() { + const me = this; + const gantt_view_mode = this.view_user_settings.gantt_view_mode || 'Day'; + const field_map = this.calendar_settings.field_map; + const date_format = 'YYYY-MM-DD'; this.gantt = new Gantt(".gantt-container", this.tasks, { - view_mode: this.gantt_view_mode, + view_mode: gantt_view_mode, date_format: "YYYY-MM-DD", on_click: function (task) { frappe.set_route('Form', task.doctype, task.id); }, - on_date_change: function(task, start, end) { - if(!me.can_write()) return; - me.update_gantt_task(task, start, end); + on_date_change: function (task, start, end) { + if (!me.can_write) return; + frappe.db.set_value(task.doctype, task.id, { + [field_map.start]: start.format(date_format), + [field_map.end]: end.format(date_format) + }); }, - on_progress_change: function(task, progress) { - if(!me.can_write()) return; + on_progress_change: function (task, progress) { + if (!me.can_write) return; var progress_fieldname = 'progress'; - if($.isFunction(field_map.progress)) { + if ($.isFunction(field_map.progress)) { progress_fieldname = null; - } else if(field_map.progress) { + } else if (field_map.progress) { progress_fieldname = field_map.progress; } - if(progress_fieldname) { - frappe.db.set_value(task.doctype, task.id, - progress_fieldname, parseInt(progress)); + if (progress_fieldname) { + frappe.db.set_value(task.doctype, task.id, { + [progress_fieldname]: parseInt(progress) + }); } }, - on_view_change: function(mode) { + on_view_change: function (mode) { // save view mode - frappe.model.user_settings.save(me.doctype, 'Gantt', { + me.save_view_user_settings({ gantt_view_mode: mode }); }, - custom_popup_html: function(task) { + custom_popup_html: function (task) { var item = me.get_item(task.id); var html = @@ -76,48 +120,51 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ // custom html in doctype settings var custom = me.settings.gantt_custom_popup_html; - if(custom && $.isFunction(custom)) { + if (custom && $.isFunction(custom)) { var ganttobj = task; html = custom(ganttobj, item); } return '
    ' + html + '
    '; } }); - this.render_dropdown(); + this.setup_view_mode_buttons(); this.set_colors(); - }, - - render_dropdown: function() { - var me = this; - var view_modes = this.gantt.config.view_modes || []; - var dropdown = ""; + } + setup_view_mode_buttons() { // view modes (for translation) __("Day"), __("Week"), __("Month"), //__("Half Day"), __("Quarter Day") - var dropdown_list = ""; - view_modes.forEach(function(view_mode) { - dropdown_list += "
  • " + - "" + - __(view_mode) + "
  • "; - }); - var $dropdown = $(dropdown) - $dropdown.find(".dropdown-menu").append(dropdown_list); - me.list_view.$page - .find(`[data-list-renderer='Gantt'] > .list-row-right`) - .css("margin-right", "15px").html($dropdown) - $dropdown.on("click", ".option", function() { - var mode = $(this).data('value'); - me.gantt.change_view_mode(mode); - $dropdown.find(".dropdown-text").text(mode); + let $btn_group = this.$paging_area.find('.gantt-view-mode'); + if ($btn_group.length > 0) return; + + const view_modes = this.gantt.config.view_modes || []; + const active_class = view_mode => this.gantt.view_is(view_mode) ? 'btn-info' : ''; + const html = + `
    + ${view_modes.map(value => ``).join('')} +
    `; + + this.$paging_area.find('.level-left').append(html); + + // change view mode asynchronously + const change_view_mode = (value) => setTimeout(() => this.gantt.change_view_mode(value), 0); + + this.$paging_area.on('click', '.btn-view-mode', e => { + const $btn = $(e.currentTarget); + this.$paging_area.find('.btn-view-mode').removeClass('btn-info'); + $btn.addClass('btn-info'); + + const value = $btn.data().value; + change_view_mode(value); }); - }, + } - set_colors: function() { + set_colors() { const classes = this.tasks .map(t => t.custom_class) .filter(c => c && c.startsWith('color-')); @@ -137,102 +184,19 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ }).join(""); style = ``; + this.$result.prepend(style); + } - this.wrapper.prepend(style); - }, - - prepare_tasks: function() { - var me = this; - var meta = frappe.get_meta(this.doctype); - var field_map = frappe.views.calendar[this.doctype].field_map; - this.tasks = this.items.map(function(item) { - // set progress - var progress = 0; - if(field_map.progress && $.isFunction(field_map.progress)) { - progress = field_map.progress(item); - } else if(field_map.progress) { - progress = item[field_map.progress] - } - - // title - if(meta.title_field) { - var label = $.format("{0} ({1})", [item[meta.title_field], item.name]); - } else { - var label = item[field_map.title]; - } - - var r = { - start: item[field_map.start], - end: item[field_map.end], - name: label, - id: item[field_map.id || 'name'], - doctype: me.doctype, - progress: progress, - dependencies: item.depends_on_tasks || "" - }; - - if(item.color && frappe.ui.color.validate_hex(item.color)) { - r['custom_class'] = 'color-' + item.color.substr(1); - } - - if(item.is_milestone) { - r['custom_class'] = 'bar-milestone'; - } - - return r; - }); - }, - get_item: function(name) { - return this.items.find(function(item) { + get_item(name) { + return this.data.find(function (item) { return item.name === name; }); - }, - update_gantt_task: function(task, start, end) { - var me = this; - if(me.gantt.updating_task) { - setTimeout(me.update_gantt_task.bind(me, task, start, end), 200) - return; - } - me.gantt.updating_task = true; - - var field_map = frappe.views.calendar[this.doctype].field_map; - frappe.call({ - method: 'frappe.desk.gantt.update_task', - args: { - args: { - doctype: task.doctype, - name: task.id, - start: start.format('YYYY-MM-DD'), - end: end.format('YYYY-MM-DD') - }, - field_map: field_map - }, - callback: function() { - me.gantt.updating_task = false; - frappe.show_alert({message:__("Saved"), indicator: 'green'}, 1); - } - }); - }, - get_header_html: function() { - return frappe.render_template('list_item_row_head', { main: '', list: this }); - }, - refresh: function(values) { - this.prepare(values); - this.render(); - }, - can_write: function() { - if(frappe.model.can_write(this.doctype)) { - return true; - } else { - // reset gantt state - this.gantt.change_view_mode(this.gantt_view_mode); - frappe.show_alert({message: __("Not permitted"), indicator: 'red'}, 1); - return false; - } - }, - set_columns: function() {}, - required_libs: [ - "assets/frappe/js/lib/snap.svg-min.js", - "assets/frappe/js/lib/frappe-gantt/frappe-gantt.js" - ] -}); \ No newline at end of file + } + + get required_libs() { + return [ + "assets/frappe/js/lib/snap.svg-min.js", + "assets/frappe/js/lib/frappe-gantt/frappe-gantt.js" + ]; + } +}; diff --git a/frappe/public/js/frappe/views/image/image_view.js b/frappe/public/js/frappe/views/image/image_view.js index b5f3d369b3..96c4ed06a6 100644 --- a/frappe/public/js/frappe/views/image/image_view.js +++ b/frappe/public/js/frappe/views/image/image_view.js @@ -3,11 +3,27 @@ */ frappe.provide("frappe.views"); -frappe.views.ImageView = frappe.views.ListRenderer.extend({ - name: 'Image', - render_view: function (values) { - this.items = values; +frappe.views.ImageView = class ImageView extends frappe.views.ListView { + setup_defaults() { + super.setup_defaults(); + this.page_title = this.page_title + ' ' + __('Images'); + } + + set_fields() { + this._fields = [ + 'name', + this.meta.title_field, + this.meta.image_field + ]; + } + + update_data(data) { + super.update_data(data); + this.items = this.data.map(this.prepare_data.bind(this)); + } + + render() { this.get_attached_images() .then(() => { this.render_image_view(); @@ -18,105 +34,138 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ this.gallery.prepare_pswp_items(this.items, this.images_map); } }); - }, - set_defaults: function() { - this._super(); - this.page_title = this.page_title + ' ' + __('Images'); - }, - prepare_data: function(data) { - data = this._super(data); + } + + prepare_data(data) { // absolute url if cordova, else relative data._image_url = this.get_image_url(data); return data; - }, - render_image_view: function () { - var html = this.items.map(this.render_item.bind(this)).join(""); - - this.container = this.wrapper.find('.image-view-container'); - if (this.container.length === 0) { - this.container = $('
    ') - .addClass('image-view-container') - .appendTo(this.wrapper); - } + } - this.container.append(html); - }, - render_item: function (item) { - var indicator = this.get_indicator_html(item); - return frappe.render_template("image_view_item_row", { - data: item, - indicator: indicator, - subject: this.get_subject_html(item, true), - additional_columns: this.additional_columns, - color: frappe.get_palette(item.item_name) - }); - }, - get_image_url: function (item) { + render_image_view() { + var html = this.items.map(this.item_html.bind(this)).join(""); + + this.$result.html(` + ${this.get_header_html()} +
    + ${html} +
    + `); + } + + item_html(item) { + item._name = encodeURI(item.name); + const encoded_name = item._name; + const title = strip_html(item[this.meta.title_field || 'name']); + const _class = !item._image_url ? 'no-image' : ''; + const _html = item._image_url ? + `${ title }` : + ` + ${ frappe.get_abbr(title) } + `; + + return ` +
    +
    +
    + ${this.get_subject_html(item)} +
    +
    + +
    + `; + } + + get_image_url(data) { var url; - url = item.image ? item.image : item[this.meta.image_field]; + url = data.image ? data.image : data[this.meta.image_field]; // absolute url for mobile if (window.cordova && !frappe.utils.is_url(url)) { url = frappe.base_url + url; } if (url) { - return url + return url; } return null; - }, - get_attached_images: function () { + } + + get_attached_images() { return frappe.call({ method: 'frappe.core.doctype.file.file.get_attached_images', - args: { doctype: this.doctype, names: this.items.map(i => i.name) } + args: { + doctype: this.doctype, + names: this.items.map(i => i.name) + } }).then(r => { this.images_map = Object.assign(this.images_map || {}, r.message); }); - }, - get_header_html: function () { - var main = frappe.render_template('list_item_main_head', { - col: { type: "Subject" }, - _checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) - && !this.no_delete) - }); - return frappe.render_template('list_item_row_head', { main: main, list: this }); - }, - setup_gallery: function() { + } + + get_header_html() { + return this.get_header_html_skeleton(` +
    + + + + + +
    + `); + } + + setup_gallery() { var me = this; this.gallery = new frappe.views.GalleryView({ doctype: this.doctype, items: this.items, - wrapper: this.container, + wrapper: this.$result, images_map: this.images_map }); - this.container.on('click', '.btn.zoom-view', function(e) { + this.$result.on('click', '.btn.zoom-view', function (e) { e.preventDefault(); e.stopPropagation(); var name = $(this).data().name; + name = decodeURIComponent(name); me.gallery.show(name); return false; }); } -}); +}; frappe.views.GalleryView = Class.extend({ - init: function(opts) { + init: function (opts) { $.extend(this, opts); var me = this; this.lib_ready = this.load_lib(); - this.lib_ready.then(function() { + this.lib_ready.then(function () { me.prepare(); }); }, - prepare: function() { + prepare: function () { // keep only one pswp dom element this.pswp_root = $('body > .pswp'); - if(this.pswp_root.length === 0) { + if (this.pswp_root.length === 0) { var pswp = frappe.render_template('photoswipe_dom'); this.pswp_root = $(pswp).appendTo('body'); } }, - prepare_pswp_items: function(_items, _images_map) { + prepare_pswp_items: function (_items, _images_map) { var me = this; if (_items) { @@ -126,18 +175,18 @@ frappe.views.GalleryView = Class.extend({ } return new Promise(resolve => { - const items = this.items.map(function(i) { - const query = 'img[data-name="'+i.name+'"]'; + const items = this.items.map(function (i) { + const query = 'img[data-name="' + i._name + '"]'; let el = me.wrapper.find(query).get(0); let width, height; - if(el) { + if (el) { width = el.naturalWidth; height = el.naturalHeight; } - if(!el) { - el = me.wrapper.find('.image-field[data-name="'+i.name+'"]').get(0); + if (!el) { + el = me.wrapper.find('.image-field[data-name="' + i._name + '"]').get(0); width = el.getBoundingClientRect().width; height = el.getBoundingClientRect().height; } @@ -149,26 +198,26 @@ frappe.views.GalleryView = Class.extend({ w: width, h: height, el: el - } + }; }); this.pswp_items = items; resolve(); }); }, - show: function(docname) { + show: function (docname) { this.lib_ready .then(() => this.prepare_pswp_items()) .then(() => this._show(docname)); }, - _show: function(docname) { + _show: function (docname) { const me = this; const items = this.pswp_items; const item_index = items.findIndex(item => item.name === docname); var options = { index: item_index, - getThumbBoundsFn: function(index) { - const query = 'img[data-name="' + items[index].name + '"]'; + getThumbBoundsFn: function (index) { + const query = 'img[data-name="' + items[index]._name + '"]'; let thumbnail = me.wrapper.find(query).get(0); if (!thumbnail) { @@ -178,12 +227,16 @@ frappe.views.GalleryView = Class.extend({ var pageYScroll = window.pageYOffset || document.documentElement.scrollTop, rect = thumbnail.getBoundingClientRect(); - return {x:rect.left, y:rect.top + pageYScroll, w:rect.width}; + return { + x: rect.left, + y: rect.top + pageYScroll, + w: rect.width + }; }, history: false, shareEl: false, showHideOpacity: true - } + }; // init this.pswp = new PhotoSwipe( @@ -195,12 +248,12 @@ frappe.views.GalleryView = Class.extend({ this.browse_images(); this.pswp.init(); }, - browse_images: function() { + browse_images: function () { const $more_items = this.pswp_root.find('.pswp__more-items'); const images_map = this.images_map; let last_hide_timeout = null; - this.pswp.listen('afterChange', function() { + this.pswp.listen('afterChange', function () { const images = images_map[this.currItem.name]; if (!images || images.length === 1) { $more_items.html(''); @@ -214,7 +267,9 @@ frappe.views.GalleryView = Class.extend({ this.pswp.listen('beforeChange', hide_more_items); this.pswp.listen('initialZoomOut', hide_more_items); - this.pswp.listen('destroy', $(document).off('mousemove', hide_more_items_after_2s)); + this.pswp.listen('destroy', () => { + $(document).off('mousemove', hide_more_items_after_2s); + }); // Replace current image on click $more_items.on('click', '.pswp__more-item', (e) => { @@ -255,7 +310,7 @@ frappe.views.GalleryView = Class.extend({
    `; } }, - load_lib: function() { + load_lib: function () { return new Promise(resolve => { var asset_dir = 'assets/frappe/js/lib/photoswipe/'; frappe.require([ @@ -266,4 +321,4 @@ frappe.views.GalleryView = Class.extend({ ], resolve); }); } -}); +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/image/image_view_item_row.html b/frappe/public/js/frappe/views/image/image_view_item_row.html index 7eca23dddb..a2adeaf0cf 100644 --- a/frappe/public/js/frappe/views/image/image_view_item_row.html +++ b/frappe/public/js/frappe/views/image/image_view_item_row.html @@ -32,13 +32,4 @@
    -
    diff --git a/frappe/public/js/frappe/views/inbox/inbox_no_result.html b/frappe/public/js/frappe/views/inbox/inbox_no_result.html deleted file mode 100644 index 0251d68365..0000000000 --- a/frappe/public/js/frappe/views/inbox/inbox_no_result.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - {% if(!frappe.model.can_create(doctype) && doctype == "Email Account") { %} -

    {{ __("No Email Accounts Assigned") }}

    - {% } else { %} -

    {{ msg }}

    -

    - -

    - {% } %} -
    \ No newline at end of file diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js index a028860c73..ebebffc673 100644 --- a/frappe/public/js/frappe/views/inbox/inbox_view.js +++ b/frappe/public/js/frappe/views/inbox/inbox_view.js @@ -1,168 +1,204 @@ /** - * frappe.views.EmailInboxView + * frappe.views.InboxView */ frappe.provide("frappe.views"); -frappe.views.InboxView = frappe.views.ListRenderer.extend({ - name: 'Inbox', - render_view: function(values) { - var me = this; +frappe.views.InboxView = class InboxView extends frappe.views.ListView { + static load_last_view() { + const route = frappe.get_route(); + if (!route[3] && frappe.boot.email_accounts.length) { + let email_account; + if (frappe.boot.email_accounts[0].email_id == "All Accounts") { + email_account = "All Accounts"; + } else { + email_account = frappe.boot.email_accounts[0].email_account; + } + frappe.set_route("List", "Communication", "Inbox", email_account); + return true; + } else if (!route[3] || (route[3] !== "All Accounts" && !is_valid(route[3]))) { + frappe.msgprint(__('Invalid Email Account')); + window.history.back(); + return true; + } + return false; - this.emails = values; + function is_valid(email_account) { + return frappe.boot.email_accounts.find(d => d.email_account === email_account); + } + } + show() { + super.show(); // save email account in user_settings - frappe.model.user_settings.save("Communication", 'Inbox', { + this.save_view_user_settings({ last_email_account: this.current_email_account }); + } - this.render_inbox_view(); - }, - render_inbox_view: function() { - var html = "" - - var email_account = this.get_current_email_account() - if(email_account) - html = this.emails.map(this.render_email_row.bind(this)).join(""); - else - html = this.make_no_result() - - this.container = $('
    ') - .addClass('inbox-container') - .appendTo(this.wrapper); - this.container.append(html); - }, - render_email_row: function(email) { - if(!email.css_seen && email.seen) - email.css_seen = "seen" - - return frappe.render_template("inbox_view_item_row", { - data: email, - is_sent_emails: this.is_sent_emails, - }); - }, - set_defaults: function() { - this._super(); - this.page_title = __("Email Inbox"); - }, - - init_settings: function() { - this._super(); + setup_defaults() { + super.setup_defaults(); + this.email_account = frappe.get_route()[3]; + this.page_title = this.email_account; this.filters = this.get_inbox_filters(); - }, - should_refresh: function() { - var to_refresh = this._super(); - if(!to_refresh) { - this.last_email_account = this.current_email_account || ''; - this.current_email_account = this.get_current_email_account(); - this.is_sent_emails = this.current_email_account === "Sent"? true: false - - to_refresh = this.current_email_account !== this.last_email_account; - } + } - if(to_refresh){ - this.list_view.page.main.find(".list-headers").empty(); - } - return to_refresh; - }, - get_inbox_filters: function() { - var email_account = this.get_current_email_account(); + get is_sent_emails() { + const f = this.filter_area.get() + .find(filter => filter[1] === 'sent_or_received'); + return f && f[3] === 'Sent'; + } + + render() { + this.emails = this.data; + this.render_inbox_view(); + } + + render_inbox_view() { + let html = this.emails.map(this.render_email_row.bind(this)).join(""); + + this.$result.html(` + ${this.get_header_html()} + ${html} + `); + } + + get_header_html() { + return this.get_header_html_skeleton(` +
    + + ${__('Subject')} +
    + + `); + } + + render_email_row(email) { + if (!email.css_seen && email.seen) + email.css_seen = "seen"; + + const columns_html = ` + + + `; + + return this.get_list_row_html_skeleton(columns_html, this.get_meta_html(email)); + } + + get_meta_html(email) { + const attachment = email.has_attachment ? + `` : ''; + + const form_link = frappe.utils.get_form_link(email.reference_doctype, email.reference_name); + const link = email.reference_doctype && email.reference_doctype !== this.doctype ? + ` + + ` : ''; + + const modified = comment_when(email.modified, true); + + return ` + + `; + } + + get_inbox_filters() { + var email_account = this.email_account; var default_filters = [ ["Communication", "communication_type", "=", "Communication", true], ["Communication", "communication_medium", "=", "Email", true], - - ] - var filters = [] + ]; + var filters = []; if (email_account === "Sent") { filters = default_filters.concat([ ["Communication", "sent_or_received", "=", "Sent", true], ["Communication", "email_status", "not in", "Spam,Trash", true], - ]) - } - else if (in_list(["Spam", "Trash"], email_account)) { + ]); + } else if (in_list(["Spam", "Trash"], email_account)) { filters = default_filters.concat([ ["Communication", "email_status", "=", email_account, true], ["Communication", "email_account", "in", frappe.boot.all_accounts, true] - ]) - } - else { - var op = "=" + ]); + } else { + var op = "="; if (email_account == "All Accounts") { op = "in"; - email_account = frappe.boot.all_accounts + email_account = frappe.boot.all_accounts; } filters = default_filters.concat([ ["Communication", "sent_or_received", "=", "Received", true], ["Communication", "email_account", op, email_account, true], ["Communication", "email_status", "not in", "Spam,Trash", true], - ]) + ]); } - return filters - }, - get_header_html: function() { - var header = "" - if(this.current_email_account) { - header = frappe.render_template('inbox_view_item_main_head', { - _checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) - && !this.no_delete), - is_sent_emails: this.is_sent_emails - }); - } + return filters; + } - return header; - }, - get_current_email_account: function() { - var route = frappe.get_route(); - if(!route[3] && frappe.boot.email_accounts.length) { - var email_account; - if(frappe.boot.email_accounts[0].email_id == "All Accounts") { - email_account = "All Accounts" - } else { - email_account = frappe.boot.email_accounts[0].email_account - } - frappe.set_route("List", "Communication", "Inbox", email_account); - } else if(route[3] && route[3] != "All Accounts" && - !frappe.boot.email_accounts.find(b => b.email_account === route[3])) { - // frappe.throw(__(`Email Account ${route[3] || ''} not found`)); - return '' - } - return route[3]; - }, - make_no_result: function () { - var no_result_message = "" - var email_account = this.get_current_email_account(); + get_no_result_message() { + var email_account = this.email_account; var args; if (in_list(["Spam", "Trash"], email_account)) { - return __("No {0} mail", [email_account]) - } else if(!email_account && !frappe.boot.email_accounts.length) { + return __("No {0} mail", [email_account]); + } else if (!email_account && !frappe.boot.email_accounts.length) { // email account is not configured - this.no_result_doctype = "Email Account" args = { doctype: "Email Account", msg: __("No Email Account"), label: __("New Email Account"), - } + }; } else { // no sent mail - this.no_result_doctype = "Communication"; args = { doctype: "Communication", msg: __("No Emails"), label: __("Compose Email") - } + }; } - var no_result_message = frappe.render_template("inbox_no_result", args) - return no_result_message; - }, - make_new_doc: function() { - if (this.no_result_doctype == "Communication") { + + const html = frappe.model.can_create(args.doctype) ? + `

    ${args.msg}

    +

    + +

    + ` : + `

    ${ __("No Email Accounts Assigned") }

    `; + + return ` +
    + ${html} +
    + `; + } + + make_new_doc() { + if (!this.email_account && !frappe.boot.email_accounts.length) { + frappe.route_options = { + 'email_id': frappe.session.user_email + }; + frappe.new_doc('Email Account'); + } else { new frappe.views.CommunicationComposer({ doc: {} - }) - } else { - frappe.route_options = { 'email_id': frappe.session.user_email } - frappe.new_doc(this.no_result_doctype) + }); } } -}); +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/inbox/inbox_view_item_main_head.html b/frappe/public/js/frappe/views/inbox/inbox_view_item_main_head.html deleted file mode 100644 index 40ebaa94cf..0000000000 --- a/frappe/public/js/frappe/views/inbox/inbox_view_item_main_head.html +++ /dev/null @@ -1,24 +0,0 @@ -
    -
    -
    - -
    -
    -
    - {% if (_checkbox) { %} - - {% } %} - {%= __("Subject") %} -
    -
    - -
    -
    - -
    -
    \ No newline at end of file diff --git a/frappe/public/js/frappe/views/inbox/inbox_view_item_row.html b/frappe/public/js/frappe/views/inbox/inbox_view_item_row.html deleted file mode 100644 index c042a92bbd..0000000000 --- a/frappe/public/js/frappe/views/inbox/inbox_view_item_row.html +++ /dev/null @@ -1,49 +0,0 @@ -
    -
    -
    -
    -
    - - {% if (data._checkbox) { %} - - {% } %} - - {%= data.subject %} - - -
    - -
    -
    -
    -
    - - {% if(data.has_attachment) { %} - - {% } %} - -
    - -
    -
    -
    \ No newline at end of file diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index a236d6a4fe..8c8d3552f7 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -33,11 +33,6 @@ frappe.provide("frappe.views"); }); var columns = prepare_columns(board.columns); - // save kanban board name in user_settings - frappe.model.user_settings.save(opts.doctype, 'Kanban', { - last_kanban_board: opts.board_name - }); - updater.set({ doctype: opts.doctype, board: board, @@ -57,10 +52,8 @@ frappe.provide("frappe.views"); }, update_cards: function (updater, cards) { var state = this; - var _cards = - cards.map(card => { - return prepare_card(card, state); - }) + var _cards = cards + .map(card => prepare_card(card, state)) .concat(this.cards) .uniqBy(card => card.name); @@ -90,18 +83,18 @@ frappe.provide("frappe.views"); var board = this.board; fetch_customization(doctype) .then(function (doc) { - return modify_column_field_in_c11n(doc, board, col.title, action) + return modify_column_field_in_c11n(doc, board, col.title, action); }) .then(save_customization) - .then(function (r) { - return update_kanban_board(board.name, col.title, action) + .then(function () { + return update_kanban_board(board.name, col.title, action); }).then(function (r) { var cols = r.message; updater.set({ columns: prepare_columns(cols) }); }, function (err) { - console.error(err); + console.error(err); // eslint-disable-line }); }, set_filter_state: function (updater) { @@ -115,14 +108,14 @@ frappe.provide("frappe.views"); save_filters: function (updater) { if(saving_filters) return; saving_filters = true; - var filters = JSON.stringify(this.cur_list.filter_list.get_filters()); + var filters = JSON.stringify(this.cur_list.filter_area.get()); frappe.call({ method: method_prefix + 'save_filters', args: { board_name: this.board.name, filters: filters } - }).then(function(r) { + }).then(function() { saving_filters = false; updater.set({ filters_modified: false }); frappe.show_alert({ @@ -135,7 +128,6 @@ frappe.provide("frappe.views"); var doc = frappe.model.get_new_doc(this.doctype); var field = this.card_meta.title_field; var quick_entry = this.card_meta.quick_entry; - var board = this.board; var state = this; var doc_fields = {}; @@ -198,7 +190,6 @@ frappe.provide("frappe.views"); order: order }, callback: (r) => { - var state = this; var board = r.message[0]; var updated_cards = r.message[1]; var cards = update_cards_column(updated_cards); @@ -208,8 +199,7 @@ frappe.provide("frappe.views"); columns: columns }); } - }) - .fail(function(e) { + }).fail(function() { // revert original order updater.set({ cards: _cards, @@ -246,7 +236,7 @@ frappe.provide("frappe.views"); updater.set({ columns: columns }); - }) + }); } } }); @@ -262,12 +252,12 @@ frappe.provide("frappe.views"); // update cards internally opts.cards = cards; - if(self.wrapper.find('.kanban').length > 0) { + if(self.wrapper.find('.kanban').length > 0 && self.cur_list.start !== 0) { fluxify.doAction('update_cards', cards); } else { init(); } - } + }; function init() { fluxify.doAction('init', opts); @@ -286,7 +276,7 @@ frappe.provide("frappe.views"); self.$kanban_board.appendTo(self.wrapper); } - self.$filter_area = self.cur_list.$page.find('.set-filters'); + self.$filter_area = self.cur_list.$page.find('.active-tag-filters'); bind_events(); setup_sortable(); } @@ -312,7 +302,7 @@ frappe.provide("frappe.views"); dataIdAttr: 'data-column-value', filter: '.add-new-column', handle: '.kanban-column-title', - onEnd: function(evt) { + onEnd: function() { var order = sortable.toArray(); order = order.slice(1); fluxify.doAction('update_column_order', order); @@ -322,7 +312,6 @@ frappe.provide("frappe.views"); function bind_add_column() { - var wrapper = self.$kanban_board; var $add_new_column = self.$kanban_board.find(".add-new-column"), $compose_column = $add_new_column.find(".compose-column"), $compose_column_form = $add_new_column.find(".compose-column-form").hide(); @@ -342,7 +331,7 @@ frappe.provide("frappe.views"); var title = $compose_column_form.serializeArray()[0].value; var col = { title: title.trim() - } + }; fluxify.doAction('add_column', col); $compose_column_form.find('input').val(''); $compose_column.show(); @@ -352,7 +341,7 @@ frappe.provide("frappe.views"); }); // on form blur - $compose_column_form.find('input').on("blur", function (e) { + $compose_column_form.find('input').on("blur", function () { $(this).val(''); $compose_column.show(); $compose_column_form.hide(); @@ -362,7 +351,7 @@ frappe.provide("frappe.views"); function bind_save_filter() { var set_filter_state = function () { fluxify.doAction('set_filter_state'); - } + }; if(isBound(self.$kanban_board, 'after-refresh', set_filter_state)) return; @@ -375,8 +364,8 @@ frappe.provide("frappe.views"); function setup_restore_columns() { var cur_list = store.getState().cur_list; var columns = store.getState().columns; - var list_row_right = - cur_list.$page.find(`[data-list-renderer='Kanban'] .list-row-right`) + var list_row_right = cur_list.$page + .find(`[data-list-renderer='Kanban'] .list-row-right`) .css('margin-right', '15px'); list_row_right.empty(); @@ -398,16 +387,16 @@ frappe.provide("frappe.views"); "" + "" + __('Archived Columns') + "" + "" + - "
    ") + "
    "); list_row_right.html($dropdown); - $dropdown.find(".dropdown-menu").on("click", "button.restore-column", function (e) { + $dropdown.find(".dropdown-menu").on("click", "button.restore-column", function () { var column_title = $(this).data().column; var col = { title: column_title, status: 'Archived' - } + }; fluxify.doAction('restore_column', col); }); } @@ -427,7 +416,7 @@ frappe.provide("frappe.views"); init(); return self; - } + }; frappe.views.KanbanBoardColumn = function (column, wrapper) { var self = {}; @@ -455,7 +444,6 @@ frappe.provide("frappe.views"); function make_cards() { self.$kanban_cards.empty(); var cards = store.getState().cards; - var board = store.getState().board; filtered_cards = get_cards_for_column(cards, column); var filtered_cards_names = filtered_cards.map(card => card.name); @@ -480,20 +468,20 @@ frappe.provide("frappe.views"); } function setup_sortable() { - var sortable = Sortable.create(self.$kanban_cards.get(0), { + Sortable.create(self.$kanban_cards.get(0), { group: "cards", animation: 150, dataIdAttr: 'data-name', - onStart: function (evt) { + onStart: function () { wrapper.find('.kanban-card.add-card').fadeOut(200, function () { wrapper.find('.kanban-cards').height('100vh'); }); }, - onEnd: function (evt) { + onEnd: function () { wrapper.find('.kanban-card.add-card').fadeIn(100); wrapper.find('.kanban-cards').height('auto'); // update order - var order = {} + var order = {}; wrapper.find('.kanban-column[data-column-value]') .each(function() { var col_name = $(this).data().columnValue; @@ -505,7 +493,7 @@ frappe.provide("frappe.views"); }); fluxify.doAction('update_order', order); }, - onAdd: function (evt) { + onAdd: function () { }, }); } @@ -543,7 +531,7 @@ frappe.provide("frappe.views"); }); // on textarea blur - $textarea.on("blur", function (e) { + $textarea.on("blur", function () { $(this).val(''); $btn_add.show(); $new_card_area.hide(); @@ -552,7 +540,7 @@ frappe.provide("frappe.views"); function bind_options() { self.$kanban_column.find(".column-options .dropdown-menu") - .on("click", "[data-action]", function (e) { + .on("click", "[data-action]", function () { var $btn = $(this); var action = $btn.data().action; @@ -564,11 +552,11 @@ frappe.provide("frappe.views"); } }); get_column_indicators(function(indicators) { - var html = '
  • ' + var html = '
  • '; html += indicators.reduce(function(prev, curr) { return prev + '
    ' + class="btn btn-default btn-xs indicator ' + curr + '">
  • '; }, ""); html += ''; self.$kanban_column.find(".column-options .dropdown-menu") @@ -577,7 +565,7 @@ frappe.provide("frappe.views"); } init(); - } + }; frappe.views.KanbanBoardCard = function (card, wrapper) { var self = {}; @@ -630,56 +618,6 @@ frappe.provide("frappe.views"); }); } - function setup_edit_card() { - if (self.edit_dialog) { - refresh_dialog(); - self.edit_dialog.show(); - return; - } - - var card_meta = store.getState().card_meta; - get_doc().then(function () { - // prepare dialog fields - var fields = []; - if (card_meta.description_field) { - fields.push({ - fieldtype: "Small Text", label: __("Description"), - fieldname: card_meta.description_field.fieldname - }); - } - - fields.push({ fieldtype: "Section Break" }); - fields.push({ - fieldtype: "Read Only", label: "Assigned to", - fieldname: "assignees" - }); - fields.push({ fieldtype: "Column Break" }); - - if (card_meta.due_date_field) { - fields.push(card_meta.due_date_field); - } - - var d = make_edit_dialog(card.title, fields); - - refresh_dialog(); - make_timeline(); - edit_card_title(); - - d.set_primary_action(__('Save'), function () { - if (d.working) return; - var doc = d.get_values(true); - $.extend(doc, { name: card.name, doctype: card.doctype }); - d.working = true; - fluxify.doAction('update_doc', doc, card) - .then(function (r) { - d.working = false; - d.hide(); - }); - }); - d.show(); - }); - } - function refresh_dialog() { set_dialog_fields(); make_assignees(); @@ -694,36 +632,6 @@ frappe.provide("frappe.views"); }); } - function get_doc() { - return new Promise(function (resolve, reject) { - frappe.model.with_doc(card.doctype, card.name, function () { - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: card.doctype, - name: card.name - }, - callback: function (r) { - var doc = r.message; - if (!doc) { - reject(__("{0} {1} does not exist", [card.doctype, card.name])); - } - card.doc = doc; - resolve(); - } - }); - }); - }); - } - - function make_edit_dialog(title, fields) { - self.edit_dialog = new frappe.ui.Dialog({ - title: title, - fields: fields - }); - return self.edit_dialog; - } - function make_assignees() { var d = self.edit_dialog; var html = get_assignees_html() + '\ @@ -751,7 +659,7 @@ frappe.provide("frappe.views"); method: 'frappe.desk.form.assign_to.add', doctype: card.doctype, docname: card.name, - callback: function(r) { + callback: function() { var user = self.assign_to_dialog.get_values().assign_to; card.assigned_list.push(user); fluxify.doAction('update_card', card); @@ -762,100 +670,8 @@ frappe.provide("frappe.views"); self.assign_to_dialog.show(); } - function make_timeline() { - var d = self.edit_dialog; - // timeline wrapper - d.$wrapper.find('.modal-body').append('
    '); - - // edit in full page button - $('') - .appendTo(d.$wrapper.find('.modal-body')) - .on('click', function () { - frappe.set_route("Form", card.doctype, card.name); - }); - var tl = new frappe.ui.form.Timeline({ - parent: d.$wrapper.find(".form-comments"), - frm: { - doctype: card.doctype, - docname: card.name, - get_docinfo: function () { - return frappe.model.get_docinfo(card.doctype, card.name) - }, - doc: card.doc, - sidebar: { - refresh_comments: function () { } - }, - trigger: function () { } - } - }); - tl.wrapper.addClass('in-dialog'); - tl.wrapper.find('.timeline-new-email').remove(); - // update comment count - var tl_refresh = tl.refresh.bind(tl); - tl.refresh = function () { - tl_refresh(); - var communications = tl.get_communications(); - var comment_count = communications.filter(function (c) { - return c.comment_type === 'Comment'; - }).length; - if (comment_count !== card.comment_count) { - card.comment_count = comment_count; - fluxify.doAction('update_card', card); - } - } - tl.refresh(); - } - - function edit_card_title() { - var $card_title = self.edit_dialog.header.find('.modal-title'); - var $title_wrapper = $card_title.parent(); - - $title_wrapper.addClass('edit-card-title').empty(); - - var template = repl('
    \ - %(card_title)s\ - \ -
    ', { card_title: card.title }); - - $title_wrapper.html(template); - - var $input = $title_wrapper.find('input').hide(); - var $span = $title_wrapper.find('span'); - - $span.on('click', function() { - $input.show(); - $span.hide(); - $input.val(card.title); - $input.focus(); - }); - - $input.on('blur', function() { - $input.hide(); - $span.show(); - }); - - $input.keydown(function(e) { - if (e.which === 13) { - e.preventDefault(); - var new_title = $input.val(); - if (card.title === new_title) { - return; - } - get_doc().then(function () { - var tf = store.getState().card_meta.title_field.fieldname; - var doc = card.doc; - doc[tf] = new_title; - fluxify.doAction('update_doc', doc, card); - $span.html(new_title); - $input.trigger('blur'); - }) - } - }) - } - init(); - } + }; // Helpers function get_board(board_name) { @@ -874,7 +690,7 @@ frappe.provide("frappe.views"); } return prepare_board(board); }, function(e) { - console.log(e) + console.log(e); // eslint-disable-line }); } @@ -897,7 +713,11 @@ frappe.provide("frappe.views"); } meta.fields.forEach(function (df) { - if (in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) && !title_field) { + const is_valid_field = + in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) + && !df.hidden; + + if (is_valid_field && !title_field) { // can be mapped to textarea title_field = df; } @@ -926,7 +746,7 @@ frappe.provide("frappe.views"); title_field: title_field, description_field: description_field, due_date_field: due_date_field, - } + }; } function get_date_field(fields) { @@ -993,7 +813,7 @@ frappe.provide("frappe.views"); } function fetch_customization(doctype) { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { frappe.model.with_doc("Customize Form", "Customize Form", function () { var doc = frappe.get_doc("Customize Form"); doc.doc_type = doctype; @@ -1023,7 +843,7 @@ frappe.provide("frappe.views"); args: { doc: doc }, - callback: function (r) { + callback: function () { frappe.model.clear_doc(doc.doctype, doc.name); frappe.show_alert({ message: __("Saved"), indicator: 'green' }, 1); } @@ -1049,24 +869,27 @@ frappe.provide("frappe.views"); } function is_filters_modified(board, cur_list) { - return new Promise(function(resolve, reject) { + return new Promise(function(resolve) { setTimeout(function() { - // sometimes the filter_list is not initiated, so early return - if(!cur_list.filter_list) resolve(false); + try { + var list_filters = JSON.stringify(cur_list.filter_area.get()); + resolve(list_filters !== board.filters); + } catch(e) { + // sometimes the filter_list is not initiated + resolve(false); + } - var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); - resolve(list_filters !== board.filters); }, 2000); - }) + }); } function is_active_column(col) { - return col.status !== 'Archived' + return col.status !== 'Archived'; } function get_cards_for_column(cards, column) { return cards.filter(function (card) { - return card.column === column.title + return card.column === column.title; }); } @@ -1099,7 +922,7 @@ frappe.provide("frappe.views"); }); if(!indicators) { // - indicators = ['green', 'blue', 'orange', 'grey'] + indicators = ['green', 'blue', 'orange', 'grey']; } callback(indicators); }); @@ -1118,7 +941,7 @@ frappe.provide("frappe.views"); } function remove_img_tags(html) { - const $temp = $(`
    ${html}
    `) + const $temp = $(`
    ${html}
    `); $temp.find('img').remove(); return $temp.html(); } diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 5703282254..4ff6f2fe87 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -1,77 +1,63 @@ frappe.provide('frappe.views'); -frappe.views.KanbanView = frappe.views.ListRenderer.extend({ - name: 'Kanban', - render_view: function(values) { - var board_name = this.get_board_name(); +frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { + static load_last_view() { + const route = frappe.get_route(); + if (route.length === 3) { + const doctype = route[1]; + const user_settings = frappe.get_user_settings(doctype)['Kanban'] || {}; + if (!user_settings.last_kanban_board) { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('Missing parameter Kanban Board Name') + }); + frappe.set_route('List', doctype, 'List'); + return true; + } + route.push(user_settings.last_kanban_board); + frappe.set_route(route); + return true; + } + return false; + } + + setup_defaults() { + super.setup_defaults(); + this.board_name = frappe.get_route()[3]; + this.page_title = this.board_name; + } + + show() { + super.show(); + this.save_view_user_settings({ + last_kanban_board: this.board_name + }); + } + + render() { + const board_name = this.board_name; if(this.kanban && board_name === this.kanban.board_name) { - this.kanban.update(values); + this.kanban.update(this.data); + this.kanban.$kanban_board.trigger('after-refresh'); return; } this.kanban = new frappe.views.KanbanBoard({ doctype: this.doctype, board_name: board_name, - cards: values, - wrapper: this.wrapper, - cur_list: this.list_view, - user_settings: this.user_settings + cards: this.data, + wrapper: this.$result, + cur_list: this, + user_settings: this.view_user_settings }); - }, - after_refresh: function() { - this.wrapper.find('.kanban').trigger('after-refresh'); - frappe.kanban_filters[this.get_board_name()] = - this.list_view.filter_list.get_filters(); - }, - should_refresh: function() { - var to_refresh = this._super(); - if(!to_refresh) { - this.last_kanban_board = this.current_kanban_board || ''; - this.current_kanban_board = this.get_board_name(); - this.page_title = __(this.get_board_name()); + this.kanban.$kanban_board.trigger('after-refresh'); + } - to_refresh = this.current_kanban_board !== this.last_kanban_board; - } - return to_refresh; - }, - init_settings: function() { - this._super(); - this.filters = this.get_kanban_filters(); - }, - get_kanban_filters: function() { - frappe.provide('frappe.kanban_filters'); - - var board_name = this.get_board_name(); - if (!frappe.kanban_filters[board_name]) { - var kb = this.meta.__kanban_boards.find( - board => board.name === board_name - ); - frappe.kanban_filters[board_name] = JSON.parse(kb && kb.filters || '[]'); - } - if(typeof frappe.kanban_filters[board_name] === 'string') { - frappe.kanban_filters[board_name] = - JSON.parse( - frappe.kanban_filters[board_name] || '[]' - ) - } - var filters = frappe.kanban_filters[board_name]; - return filters; - }, - set_defaults: function() { - this._super(); - this.no_realtime = true; - this.show_no_result = false; - this.page_title = __(this.get_board_name()); - }, - get_board_name: function() { - var route = frappe.get_route(); - return route[3]; - }, - get_header_html: function() { - return frappe.render_template('list_item_row_head', { main: '', list: this }); - }, - required_libs: [ - 'assets/frappe/js/lib/fluxify.min.js', - 'assets/frappe/js/frappe/views/kanban/kanban_board.js' - ] -}); \ No newline at end of file + get required_libs() { + return [ + 'assets/frappe/js/lib/fluxify.min.js', + 'assets/frappe/js/frappe/views/kanban/kanban_board.js' + ]; + } +}; diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index e30f01458d..4ec8184e77 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -10,7 +10,7 @@ {% for col in columns %} {% if col.name && col._id !== "_check" %} {{ __(col.name) }} {% endif %} @@ -24,11 +24,13 @@ {% for col in columns %} {% if col.name && col._id !== "_check" %} - {% var value = col.fieldname ? row[col.fieldname] : row[col.field]; %} + {% var value = col.fieldname ? row[col.fieldname] : row[col.field]; %} - {{ col.formatter + + {{ col.formatter ? col.formatter(row._index, col._index, value, col, row, true) - : value }} + : (col.docfield ? frappe.format(value, col.docfield) : value) }} + {% endif %} {% endfor %} diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js new file mode 100644 index 0000000000..430d0fbb45 --- /dev/null +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -0,0 +1,716 @@ +/** + * frappe.views.ReportView + */ +frappe.provide('frappe.views'); + +frappe.views.ReportView = class ReportView extends frappe.views.ListView { + setup_defaults() { + super.setup_defaults(); + this.page_title = __('Report:') + ' ' + this.page_title; + this.menu_items = this.report_menu_items(); + + const route = frappe.get_route(); + if (route.length === 4) { + this.report_name = route[3]; + } + + this.add_totals_row = this.view_user_settings.add_totals_row || 0; + + if (this.report_name) { + return this.get_report_doc() + .then(doc => { + this.report_doc = doc; + this.report_doc.json = JSON.parse(this.report_doc.json); + + this.filters = this.report_doc.json.filters; + this.order_by = this.report_doc.json.order_by; + this.add_totals_row = this.report_doc.json.add_totals_row; + this.page_title = this.report_name; + }); + } + } + + setup_view() { + this.setup_columns(); + } + + before_render() { + this.save_report_settings(); + } + + save_report_settings() { + frappe.model.user_settings.save(this.doctype, 'last_view', this.view_name); + + if (!this.report_name) { + this.save_view_user_settings({ + fields: this._fields, + filters: this.filter_area.get(), + order_by: this.sort_selector.get_sql_string(), + add_totals_row: this.add_totals_row + }); + } + } + + update_data(r) { + let data = r.message || {}; + data = frappe.utils.dict(data.keys, data.values); + + if (this.start === 0) { + this.data = data; + } else { + this.data = this.data.concat(data); + } + } + + render() { + if (this.datatable) { + this.datatable.refresh(this.get_data(this.data)); + return; + } + this.setup_datatable(this.data); + } + + on_update(data) { + if (this.doctype === data.doctype && data.name) { + // flash row when doc is updated by some other user + const flash_row = data.user !== frappe.session.user; + if (this.data.find(d => d.name === data.name)) { + // update existing + frappe.db.get_doc(data.doctype, data.name) + .then(doc => this.update_row(doc, flash_row)); + } else { + // refresh + this.refresh(); + } + } + } + + update_row(doc, flash_row) { + // update this.data + const data = this.data.find(d => d.name === doc.name); + const rowIndex = this.data.findIndex(d => d.name === doc.name); + if (!data) return; + + for (let fieldname in data) { + data[fieldname] = doc[fieldname]; + } + + const new_row = this.build_row(data); + this.datatable.refreshRow(new_row, rowIndex); + + // indicate row update + if (flash_row) { + const $row = this.$result.find(`.data-table-row[data-row-index="${rowIndex}"]`); + $row.addClass('row-update'); + setTimeout(() => $row.removeClass('row-update'), 500); + } + } + + setup_datatable(values) { + this.datatable = new DataTable(this.$result[0], { + data: this.get_data(values), + enableClusterize: true, + addCheckbox: this.can_delete, + takeAvailableSpace: true, + editing: this.get_editing_object.bind(this), + events: { + onRemoveColumn: (column) => { + this.remove_column_from_datatable(column); + }, + onSwitchColumn: (column1, column2) => { + this.switch_column(column1, column2); + } + }, + headerDropdown: [{ + label: __('Add Column'), + action: (datatabe_col) => { + let columns_in_picker = []; + const columns = this.get_columns_for_picker(); + + columns_in_picker = columns[this.doctype] + .filter(df => !this.is_column_added(df)) + .map(df => ({ + label: __(df.label), + value: df.fieldname + })); + + delete columns[this.doctype]; + + for (let cdt in columns) { + columns[cdt] + .filter(df => !this.is_column_added(df)) + .map(df => ({ + label: __(df.label) + ` (${cdt})`, + value: df.fieldname + ',' + cdt + })) + .forEach(df => columns_in_picker.push(df)); + } + + const d = new frappe.ui.Dialog({ + title: __('Add Column'), + fields: [ + { + label: __('Select Column'), + fieldname: 'column', + fieldtype: 'Autocomplete', + options: columns_in_picker + }, + { + label: __('Insert Column Before {0}', [datatabe_col.docfield.label.bold()]), + fieldname: 'insert_before', + fieldtype: 'Check' + } + ], + primary_action: ({ column, insert_before }) => { + + let doctype = this.doctype; + if (column.includes(',')) { + [column, doctype] = column.split(','); + } + + let index = datatabe_col.colIndex; + if (insert_before) { + index = index - 1; + } + + this.add_column_to_datatable(column, doctype, index); + d.hide(); + } + }); + + d.show(); + } + }] + }); + + } + + get_editing_object(colIndex, rowIndex, value, parent) { + const control = this.render_editing_input(colIndex, value, parent); + if (!control) return false; + + return { + initValue: (value) => { + control.set_focus(); + return control.set_value(value); + }, + setValue: (value) => { + const cell = this.datatable.getCell(colIndex, rowIndex); + let fieldname = this.datatable.getColumn(colIndex).docfield.fieldname; + let docname = cell.name; + + control.set_value(value); + return this.set_control_value(docname, fieldname, value); + }, + getValue: () => { + return control.get_value(); + } + }; + } + + set_control_value(docname, fieldname, value) { + this.last_updated_doc = docname; + return new Promise((resolve, reject) => { + frappe.db.set_value(this.doctype, docname, {[fieldname]: value}) + .then(r => { + if (r.message) { + resolve(); + } else { + reject(); + } + }) + .fail(reject); + }); + } + + render_editing_input(colIndex, value, parent) { + const col = this.datatable.getColumn(colIndex); + + // make control + const control = frappe.ui.form.make_control({ + df: col.docfield, + parent: parent, + render_input: true + }); + control.set_value(value); + control.toggle_label(false); + control.toggle_description(false); + + return control; + } + + is_editable(df, data) { + if (!df || data.docstatus !== 0) return false; + const is_standard_field = frappe.model.std_fields_list.includes(df.fieldname); + const can_edit = !( + is_standard_field + || df.read_only + || df.hidden + || !frappe.model.can_write(this.doctype) + ); + return can_edit; + } + + get_data(values) { + return { + columns: this.columns, + rows: this.build_rows(values) + }; + } + + set_fields() { + if (this.report_name) { + this._fields = this.report_doc.json._fields; + return; + } + + // get from user_settings + else if (this.view_user_settings.fields) { + this._fields = this.view_user_settings.fields; + return; + } + + // get fields from meta + this._fields = []; + const add_field = f => this._add_field(f); + + // default fields + [ + 'name', 'docstatus', + this.meta.title_field, + this.meta.image_field + ].map(add_field); + + // fields in_list_view or in_standard_filter + const fields = this.meta.fields.filter(df => { + return (df.in_list_view || df.in_standard_filter) + && frappe.perm.has_perm(this.doctype, df.permlevel, 'read') + && frappe.model.is_value_type(df.fieldtype) + && !df.report_hide; + }); + + fields.map(add_field); + + // currency fields + fields.filter( + df => df.fieldtype === 'Currency' && df.options + ).map(df => { + if (df.options.includes(':')) { + add_field(df.options.split(':')[1]); + } else { + add_field(df.options); + } + }); + + // fields in listview_settings + (this.settings.add_fields || []).map(add_field); + } + + build_fields() { + this._fields.push(['docstatus', this.doctype]); + super.build_fields(); + } + + add_column_to_datatable(fieldname, doctype, col_index) { + const field = [fieldname, doctype]; + this._fields.splice(col_index, 0, field); + + this.build_fields(); + this.setup_columns(); + + this.datatable.destroy(); + this.datatable = null; + this.refresh(); + } + + remove_column_from_datatable(column) { + const index = this._fields.findIndex(f => column.field === f[0]); + if (index === -1) return; + const field = this._fields[index]; + if (field[0] === 'name') { + frappe.throw(__('Cannot remove ID field')); + } + this._fields.splice(index, 1); + this.build_fields(); + this.setup_columns(); + this.refresh(); + } + + switch_column(col1, col2) { + const index1 = this._fields.findIndex(f => col1.field === f[0]); + const index2 = this._fields.findIndex(f => col2.field === f[0]); + const _fields = this._fields.slice(); + + let temp = _fields[index1]; + _fields[index1] = _fields[index2]; + _fields[index2] = temp; + + this._fields = _fields; + this.build_fields(); + this.setup_columns(); + this.save_report_settings(); + } + + get_columns_for_picker() { + let out = {}; + let doctype_fields = frappe.meta.get_docfields(this.doctype).filter(df => + !in_list(frappe.model.no_value_type, df.fieldtype) && + !df.report_hide && df.fieldname !== 'naming_series' && + !df.hidden + ); + + doctype_fields = [{ + label: __('ID'), + fieldname: 'name', + fieldtype: 'Data' + }].concat(doctype_fields); + + out[this.doctype] = doctype_fields; + + const table_fields = frappe.meta.get_table_fields(this.doctype) + .filter(df => !df.hidden); + + table_fields.forEach(df => { + const cdt = df.options; + const child_table_fields = + frappe.meta.get_docfields(cdt) + .filter(df => df.in_list_view); + + out[cdt] = child_table_fields; + }); + + return out; + } + + get_dialog_fields() { + const dialog_fields = []; + const columns = this.get_columns_for_picker(); + + dialog_fields.push({ + label: __(this.doctype), + fieldname: this.doctype, + fieldtype: 'MultiCheck', + columns: 2, + options: columns[this.doctype] + .map(df => ({ + label: __(df.label), + value: df.fieldname, + checked: this._fields.find(f => f[0] === df.fieldname) + })) + }); + + delete columns[this.doctype]; + + const table_fields = frappe.meta.get_table_fields(this.doctype) + .filter(df => !df.hidden); + + table_fields.forEach(df => { + const cdt = df.options; + + dialog_fields.push({ + label: __(df.label) + ` (${__(cdt)})`, + fieldname: df.options, + fieldtype: 'MultiCheck', + columns: 2, + options: columns[cdt] + .map(df => ({ + label: __(df.label), + value: df.fieldname, + checked: this._fields.find(f => f[0] === df.fieldname && f[1] === cdt) + })) + }); + }); + + return dialog_fields; + } + + is_column_added(df) { + return Boolean( + this._fields.find(f => f[0] === df.fieldname && df.parent === f[1]) + ); + } + + setup_columns() { + const hide_columns = ['docstatus']; + const fields = this._fields.filter(f => !hide_columns.includes(f[0])); + this.columns = fields.map(f => this.build_column(f)); + } + + build_column(c) { + let [fieldname, doctype] = c; + let docfield = frappe.meta.docfield_map[doctype || this.doctype][fieldname]; + + if (!docfield) { + docfield = frappe.model.get_std_field(fieldname); + + if (docfield) { + docfield.parent = this.doctype; + if (fieldname == "name") { + docfield.options = this.doctype; + } + } + } + if (!docfield) return; + + const title = __(docfield ? docfield.label : toTitle(fieldname)); + const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only; + + return { + id: fieldname, + field: fieldname, + docfield: docfield, + name: title, + content: title, // required by datatable + width: (docfield ? cint(docfield.width) : 120) || 120, + editable: editable + }; + } + + build_rows(data) { + const out = data.map(d => this.build_row(d)); + + if (this.add_totals_row) { + const totals_row = data.reduce((totals_row, d) => { + this.columns.forEach((col, i) => { + totals_row[i] = totals_row[i] || { + name: 'Totals Row', + content: '' + }; + + if (col.field in d && frappe.model.is_numeric_field(col.docfield)) { + + if (!totals_row[i].format) { + totals_row[i].format = value => frappe.format(value, col.docfield, { always_show_decimals: true }); + } + + totals_row[i].content = totals_row[i].content || 0; + totals_row[i].content += parseInt(d[col.field], 10); + } + }); + + return totals_row; + }, []); + + totals_row[0].content = __('Totals').bold(); + + out.push(totals_row); + } + + return out; + } + + build_row(d) { + return this.columns.map(col => { + if (col.field in d) { + const value = d[col.field]; + return { + name: d.name, + content: value, + editable: this.is_editable(col.docfield, d), + format: value => { + if (col.field === 'name') { + return frappe.utils.get_form_link(this.doctype, value, true); + } + return frappe.format(value, col.docfield, { always_show_decimals: true }); + } + }; + } + return { + content: '' + }; + }); + } + + get_checked_items(only_docnames) { + const indexes = this.datatable.rowmanager.getCheckedRows(); + const items = indexes.filter(i => i != undefined) + .map(i => this.data[i]); + + if (only_docnames) { + return items.map(d => d.name); + } + + return items; + } + + save_report(save_type) { + const _save_report = (name) => { + // callback + return frappe.call({ + method: 'frappe.desk.reportview.save_report', + args: { + name: name, + doctype: this.doctype, + json: JSON.stringify({ + filters: this.filter_area.get(), + _fields: this._fields, + order_by: this.sort_selector.get_sql_string(), + add_totals_row: this.add_totals_row + }) + }, + callback:(r) => { + if(r.exc) { + frappe.msgprint(__("Report was not saved (there were errors)")); + return; + } + if(r.message != this.report_name) { + frappe.set_route('List', this.doctype, 'Report', r.message); + } + } + }); + + }; + + if(this.report_name && save_type == "save") { + _save_report(this.report_name); + } else { + frappe.prompt({fieldname: 'name', label: __('New Report name'), reqd: 1, fieldtype: 'Data'}, (data) => { + _save_report(data.name); + }, __('Save As')); + } + } + + get_report_doc() { + return new Promise(resolve => { + frappe.model.with_doc('Report', this.report_name, () => { + resolve(frappe.get_doc('Report', this.report_name)); + }); + }); + } + + report_menu_items() { + let items = [ + { + label: __('Show Totals'), + action: () => { + this.add_totals_row = !this.add_totals_row; + this.save_view_user_settings({ add_totals_row: this.add_totals_row }); + this.datatable.refresh(this.get_data(this.data)); + } + }, + { + label: __('Print'), + action: () => { + frappe.ui.get_print_settings(false, (print_settings) => { + var title = __(this.doctype); + frappe.render_grid({ + title: title, + print_settings: print_settings, + columns: this.columns, + data: this.data + }); + }); + } + }, + { + label: __('Pick Columns'), + action: () => { + const d = new frappe.ui.Dialog({ + title: __('Pick Columns'), + fields: this.get_dialog_fields(), + primary_action: (values) => { + // doctype fields + let fields = values[this.doctype].map(f => [f, this.doctype]); + delete values[this.doctype]; + + // child table fields + for (let cdt in values) { + fields = fields.concat(values[cdt].map(f => [f, cdt])); + } + + // this._fields = this._fields.concat(fields); + this._fields = fields; + + this.build_fields(); + this.setup_columns(); + + this.datatable.destroy(); + this.datatable = null; + this.refresh(); + + d.hide(); + } + }); + + d.show(); + } + } + ]; + + if (frappe.model.can_export(this.doctype)) { + items.push({ + label: __('Export'), + action: () => { + const args = this.get_args(); + const selected_items = this.get_checked_items(true); + + frappe.prompt({ + fieldtype:"Select", label: __("Select File Type"), fieldname:"file_format_type", + options:"Excel\nCSV", default:"Excel", reqd: 1 + }, + (data) => { + args.cmd = 'frappe.desk.reportview.export_query'; + args.file_format_type = data.file_format_type; + + if(this.add_totals_row) { + args.add_totals_row = 1; + } + + if(selected_items.length > 0) { + args.selected_items = selected_items; + } + open_url_post(frappe.request.url, args); + }, + __("Export Report: {0}",[__(this.doctype)]), __("Download")); + } + }); + } + + items.push({ + label: __("Setup Auto Email"), + action: () => { + if(this.report_name) { + frappe.set_route('List', 'Auto Email Report', {'report' : this.report_name}); + } else { + frappe.msgprint(__('Please save the report first')); + } + } + }); + + // save buttons + if(frappe.user.is_report_manager()) { + items = items.concat([ + { label: __('Save'), action: () => this.save_report('save') }, + { label: __('Save As'), action: () => this.save_report('save_as') } + ]); + } + + // user permissions + if(this.report_name && frappe.model.can_set_user_permissions("Report")) { + items.push({ + label: __("User Permissions"), + action: () => { + const args = { + doctype: "Report", + name: this.report_name + }; + frappe.set_route('List', 'User Permission', args); + } + }); + } + + // add to desktop + items.push({ + label: __('Add to Desktop'), + action: () => { + frappe.add_to_desktop( + this.report_name || __('{0} Report', [this.doctype]), + this.doctype, this.report_name + ); + } + }); + + return items.map(i => Object.assign(i, { standard: true })); + } + +}; diff --git a/frappe/public/js/frappe/views/reports/reportview.js b/frappe/public/js/frappe/views/reports/reportview.js index 9e4b84bfaf..c44ba3bc77 100644 --- a/frappe/public/js/frappe/views/reports/reportview.js +++ b/frappe/public/js/frappe/views/reports/reportview.js @@ -54,889 +54,10 @@ frappe.views.ReportViewPage = Class.extend({ var module = locals.DocType[this.doctype].module; frappe.breadcrumbs.add(module, this.doctype); - this.parent.reportview = new frappe.views.ReportView({ + this.parent.reportview = new frappe.views.ReportView2({ doctype: this.doctype, docname: this.docname, parent: this.parent }); } }); - -frappe.views.ReportView = frappe.ui.BaseList.extend({ - init: function(opts) { - var me = this; - $.extend(this, opts); - this.can_delete = frappe.model.can_delete(me.doctype); - this.tab_name = '`tab'+this.doctype+'`'; - this.setup(); - }, - - setup: function() { - var me = this; - - this.add_totals_row = 0; - this.page = this.parent.page; - this.meta = frappe.get_meta(this.doctype); - this._body = $('
    ').appendTo(this.page.main); - this.page_title = __('Report')+ ': ' + (this.docname ? - __(this.doctype) + ' - ' + __(this.docname) : __(this.doctype)); - this.page.set_title(this.page_title); - this.init_user_settings(); - this.make({ - page: this.parent.page, - method: 'frappe.desk.reportview.get', - save_user_settings: true, - get_args: this.get_args, - parent: this._body, - start: 0, - show_filters: true, - allow_delete: true, - }); - - this.make_new_and_refresh(); - this.make_delete(); - this.make_column_picker(); - this.make_sorter(); - this.make_totals_row_button(); - this.setup_print(); - this.make_export(); - this.setup_auto_email(); - this.set_init_columns(); - this.make_save(); - this.make_user_permissions(); - this.set_tag_and_status_filter(); - this.setup_listview_settings(); - - // add to desktop - this.page.add_menu_item(__("Add to Desktop"), function() { - frappe.add_to_desktop(me.docname || __('{0} Report', [me.doctype]), me.doctype, me.docname); - }, true); - - }, - - make_new_and_refresh: function() { - var me = this; - this.page.set_primary_action(__("Refresh"), function() { - me.run(); - }); - - this.page.add_menu_item(__("New {0}", [this.doctype]), function() { - me.make_new_doc(me.doctype); - }, true); - - }, - - setup_auto_email: function() { - var me = this; - this.page.add_menu_item(__("Setup Auto Email"), function() { - if(me.docname) { - frappe.set_route('List', 'Auto Email Report', {'report' : me.docname}); - } else { - frappe.msgprint({message:__('Please save the report first'), indicator: 'red'}); - } - }, true); - }, - - set_init_columns: function() { - // pre-select mandatory columns - var me = this; - var columns = []; - if(this.user_settings.fields && !this.docname) { - this.user_settings.fields.forEach(function(field) { - var coldef = me.get_column_info_from_field(field); - if(!in_list(['_seen', '_comments', '_user_tags', '_assign', '_liked_by', 'docstatus'], coldef[0])) { - columns.push(coldef); - } - }); - } - if(!columns.length) { - var columns = [['name', this.doctype],]; - $.each(frappe.meta.docfield_list[this.doctype], function(i, df) { - if((df.in_standard_filter || df.in_list_view) && df.fieldname!='naming_series' - && !in_list(frappe.model.no_value_type, df.fieldtype) - && !df.report_hide) { - columns.push([df.fieldname, df.parent]); - } - }); - } - - this.set_columns(columns); - - this.page.footer.on('click', '.show-all-data', function() { - me.show_all_data = $(this).prop('checked'); - me.run(); - }) - }, - - set_columns: function(columns) { - this.columns = columns; - this.column_info = this.get_columns(); - this.refresh_footer(); - }, - - refresh_footer: function() { - var can_write = frappe.model.can_write(this.doctype); - var has_child_column = this.has_child_column(); - - this.page.footer.empty(); - - if(can_write || has_child_column) { - $(frappe.render_template('reportview_footer', { - has_child_column: has_child_column, - can_write: can_write, - show_all_data: this.show_all_data - })).appendTo(this.page.footer); - this.page.footer.removeClass('hide'); - } else { - this.page.footer.addClass('hide'); - } - }, - - // preset columns and filters from saved info - set_columns_and_filters: function(opts) { - var me = this; - this.filter_list.clear_filters(); - if(opts.columns) { - this.set_columns(opts.columns); - } - if(opts.filters) { - $.each(opts.filters, function(i, f) { - // f = [doctype, fieldname, condition, value] - var df = frappe.meta.get_docfield(f[0], f[1]); - if (df && df.fieldtype == "Check") { - var value = f[3] ? "Yes" : "No"; - } else { - var value = f[3]; - } - me.filter_list.add_filter(f[0], f[1], f[2], value); - }); - } - - if(opts.add_total_row) { - this.add_total_row = opts.add_total_row - } - - // first sort - if(opts.sort_by) this.sort_by_select.val(opts.sort_by); - if(opts.sort_order) this.sort_order_select.val(opts.sort_order); - - // second sort - if(opts.sort_by_next) this.sort_by_next_select.val(opts.sort_by_next); - if(opts.sort_order_next) this.sort_order_next_select.val(opts.sort_order_next); - - this.add_totals_row = cint(opts.add_totals_row); - }, - - set_route_filters: function() { - var me = this; - if(frappe.route_options) { - this.set_filters_from_route_options({clear_filters: this.docname ? false : true}); - return true; - } else if(this.user_settings - && this.user_settings.filters - && !this.docname - && (this.user_settings.updated_on != this.user_settings_updated_on)) { - // list settings (previous settings) - this.filter_list.clear_filters(); - $.each(this.user_settings.filters, function(i, f) { - me.filter_list.add_filter(f[0], f[1], f[2], f[3]); - }); - return true; - } - this.user_settings_updated_on = this.user_settings.updated_on; - }, - - setup_print: function() { - var me = this; - this.page.add_menu_item(__("Print"), function() { - frappe.ui.get_print_settings(false, function(print_settings) { - var title = __(me.docname || me.doctype); - frappe.render_grid({grid:me.grid, title:title, print_settings:print_settings}); - }) - - }, true); - }, - - // build args for query - get_args: function() { - let me = this; - let filters = this.filter_list? this.filter_list.get_filters(): []; - - return { - doctype: this.doctype, - fields: $.map(this.columns || [], function(v) { return me.get_full_column_name(v); }), - order_by: this.get_order_by(), - add_total_row: this.add_total_row, - filters: filters, - save_user_settings_fields: 1, - with_childnames: 1, - file_format_type: this.file_format_type - } - }, - - get_order_by: function() { - var order_by = []; - - // first - var sort_by_select = this.get_selected_table_and_column(this.sort_by_select); - if (sort_by_select) { - order_by.push(sort_by_select + " " + this.sort_order_select.val()); - } - - // second - if(this.sort_by_next_select && this.sort_by_next_select.val()) { - order_by.push(this.get_selected_table_and_column(this.sort_by_next_select) - + ' ' + this.sort_order_next_select.val()); - } - - return order_by.join(", "); - }, - - get_selected_table_and_column: function(select) { - if(!select) { - return - } - - return select.selected_doctype ? - this.get_full_column_name([select.selected_fieldname, select.selected_doctype]) : ""; - }, - - // get table_name.column_name - get_full_column_name: function(v) { - if(!v) return; - return (v[1] ? ('`tab' + v[1] + '`') : this.tab_name) + '.`' + v[0] + '`'; - }, - - get_column_info_from_field: function(t) { - if(t.indexOf('.')===-1) { - return [strip(t, '`'), this.doctype]; - } else { - var parts = t.split('.'); - return [strip(parts[1], '`'), strip(parts[0], '`').substr(3)]; - } - }, - - // build columns for slickgrid - build_columns: function() { - var me = this; - return $.map(this.columns, function(c) { - var docfield = frappe.meta.docfield_map[c[1] || me.doctype][c[0]]; - if(!docfield) { - var docfield = frappe.model.get_std_field(c[0]); - if(docfield) { - docfield.parent = me.doctype; - if(c[0]=="name") { - docfield.options = me.doctype; - } - } - } - if(!docfield) return; - - let coldef = { - id: c[0], - field: c[0], - docfield: docfield, - name: __(docfield ? docfield.label : toTitle(c[0])), - width: (docfield ? cint(docfield.width) : 120) || 120, - formatter: function(row, cell, value, columnDef, dataContext, for_print) { - var docfield = columnDef.docfield; - docfield.fieldtype = { - "_user_tags": "Tag", - "_comments": "Comment", - "_assign": "Assign", - "_liked_by": "LikedBy", - }[docfield.fieldname] || docfield.fieldtype; - - if(docfield.fieldtype==="Link" && docfield.fieldname!=="name") { - - // make a copy of docfield for reportview - // as it needs to add a link_onclick property - if(!columnDef.report_docfield) { - columnDef.report_docfield = copy_dict(docfield); - } - docfield = columnDef.report_docfield; - - docfield.link_onclick = - repl('frappe.container.page.reportview.filter_or_open("%(parent)s", "%(fieldname)s", "%(value)s")', - {parent: docfield.parent, fieldname:docfield.fieldname, value:value}); - } - return frappe.format(value, docfield, {for_print: for_print, always_show_decimals: true}, dataContext); - } - } - return coldef; - }); - }, - - filter_or_open: function(parent, fieldname, value) { - // set filter on click, if filter is set, open the document - var filter_set = false; - this.filter_list.get_filters().forEach(function(f) { - if(f[1]===fieldname) { - filter_set = true; - } - }); - - if(!filter_set) { - this.set_filter(fieldname, value, false, false, parent); - } else { - var df = frappe.meta.get_docfield(parent, fieldname); - if(df.fieldtype==='Link') { - frappe.set_route('Form', df.options, value); - } - } - }, - - // render data - render_view: function() { - var me = this; - var data = this.get_unique_data(this.column_info); - - this.set_totals_row(data); - - // add sr in data - $.each(data, function(i, v) { - // add index - v._idx = i+1; - v.id = v._idx; - }); - - var options = { - enableCellNavigation: true, - enableColumnReorder: false, - }; - - if(this.slickgrid_options) { - $.extend(options, this.slickgrid_options); - } - - this.dataView = new Slick.Data.DataView(); - this.set_data(data); - - var grid_wrapper = this.wrapper.find('.result-list').addClass("slick-wrapper"); - - // set height if not auto - if(!options.autoHeight) - grid_wrapper.css('height', '500px'); - - this.grid = new Slick.Grid(grid_wrapper - .get(0), this.dataView, - this.column_info, options); - - if (!frappe.dom.is_touchscreen()) { - this.grid.setSelectionModel(new Slick.CellSelectionModel()); - this.grid.registerPlugin(new Slick.CellExternalCopyManager({ - dataItemColumnValueExtractor: function(item, columnDef, value) { - return item[columnDef.field]; - } - })); - } - - frappe.slickgrid_tools.add_property_setter_on_resize(this.grid); - if(this.start!=0 && !options.autoHeight) { - this.grid.scrollRowIntoView(data.length-1); - } - - this.grid.onDblClick.subscribe(function(e, args) { - var row = me.dataView.getItem(args.row); - var cell = me.grid.getColumns()[args.cell]; - me.edit_cell(row, cell.docfield); - }); - - this.dataView.onRowsChanged.subscribe(function (e, args) { - me.grid.invalidateRows(args.rows); - me.grid.render(); - }); - - this.grid.onHeaderClick.subscribe(function(e, args) { - if(e.target.className === "slick-resizable-handle") - return; - - - var df = args.column.docfield, - sort_by = df.parent + "." + df.fieldname; - - if(sort_by===me.sort_by_select.val()) { - me.sort_order_select.val(me.sort_order_select.val()==="asc" ? "desc" : "asc"); - } else { - me.sort_by_select.val(df.parent + "." + df.fieldname); - me.sort_order_select.val("asc"); - } - - me.run(); - }); - }, - - has_child_column: function() { - var me = this; - return this.column_info.some(function(c) { - return c.docfield && c.docfield.parent !== me.doctype; - }); - }, - - get_unique_data: function(columns) { - // if child columns are selected, show parent data only once - let has_child_column = this.has_child_column(); - - var data = [], prev_row = null; - this.data.forEach((d) => { - if (this.show_all_data || !has_child_column) { - data.push(d); - } else if (prev_row && d.name == prev_row.name) { - var new_row = {}; - columns.forEach((c) => { - if(!c.docfield || c.docfield.parent!==this.doctype) { - var val = d[c.field]; - // add child table row name for update - if(c.docfield && c.docfield.parent!==this.doctype) { - new_row[c.docfield.parent+":name"] = d[c.docfield.parent+":name"]; - } - } else { - var val = ''; - new_row.__is_repeat = true; - } - new_row[c.field] = val; - }); - data.push(new_row); - } else { - data.push(d); - } - prev_row = d; - }); - return data; - }, - - edit_cell: function(row, docfield) { - if(!docfield || docfield.fieldname !== "idx" - && frappe.model.std_fields_list.indexOf(docfield.fieldname)!==-1) { - return; - } else if(frappe.boot.user.can_write.indexOf(this.doctype)===-1) { - frappe.throw({message:__("No permission to edit"), title:__('Not Permitted')}); - } - var me = this; - var d = new frappe.ui.Dialog({ - title: __("Edit") + " " + __(docfield.label), - fields: [docfield], - primary_action_label: __("Update"), - primary_action: function() { - me.update_value(docfield, d, row); - } - }); - d.set_input(docfield.fieldname, row[docfield.fieldname]); - - // Show dialog if field is editable and not hidden - if (d.fields_list[0].disp_status != "Write") d.hide(); - else d.show(); - }, - - update_value: function(docfield, dialog, row) { - // update value on the serverside - var me = this; - var args = { - doctype: docfield.parent, - name: row[docfield.parent===me.doctype ? "name" : docfield.parent+":name"], - fieldname: docfield.fieldname, - value: dialog.get_value(docfield.fieldname) - }; - - if (!args.name) { - frappe.throw(__("ID field is required to edit values using Report. Please select the ID field using the Column Picker")); - } - - frappe.call({ - method: "frappe.client.set_value", - args: args, - callback: function(r) { - if(!r.exc) { - dialog.hide(); - var doc = r.message; - $.each(me.dataView.getItems(), function(i, item) { - if (item.name === doc.name) { - var new_item = $.extend({}, item); - $.each(frappe.model.get_all_docs(doc), function(i, d) { - // find the document of the current updated record - // from locals (which is synced in the response) - var name = item[d.doctype + ":name"]; - if(!name) name = item.name; - - if(name===d.name) { - for(var k in d) { - var v = d[k]; - if(frappe.model.std_fields_list.indexOf(k)===-1 - && item[k]!==undefined) { - new_item[k] = v; - } - } - } - }); - me.dataView.updateItem(item.id, new_item); - } - }); - } - } - }); - }, - - set_data: function(data) { - this.dataView.beginUpdate(); - this.dataView.setItems(data); - this.dataView.endUpdate(); - }, - - get_columns: function() { - var std_columns = [{id:'_idx', field:'_idx', name: 'Sr.', width: 40, maxWidth: 40}]; - if(this.can_delete) { - std_columns = std_columns.concat([{ - id:'_check', field:'_check', name: "", width: 30, maxWidth: 30, - formatter: function(row, cell, value, columnDef, dataContext) { - return repl("", { - row: row, - checked: (dataContext.selected ? "checked=\"checked\"" : "") - }); - } - }]); - } - return std_columns.concat(this.build_columns()); - }, - - // setup column picker - make_column_picker: function() { - var me = this; - this.column_picker = new frappe.ui.ColumnPicker(this); - this.page.add_inner_button(__('Pick Columns'), function() { - me.column_picker.show(me.columns); - }); - }, - - make_totals_row_button: function() { - var me = this; - - this.page.add_inner_button(__('Show Totals'), function() { - me.add_totals_row = !!!me.add_totals_row; - me.render_view(); - }); - }, - - set_totals_row: function(data) { - if(this.add_totals_row) { - var totals_row = {_totals_row: 1}; - if(data.length) { - data.forEach(function(row, ri) { - $.each(row, function(key, value) { - if($.isNumeric(value)) { - totals_row[key] = (totals_row[key] || 0) + value; - } - }); - }); - } - data.push(totals_row); - } - }, - - set_tag_and_status_filter: function() { - var me = this; - this.wrapper.find('.result-list').on("click", ".label-info", function() { - if($(this).attr("data-label")) { - me.set_filter("_user_tags", $(this).attr("data-label")); - } - }); - this.wrapper.find('.result-list').on("click", "[data-workflow-state]", function() { - if($(this).attr("data-workflow-state")) { - me.set_filter(me.state_fieldname, - $(this).attr("data-workflow-state")); - } - }); - }, - - // setup sorter - make_sorter: function() { - var me = this; - this.sort_dialog = new frappe.ui.Dialog({title:__('Sorting Preferences')}); - $(this.sort_dialog.body).html('

    '+__('Sort By')+'

    \ -
    \ -
    \ -

    '+__('Then By (optional)')+'

    \ -
    \ -

    \ -
    '); - - // first - this.sort_by_select = new frappe.ui.FieldSelect({ - parent: $(this.sort_dialog.body).find('.sort-column'), - doctype: this.doctype - }); - this.sort_by_select.$select.css('width', '60%'); - this.sort_order_select = $(this.sort_dialog.body).find('.sort-order'); - - // second - this.sort_by_next_select = new frappe.ui.FieldSelect({ - parent: $(this.sort_dialog.body).find('.sort-column-1'), - doctype: this.doctype, - with_blank: true - }); - this.sort_by_next_select.$select.css('width', '60%'); - this.sort_order_next_select = $(this.sort_dialog.body).find('.sort-order-1'); - - // initial values - this.sort_by_select.set_value(this.doctype, 'modified'); - this.sort_order_select.val('desc'); - - this.sort_by_next_select.clear(); - this.sort_order_next_select.val('desc'); - - // button actions - this.page.add_inner_button(__('Sort Order'), function() { - me.sort_dialog.show(); - }); - - $(this.sort_dialog.body).find('.btn-primary').click(function() { - me.sort_dialog.hide(); - me.run(); - }); - }, - - // setup export - make_export: function() { - var me = this; - if(!frappe.model.can_export(this.doctype)) { - return; - } - var export_btn = this.page.add_menu_item(__('Export'), function() { - var args = me.get_args(); - var selected_items = me.get_checked_items() - frappe.prompt({fieldtype:"Select", label: __("Select File Type"), fieldname:"file_format_type", - options:"Excel\nCSV", default:"Excel", reqd: 1}, - function(data) { - args.cmd = 'frappe.desk.reportview.export_query'; - args.file_format_type = data.file_format_type; - - if(me.add_totals_row) { - args.add_totals_row = 1; - } - - if(selected_items.length >= 1) { - args.selected_items = $.map(selected_items, function(d) { return d.name; }); - } - open_url_post(frappe.request.url, args); - - }, __("Export Report: {0}",[__(me.doctype)]), __("Download")); - - }, true); - }, - - - // save - make_save: function() { - var me = this; - if(frappe.user.is_report_manager()) { - this.page.add_menu_item(__('Save'), function() { me.save_report('save') }, true); - this.page.add_menu_item(__('Save As'), function() { me.save_report('save_as') }, true); - } - }, - - save_report: function(save_type) { - var me = this; - - var _save_report = function(name) { - // callback - return frappe.call({ - method: 'frappe.desk.reportview.save_report', - args: { - name: name, - doctype: me.doctype, - json: JSON.stringify({ - filters: me.filter_list.get_filters(), - columns: me.columns, - sort_by: me.sort_by_select.val(), - sort_order: me.sort_order_select.val(), - sort_by_next: me.sort_by_next_select.val(), - sort_order_next: me.sort_order_next_select.val(), - add_totals_row: me.add_totals_row - }) - }, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("Report was not saved (there were errors)")); - return; - } - if(r.message != me.docname) - frappe.set_route('Report', me.doctype, r.message); - } - }); - - } - - if(me.docname && save_type == "save") { - _save_report(me.docname); - } else { - frappe.prompt({fieldname: 'name', label: __('New Report name'), reqd: 1, fieldtype: 'Data'}, function(data) { - _save_report(data.name); - }, __('Save As')); - } - - }, - - make_delete: function() { - var me = this; - if(this.can_delete) { - $(this.parent).on("click", "input[type='checkbox'][data-row]", function() { - me.data[$(this).attr("data-row")].selected - = this.checked ? true : false; - }); - - this.page.add_menu_item(__("Delete"), function() { - var delete_list = $.map(me.get_checked_items(), function(d) { return d.name; }); - if(!delete_list.length) - return; - if(frappe.confirm(__("This is PERMANENT action and you cannot undo. Continue?"), - function() { - return frappe.call({ - method: 'frappe.desk.reportview.delete_items', - args: { - items: delete_list, - doctype: me.doctype - }, - callback: function() { - me.refresh(); - } - }); - })); - - }, true); - } - }, - - make_user_permissions: function() { - var me = this; - if(this.docname && frappe.model.can_set_user_permissions("Report")) { - this.page.add_menu_item(__("User Permissions"), function() { - frappe.route_options = { - doctype: "Report", - name: me.docname - }; - frappe.set_route('List', 'User Permission'); - }, true); - } - }, - - setup_listview_settings: function() { - if(frappe.listview_settings[this.doctype] && frappe.listview_settings[this.doctype].onload) { - frappe.listview_settings[this.doctype].onload(this); - } - }, - - get_checked_items: function() { - var me = this; - var selected_records = [] - - $.each(me.data, function(i, d) { - if(d.selected && d.name) { - selected_records.push(d); - } - }); - - return selected_records - } -}); - -frappe.ui.ColumnPicker = Class.extend({ - init: function(list) { - this.list = list; - this.doctype = list.doctype; - }, - clear: function() { - this.columns = []; - $(this.dialog.body).html('
    '+__("Drag to sort columns")+'
    \ -
    \ -
    '); - - }, - show: function(columns) { - var me = this; - if(!this.dialog) { - this.dialog = new frappe.ui.Dialog({ - title: __("Pick Columns"), - width: '400', - primary_action_label: __("Update"), - primary_action: function() { - me.update_column_selection(); - } - }); - this.dialog.$wrapper.addClass("column-picker-dialog"); - } - - this.clear(); - - this.column_list = $(this.dialog.body).find('.column-list'); - - // show existing - $.each(columns, function(i, c) { - me.add_column(c); - }); - - new Sortable(this.column_list.get(0), { - //handle: '.sortable-handle', - filter: 'input', - draggable: '.column-list-item', - chosenClass: 'sortable-chosen', - dragClass: 'sortable-chosen', - onUpdate: function(event) { - me.columns = []; - $.each($(me.dialog.body).find('.column-list .column-list-item'), - function(i, ele) { - me.columns.push($(ele).data("fieldselect")) - }); - } - }); - - // add column - $(this.dialog.body).find('.btn-add').click(function() { - me.add_column(['name']); - }); - - this.dialog.show(); - }, - add_column: function(c) { - if(!c) return; - var me = this; - - var w = $('
    \ -
    \ -
    \ -
    \ - \ -
    ') - .appendTo(this.column_list); - - var fieldselect = new frappe.ui.FieldSelect({parent:w.find('.col-xs-10'), doctype:this.doctype}); - fieldselect.val((c[1] || this.doctype) + "." + c[0]); - - w.data("fieldselect", fieldselect); - - w.find('.close').data("fieldselect", fieldselect) - .click(function() { - delete me.columns[me.columns.indexOf($(this).data('fieldselect'))]; - $(this).parents('.column-list-item').remove(); - }); - - this.columns.push(fieldselect); - }, - update_column_selection: function() { - this.dialog.hide(); - // selected columns as list of [column_name, table_name] - var columns = $.map(this.columns, function(v) { - return (v && v.selected_fieldname && v.selected_doctype) - ? [[v.selected_fieldname, v.selected_doctype]] - : null; - }); - - this.list.set_columns(columns); - this.list.run(); - } -}); diff --git a/frappe/public/js/lib/clusterize.min.js b/frappe/public/js/lib/clusterize.min.js new file mode 100644 index 0000000000..36c2ecba3c --- /dev/null +++ b/frappe/public/js/lib/clusterize.min.js @@ -0,0 +1,16 @@ +/*! Clusterize.js - v0.17.6 - 2017-03-05 +* http://NeXTs.github.com/Clusterize.js/ +* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */ + +;(function(q,n){"undefined"!=typeof module?module.exports=n():"function"==typeof define&&"object"==typeof define.amd?define(n):this[q]=n()})("Clusterize",function(){function q(b,a,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)}function n(b,a,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)}function r(b){return"[object Array]"===Object.prototype.toString.call(b)}function m(b,a){return window.getComputedStyle?window.getComputedStyle(a)[b]: + a.currentStyle[b]}var l=function(){for(var b=3,a=document.createElement("b"),c=a.all||[];a.innerHTML="\x3c!--[if gt IE "+ ++b+"]>=l&&!c.tag&&(c.tag=b[0].match(/<([^>\s/]*)/)[1].toLowerCase()),1>=this.content_elem.children.length&&(a.data=this.html(b[0]+b[0]+b[0])),c.tag||(c.tag=this.content_elem.children[0].tagName.toLowerCase()), + this.getRowsHeight(b))},getRowsHeight:function(b){var a=this.options,c=a.item_height;a.cluster_height=0;if(b.length){b=this.content_elem.children;var d=b[Math.floor(b.length/2)];a.item_height=d.offsetHeight;"tr"==a.tag&&"collapse"!=m("borderCollapse",this.content_elem)&&(a.item_height+=parseInt(m("borderSpacing",this.content_elem),10)||0);"tr"!=a.tag&&(b=parseInt(m("marginTop",d),10)||0,d=parseInt(m("marginBottom",d),10)||0,a.item_height+=Math.max(b,d));a.block_height=a.item_height*a.rows_in_block; + a.rows_in_cluster=a.blocks_in_cluster*a.rows_in_block;a.cluster_height=a.blocks_in_cluster*a.block_height;return c!=a.item_height}},getClusterNum:function(){this.options.scroll_top=this.scroll_elem.scrollTop;return Math.floor(this.options.scroll_top/(this.options.cluster_height-this.options.block_height))||0},generateEmptyRow:function(){var b=this.options;if(!b.tag||!b.show_no_data_row)return[];var a=document.createElement(b.tag),c=document.createTextNode(b.no_data_text),d;a.className=b.no_data_class; + "tr"==b.tag&&(d=document.createElement("td"),d.colSpan=100,d.appendChild(c));a.appendChild(d||c);return[a.outerHTML]},generate:function(b,a){var c=this.options,d=b.length;if(de&&g++;f=l&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML=""+b+"
    ";b=a.lastChild;)a.removeChild(b);for(c=this.getChildNodes(c.firstChild.firstChild);c.length;)a.appendChild(c.shift())}else a.innerHTML=b},getChildNodes:function(b){b=b.children;for(var a=[],c=0,d=b.length;c=s&&a>=u&&r<=c&&i<=d},o.scrollTop=function(e,t){function n(){o<=t&&(o+=t/8,requestAnimationFrame(n),e.scrollTop=o)}var o=0;n()},e.exports=t.default},function(e,t,n){"use strict";function o(e){return e.replace(/([A-Z])/g,function(e){return"-"+e[0].toLowerCase()})}function a(e){return Object.keys(e).map(function(t){var n=o(t),a=e[t];return void 0===a?"":"data-"+n+'="'+a+'" '}).join("").trim()}function r(e,t){return void 0!==e?e:t}function i(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}function l(e){var t="";for(var n in e)e.hasOwnProperty(n)&&(t+=n+": "+e[n]+"; ");return t.trim()}function s(e,t){return e+" { "+l(t)+" }"}function u(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",o=i(e)+" {([^}]*)}",a=new RegExp(o,"g");if(n&&n.match(a)){for(var r in t)!function(o){var r=t[o],l=new RegExp(i(o)+":([^;]*);");n=n.replace(a,function(t,n){return n.match(l)&&(n=n.replace(l,function(e,t){return o+": "+r+";"})),n=n.trim(),e+" { "+n+" }"})}(r);return n}return""+n+s(e,t)}function c(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=i(e)+" {([^}]*)}",o=new RegExp(n,"g"),a=t;return t&&t.match(o)&&(a=t.replace(o,"")),a.trim()}function d(e){var t=document.createElement("textarea");t.style.position="fixed",t.style.top=0,t.style.left=0,t.style.width="2em",t.style.height="2em",t.style.padding=0,t.style.border="none",t.style.outline="none",t.style.boxShadow="none",t.style.background="transparent",t.value=e,document.body.appendChild(t),t.select();try{document.execCommand("copy")}catch(e){console.log("Oops, unable to copy")}document.body.removeChild(t)}function f(e){return!isNaN(e)}function h(e,t,n){var o,a,r,i=null,l=0;n||(n={});var s=function(){l=!1===n.leading?0:Date.now(),i=null,r=e.apply(o,a),i||(o=a=null)};return function(){var u=Date.now();l||!1!==n.leading||(l=u);var c=t-(u-l);return o=this,a=arguments,c<=0||c>t?(i&&(clearTimeout(i),i=null),l=u,r=e.apply(o,a),i||(o=a=null)):i||!1===n.trailing||(i=setTimeout(s,c)),r}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return function(){for(var n=arguments.length,o=Array(n),a=0;a\n "+e.map(u.getCellHTML).join("")+"\n \n "}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],n=this.getRow$(e);if(n){if(!t&&this.bodyScrollable.classList.contains("row-highlight-all"))return void n.classList.add("row-unhighlight");t&&n.classList.contains("row-unhighlight")&&n.classList.remove("row-unhighlight"),this._highlightedRows=this._highlightedRows||{},t?(n.classList.add("row-highlight"),this._highlightedRows[e]=n):(n.classList.remove("row-highlight"),delete this._highlightedRows[e])}}},{key:"highlightAll",value:function(){if(arguments.length>0&&void 0!==arguments[0]&&!arguments[0]){this.bodyScrollable.classList.remove("row-highlight-all");for(var e in this._highlightedRows)this._highlightedRows[e].classList.remove("row-highlight");this._highlightedRows={}}else this.bodyScrollable.classList.add("row-highlight-all")}},{key:"getRow$",value:function(e){return(0,l.default)('.data-table-row[data-row-index="'+e+'"]',this.bodyScrollable)}},{key:"getTotalRows",value:function(){return this.datamanager.getRowCount()}},{key:"getFirstRowIndex",value:function(){return 0}},{key:"getLastRowIndex",value:function(){return this.datamanager.getRowCount()-1}},{key:"scrollToRow",value:function(e){var t=this.getRow$(e);if(!l.default.inViewport(t,this.bodyScrollable)){var n=t.getBoundingClientRect(),o=n.top,a=n.height,r=this.bodyScrollable.getBoundingClientRect(),i=r.top,s=void 0;o\n "+l(e)+"\n \n "}function l(e){var t=e.isHeader,n=!t&&!1!==e.editable,o=n?'\n
    \n ':"",a=t&&!1!==e.sortable,r=a?'':"",i=t&&!1!==e.resizable,l=i?'':"",s=t&&!1!==e.dropdown,u=s?'
    '+(0,b.getDropdownHTML)()+"
    ":"";return'\n
    \n '+(e.format?e.format(e.content):e.content)+"\n "+r+"\n "+l+"\n "+u+"\n
    \n "+o+"\n "}function s(){return'\n
    \n '}function u(e,t){return'.data-table-col[data-col-index="'+e+'"][data-row-index="'+t+'"]'}Object.defineProperty(t,"__esModule",{value:!0});var c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},d=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.skipClearSelection,o=void 0===n?0:n;if(e&&e!==this.$editingCell){var a=v.default.data(e),r=a.colIndex;if(!a.isHeader){!1!==this.columnmanager.getColumn(r).focusable&&(this.deactivateEditing(),o||this.clearSelection(),this.$focusedCell&&this.$focusedCell.classList.remove("selected"),this.$focusedCell=e,e.classList.add("selected"),this.highlightRowColumnHeader(e))}}}},{key:"highlightRowColumnHeader",value:function(e){var t=v.default.data(e),n=t.colIndex,o=t.rowIndex,a=this.columnmanager.getSerialColumnIndex(),r='.data-table-header .data-table-col[data-col-index="'+n+'"]',i='.data-table-col[data-row-index="'+o+'"][data-col-index="'+a+'"]';this.lastHeaders&&v.default.removeStyle(this.lastHeaders,"backgroundColor");var l=(0,v.default)(r,this.wrapper),s=(0,v.default)(i,this.wrapper);v.default.style([l,s],{backgroundColor:"#f5f7fa"}),this.lastHeaders=[l,s]}},{key:"selectAreaOnClusterChanged",value:function(){if(this.$focusedCell&&this.$selectionCursor){var e=v.default.data(this.$selectionCursor),t=e.colIndex,n=e.rowIndex,o=this.getCell$(t,n);if(o&&o!==this.$selectionCursor){var a=v.default.data(this.$focusedCell);this.$focusedCell=this.getCell$(a.colIndex,a.rowIndex),this.selectArea(o)}}}},{key:"focusCellOnClusterChanged",value:function(){if(this.$focusedCell){var e=v.default.data(this.$focusedCell),t=e.colIndex,n=e.rowIndex,o=this.getCell$(t,n);o&&this.focusCell(o,{skipClearSelection:1})}}},{key:"selectArea",value:function(e){this.$focusedCell&&this._selectArea(this.$focusedCell,e)&&(this.$selectionCursor=e)}},{key:"_selectArea",value:function(e,t){var n=this;if(e===t)return!1;var o=this.getCellsInRange(e,t);return!!o&&(this.clearSelection(),o.map(function(e){return n.getCell$.apply(n,a(e))}).map(function(e){return e.classList.add("highlight")}),!0)}},{key:"getCellsInRange",value:function(e,t){var n=void 0,o=void 0,a=void 0,r=void 0;if("number"==typeof e){var i=Array.prototype.slice.call(arguments);n=i[0],o=i[1],a=i[2],r=i[3]}else if("object"===(void 0===e?"undefined":c(e))){if(!e||!t)return!1;var l=v.default.data(e),s=v.default.data(t);n=l.colIndex,o=l.rowIndex,a=s.colIndex,r=s.rowIndex}if(o>r){var u=[r,o];o=u[0],r=u[1]}if(n>a){var d=[a,n];n=d[0],a=d[1]}if(this.isStandardCell(n)||this.isStandardCell(a))return!1;for(var f=[],h=n,p=o,g=[];p<=r;)g.push(p),p++;return g.map(function(e){for(;h<=a;)f.push([h,e]),h++;h=n}),f}},{key:"clearSelection",value:function(){v.default.each(".data-table-col.highlight",this.bodyScrollable).map(function(e){return e.classList.remove("highlight")}),this.$selectionCursor=null}},{key:"getSelectionCursor",value:function(){return this.$selectionCursor||this.$focusedCell}},{key:"activateEditing",value:function(e){var t=v.default.data(e),n=t.rowIndex,o=t.colIndex,a=this.columnmanager.getColumn(o);if(!a||!1!==a.editable){var r=this.getCell(o,n);if(!r||!1!==r.editable){if(this.$editingCell){var i=v.default.data(this.$editingCell),l=i._rowIndex,s=i._colIndex;if(n===l&&o===s)return}this.$editingCell=e,e.classList.add("editing");var u=(0,v.default)(".edit-cell",e);u.innerHTML="";var c=this.getEditingObject(o,n,r.content,u);c&&(this.currentCellEditing=c,c.initValue(r.content))}}}},{key:"deactivateEditing",value:function(){this.$editingCell&&(this.$editingCell.classList.remove("editing"),this.$editingCell=null)}},{key:"getEditingObject",value:function(e,t,n,o){if(this.options.editing)return this.options.editing(e,t,n,o);var a=v.default.create("input",{type:"text",inside:o});return{initValue:function(e){a.focus(),a.value=e},getValue:function(){return a.value},setValue:function(e){a.value=e}}}},{key:"submitEditing",value:function(e){var t=this,n=v.default.data(e),o=n.rowIndex,a=n.colIndex;if(e){var r=this.currentCellEditing;if(r){var i=r.getValue(),l=r.setValue(i),s=this.getCell(a,o).content;this.updateCell(a,o,i),l&&l.then&&l.catch(function(e){console.log(e),t.updateCell(a,o,s)})}}this.currentCellEditing=null}},{key:"copyCellContents",value:function(e,t){var n=this;if(!t&&e){var o=v.default.data(e),r=o.colIndex,i=o.rowIndex,l=this.getCell(r,i);return void(0,f.copyTextToClipboard)(l.content)}var s=this.getCellsInRange(e,t);if(s){var u=s.map(function(e){return n.getCell.apply(n,a(e))}).reduce(function(e,t){var n=t.rowIndex;return e[n]=e[n]||[],e[n].push(t.content),e},[]).map(function(e){return e.join("\t")}).join("\n");(0,f.copyTextToClipboard)(u)}}},{key:"updateCell",value:function(e,t,n){var o=this.datamanager.updateCell(e,t,n);this.refreshCell(o)}},{key:"refreshCell",value:function(e){(0,v.default)(u(e.colIndex,e.rowIndex),this.bodyScrollable).innerHTML=l(e)}},{key:"isStandardCell",value:function(e){return e div",function(e,n){var a=l.default.closest(".data-table-col",n),r=l.default.data(n),i=r.index,s=l.default.data(a),u=s.colIndex,c=o[i].action;c&&c.call(t.instance,t.getColumn(u))})}},{key:"bindResizeColumn",value:function(){var e=this,t=!1,n=void 0,o=void 0,a=void 0;l.default.on(this.header,"mousedown",".data-table-col .column-resizer",function(r,i){document.body.classList.add("data-table-resize");var s=i.parentNode.parentNode;n=s;var u=l.default.data(n),c=u.colIndex,d=e.getColumn(c);d&&!1===d.resizable||(t=!0,o=l.default.style((0,l.default)(".content",n),"width"),a=r.pageX)}),l.default.on(document.body,"mouseup",function(o){if(document.body.classList.remove("data-table-resize"),n){t=!1;var a=l.default.data(n),r=a.colIndex,i=l.default.style((0,l.default)(".content",n),"width");e.setColumnWidth(r,i),e.instance.setBodyWidth(),n=null}}),l.default.on(document.body,"mousemove",function(r){if(t){var i=o+(r.pageX-a),s=l.default.data(n),u=s.colIndex;e.getColumnMinWidth(u)>i||e.setColumnHeaderWidth(u,i)}})}},{key:"bindMoveColumn",value:function(){var e=this,t=function(){if((0,l.default)(".data-table-col",e.header)){var t=(0,l.default)(".data-table-row",e.header);l.default.on(document,"drag",".data-table-col",(0,d.throttle)(function(e,t){e.offsetY>200?t.classList.add("remove-column"):setTimeout(function(){t.classList.remove("remove-column")},10)})),e.sortable=u.default.create(t,{onEnd:function(t){var n=t.oldIndex,o=t.newIndex,a=t.item;+l.default.data(a).colIndex!==o&&e.switchColumn(n,o)},preventOnFilter:!1,filter:".column-resizer, .data-table-dropdown",animation:150})}};l.default.on(document.body,"mousemove",t)}},{key:"bindSortColumn",value:function(){var e=this;l.default.on(this.header,"click",".data-table-col .column-title",function(t,n){var o=n.closest(".data-table-col"),a=l.default.data(o),r=a.colIndex,i=a.sortOrder;i=(0,d.getDefault)(i,"none");var s=e.getColumn(r);if(!s||!1!==s.sortable){(0,l.default)(".sort-indicator",e.header).textContent="",l.default.each(".data-table-col",e.header).map(function(e){l.default.data(e,{sortOrder:"none"})});var u=void 0,c=void 0;"none"===i?(u="asc",c="▲"):"asc"===i?(u="desc",c="▼"):"desc"===i&&(u="none",c=""),l.default.data(o,{sortOrder:u}),(0,l.default)(".sort-indicator",o).textContent=c,e.sortColumn(r,u)}})}},{key:"sortColumn",value:function(e,t){var n=this;this.instance.freeze(),this.sortRows(e,t).then(function(){return n.refreshHeader(),n.rowmanager.refreshRows()}).then(function(){return n.instance.unfreeze()}).then(function(){n.fireEvent("onSortColumn",n.getColumn(e))})}},{key:"removeColumn",value:function(e){var t=this,n=this.getColumn(e);this.instance.freeze(),this.datamanager.removeColumn(e).then(function(){return t.refreshHeader(),t.rowmanager.refreshRows()}).then(function(){return t.instance.unfreeze()}).then(function(){t.fireEvent("onRemoveColumn",n)})}},{key:"switchColumn",value:function(e,t){var n=this;this.instance.freeze(),this.datamanager.switchColumn(e,t).then(function(){return n.refreshHeader(),n.rowmanager.refreshRows()}).then(function(){n.setColumnWidth(e),n.setColumnWidth(t),n.instance.unfreeze()}).then(function(){n.fireEvent("onSwitchColumn",n.getColumn(e),n.getColumn(t))})}},{key:"setDimensions",value:function(){this.setHeaderStyle(),this.setupMinWidth(),this.setNaturalColumnWidth(),this.distributeRemainingWidth(),this.setColumnAlignments()}},{key:"setHeaderStyle",value:function(){this.options.takeAvailableSpace||l.default.style(this.header,{width:0}),l.default.style(this.header,{margin:0});var e=this.datamanager.getColumns().filter(function(e){return void 0!==e.resizable&&!e.resizable}).map(function(e){return e.colIndex}).map(function(e){return'.data-table-header [data-col-index="'+e+'"]'}).join();this.style.setStyle(e,{cursor:"pointer"})}},{key:"setupMinWidth",value:function(){var e=this;this.minWidthMap=(0,d.getDefault)(this.minWidthMap,[]),l.default.each(".data-table-col",this.header).map(function(t){var n=l.default.style((0,l.default)(".content",t),"width"),o=l.default.data(t),a=o.colIndex;e.minWidthMap[a]||(e.minWidthMap[a]=n)})}},{key:"setNaturalColumnWidth",value:function(){var e=this;l.default.each('.data-table-row[data-row-index="0"] .data-table-col',this.bodyScrollable).map(function(t){var n=l.default.style((0,l.default)(".content",t),"width"),o=l.default.style((0,l.default)(".content",t),"height"),a=l.default.data(t),r=a.colIndex,i=e.getColumnMinWidth(r);n=t)){var o=this.datamanager.getColumns().filter(function(e){return void 0===e.resizable||e.resizable}),a=(t-n)/o.length;o.map(function(t){var n=l.default.style(e.getColumnHeaderElement(t.colIndex),"width"),o=Math.min(n+a)-2;e.setColumnHeaderWidth(t.colIndex,o),e.setColumnWidth(t.colIndex,o)}),this.instance.setBodyWidth()}}}},{key:"setDefaultCellHeight",value:function(e){this.style.setStyle(".data-table-col .content",{height:e+"px"})}},{key:"setColumnAlignments",value:function(){var e=this;this.getColumns().map(function(t){["left","center","right"].includes(t.align)&&e.style.setStyle('[data-col-index="'+t.colIndex+'"]',{"text-align":t.align})})}},{key:"sortRows",value:function(e,t){return this.datamanager.sortRows(e,t)}},{key:"getColumn",value:function(e){return this.datamanager.getColumn(e)}},{key:"getColumns",value:function(){return this.datamanager.getColumns()}},{key:"setColumnWidth",value:function(e,t){if(this._columnWidthMap=this._columnWidthMap||[],!t){var n=(0,l.default)('.data-table-col[data-col-index="'+e+'"] .content',this.header);t=l.default.style(n,"width")}var o=this._columnWidthMap[e],a='[data-col-index="'+e+'"] .content, [data-col-index="'+e+'"] .edit-cell',r={width:t+"px"};o=this.style.setStyle(a,r,o),this._columnWidthMap[e]=o}},{key:"setColumnHeaderWidth",value:function(e,t){this.$columnMap=this.$columnMap||[];var n='[data-col-index="'+e+'"][data-is-header] .content',o=this.$columnMap[e];o||(o=this.header.querySelector(n),this.$columnMap[e]=o),o.style.width=t+"px"}},{key:"getColumnMinWidth",value:function(e){return e=+e,this.minWidthMap&&this.minWidthMap[e]}},{key:"getFirstColumnIndex",value:function(){return this.options.addCheckboxColumn&&this.options.addSerialNoColumn?2:this.options.addCheckboxColumn||this.options.addSerialNoColumn?1:0}},{key:"getHeaderCell$",value:function(e){return(0,l.default)('.data-table-col[data-col-index="'+e+'"]',this.header)}},{key:"getLastColumnIndex",value:function(){return this.datamanager.getColumnCount()-1}},{key:"getColumnHeaderElement",value:function(e){return e=+e,e<0?null:(0,l.default)('.data-table-col[data-is-header][data-col-index="'+e+'"]',this.wrapper)}},{key:"getSerialColumnIndex",value:function(){return this.datamanager.getColumns().findIndex(function(e){return e.content.includes("Sr. No")})}}]),e}();t.default=f;var h=function(){return'
    '+(arguments.length>0&&void 0!==arguments[0]?arguments[0]:"v")+'
    \n
    \n '+this.options.headerDropdown.map(function(e,t){return'
    '+e.label+"
    "}).join("")+"\n
    \n "};t.getDropdownHTML=h},function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(6),r=o(a),i=n(18),l=o(i);r.default.__version__=l.default.version,t.default=r.default,e.exports=t.default},function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n\n
    \n \n
    \n ',this.datatableWrapper=(0,l.default)(".data-table",this.wrapper),this.header=(0,l.default)(".data-table-header",this.wrapper),this.bodyScrollable=(0,l.default)(".body-scrollable",this.wrapper),this.freezeContainer=(0,l.default)(".freeze-container",this.wrapper)}},{key:"refresh",value:function(e){this.datamanager.init(e),this.render()}},{key:"destroy",value:function(){this.wrapper.innerHTML="",this.style.destroy()}},{key:"appendRows",value:function(e){this.datamanager.appendRows(e),this.rowmanager.refreshRows()}},{key:"refreshRow",value:function(e,t){this.rowmanager.refreshRow(e,t)}},{key:"render",value:function(){this.renderHeader(),this.renderBody()}},{key:"renderHeader",value:function(){this.columnmanager.renderHeader()}},{key:"renderBody",value:function(){this.bodyRenderer.render()}},{key:"setDimensions",value:function(){this.columnmanager.setDimensions(),this.setBodyWidth(),l.default.style(this.bodyScrollable,{marginTop:l.default.style(this.header,"height")+"px"}),l.default.style((0,l.default)("table",this.bodyScrollable),{margin:0})}},{key:"setBodyWidth",value:function(){var e=l.default.style(this.header,"width");l.default.style(this.bodyScrollable,{width:e+"px"})}},{key:"getColumn",value:function(e){return this.datamanager.getColumn(e)}},{key:"getCell",value:function(e,t){return this.datamanager.getCell(e,t)}},{key:"getColumnHeaderElement",value:function(e){return this.columnmanager.getColumnHeaderElement(e)}},{key:"getViewportHeight",value:function(){return this.viewportHeight||(this.viewportHeight=l.default.style(this.bodyScrollable,"height")),this.viewportHeight}},{key:"sortColumn",value:function(e,t){this.columnmanager.sortColumn(e,t)}},{key:"removeColumn",value:function(e){this.columnmanager.removeColumn(e)}},{key:"scrollToLastColumn",value:function(){this.datatableWrapper.scrollLeft=9999}},{key:"freeze",value:function(){l.default.style(this.freezeContainer,{display:""})}},{key:"unfreeze",value:function(){l.default.style(this.freezeContainer,{display:"none"})}},{key:"fireEvent",value:function(e){for(var t=arguments.length,n=Array(t>1?t-1:0),o=1;o1&&void 0!==arguments[1]&&arguments[1],{isHeader:1,editable:!0,sortable:!0,resizable:!0,focusable:!0,dropdown:!0,format:function(e){return''+e+""}});return e.map(s).map(function(e){return Object.assign({},t,e)})}function s(e,t){var n={content:"",align:"left",sortOrder:"none",colIndex:0,width:40};return"string"==typeof e&&(e={content:e}),Object.assign({},n,e,{colIndex:t})}Object.defineProperty(t,"__esModule",{value:!0}),t.DataError=void 0;var u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},c=function(){function e(e,t){for(var n=0;n'}}].concat(e)}return l(e)}},{key:"prepareNumericColumns",value:function(){var e=this.getRow(0);this.columns=this.columns.map(function(t,n){var o=e[n].content;return!t.align&&o&&(0,d.isNumeric)(o)&&(t.align="right"),t})}},{key:"prepareRows",value:function(e){var t=this;return this.validateRows(e),e=e.map(function(e,n){var o=t._getNextRowCount();if(e.length'].concat(e)}}return i(e,o)})}},{key:"validateColumns",value:function(e){if(!Array.isArray(e))throw new h("`columns` must be an array");e.forEach(function(e,t){if("string"!=typeof e&&"object"!==(void 0===e?"undefined":u(e)))throw new h('column "'+t+'" must be a string or an object')})}},{key:"validateRows",value:function(e){var t=this;if(!Array.isArray(e))throw new h("`rows` must be an array");e.forEach(function(e,n){if(!Array.isArray(e))throw new h("`row` must be an array");if(e.length!==t.getColumnCount(!0))throw new h('Row index "'+n+"\" doesn't match column length")})}},{key:"appendRows",value:function(e){this.validateRows(e),this.rows=this.rows.concat(this.prepareRows(e))}},{key:"sortRows",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"none";e=+e,this.getColumns().map(function(n){n.colIndex===e?n.sortOrder=t:n.sortOrder="none"}),this._sortRows(e,t)}},{key:"_sortRows",value:function(e,t){if(this.currentSort.colIndex===e&&("asc"===this.currentSort.sortOrder&&"desc"===t||"desc"===this.currentSort.sortOrder&&"asc"===t))return this.reverseArray(this.rows),void(this.currentSort.sortOrder=t);this.rows.sort(function(n,o){var a=n[0].rowIndex,r=o[0].rowIndex,i=n[e].content,l=o[e].content;if("none"===t)return a-r;if("asc"===t){if(il)return 1;if(i===l)return 0}else if("desc"===t){if(il)return-1;if(i===l)return 0}return 0})}},{key:"reverseArray",value:function(e){var t=null,n=null,o=e.length;for(t=0,n=o-1;t'].concat(e)}}var n=i(e,t),o=this.rows.findIndex(function(e){return e[0].rowIndex===t});return this.rows[o]=n,n}},{key:"updateCell",value:function(e,t,n){var o=void 0;return"object"===(void 0===e?"undefined":u(e))&&(o=e,e=o.colIndex,t=o.rowIndex,n=o.content),o=this.getCell(e,t),o.content=n,o}},{key:"getRowCount",value:function(){return this.rowCount}},{key:"_getNextRowCount",value:function(){var e=this.rowCount;return this.rowCount++,e}},{key:"getRows",value:function(e,t){return this.rows.slice(e,t)}},{key:"getColumns",value:function(e){var t=this.columns;return e&&(t=t.slice(this.getStandardColumnCount())),t}},{key:"getStandardColumnCount",value:function(){return this.options.addCheckboxColumn&&this.options.addSerialNoColumn?2:this.options.addCheckboxColumn||this.options.addSerialNoColumn?1:0}},{key:"getColumnCount",value:function(e){var t=this.columns.length;return e&&(t-=this.getStandardColumnCount()),t}},{key:"getColumn",value:function(e){return e=+e,this.columns.find(function(t){return t.colIndex===e})}},{key:"getRow",value:function(e){return e=+e,this.rows.find(function(t){return t[0].rowIndex===e})}},{key:"getCell",value:function(e,t){return t=+t,e=+e,this.rows.find(function(e){return e[0].rowIndex===t})[e]}},{key:"get",value:function(){return{columns:this.columns,rows:this.rows}}},{key:"hasColumn",value:function(e){return Boolean(this.columns.find(function(t){return t.content===e}))}},{key:"currentSort",get:function(){return this.columns.find(function(e){return"none"!==e.sortOrder})||{colIndex:-1,sortOrder:"none"}}}]),e}();t.default=f;var h=t.DataError=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),t}(function(e){function t(){var t=Reflect.construct(e,Array.from(arguments));return Object.setPrototypeOf(t,Object.getPrototypeOf(this)),t}return t.prototype=Object.create(e.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e,t}(TypeError))},function(e,t,n){"use strict";function o(e){var t=i[e.keyCode];e.shiftKey&&"shift"!==t&&(t="shift+"+t),(e.ctrlKey&&"ctrl"!==t||e.metaKey&&"meta"!==t)&&(t="ctrl+"+t);var n=l[t];n&&n.length>0&&n.map(function(t){var n=t();void 0!==n&&!0!==n||e.preventDefault()})}Object.defineProperty(t,"__esModule",{value:!0});var a=n(0),r=function(e){return e&&e.__esModule?e:{default:e}}(a),i={13:"enter",91:"meta",16:"shift",17:"ctrl",18:"alt",37:"left",38:"up",39:"right",40:"down",9:"tab",27:"esc",67:"c"},l={};!function(){r.default.on(document,"keydown",o)}(),t.default={on:function(e,t){e.split(",").map(function(e){return e.trim()}).map(function(e){l[e]=l[e]||[],l[e].push(t)})}},e.exports=t.default},function(t,n){t.exports=e},function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e){return"\n \n "+e.map(function(e){return(0,d.getRowHTML)(e,{rowIndex:e[0].rowIndex})}).join("")+"\n \n "}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n\n '+r(e)+"\n \n "}},{key:"renderBodyWithClusterize",value:function(){var e=this;this.bodyScrollable.innerHTML='\n \n '+r([])+"\n
    \n ",this.clusterize=new c.default({rows:[],scrollElem:this.bodyScrollable,contentElem:(0,s.default)("tbody",this.bodyScrollable),callbacks:{clusterChanged:function(){e.rowmanager.highlightCheckedRows(),e.cellmanager.selectAreaOnClusterChanged(),e.cellmanager.focusCellOnClusterChanged()}}}),this.appendRemainingData(),this.firstPagePromise.then(function(){return e.instance.setDimensions()})}},{key:"appendRemainingData",value:function(){var e=0,t=1e3,n=0;this.firstPagePromise=null;for(var o=[],a=this.datamanager.getRowCount();n+1e30){var i=this.appendNextPage(e,t);this.firstPagePromise||(this.firstPagePromise=i),o.push(i),n+=a%1e3}return(0,f.chainPromises)(o)}},{key:"appendNextPage",value:function(e,t){var n=this.datamanager.getRows(e,t),o=this.getDataForClusterize(n);this.clusterize.append(o)}},{key:"getDataForClusterize",value:function(e){return e.map(function(e){return(0,d.getRowHTML)(e,{rowIndex:e[0].rowIndex})})}}]),e}();t.default=h},function(e,n){e.exports=t},function(e,t,n){"use strict";function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:-1,o=Object.keys(t).map(function(e){return e.includes("-")||(e=(0,r.camelCaseToDash)(e)),e+":"+t[e]+";"}).join(""),a="."+this.scopeClass+" "+e+" { "+o+" }",i=this.styleSheet.cssRules.length;return-1!==n&&(this.styleSheet.deleteRule(n),i=n),this.styleSheet.insertRule(a,i),i}}]),e}();t.default=i,e.exports=t.default},function(e,t,n){var o=n(14);"string"==typeof o&&(o=[[e.i,o,""]]);var a={};a.transform=void 0;n(16)(o,a);o.locals&&(e.exports=o.locals)},function(e,t,n){t=e.exports=n(15)(void 0),t.push([e.i,"/* variables */\n/* resets */\n*, *::after, *::before {\n box-sizing: border-box; }\n\nbutton, input {\n overflow: visible;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n margin: 0; }\n\n/* styling */\n.data-table * {\n outline: none; }\n\n.data-table {\n width: 100%;\n position: relative;\n overflow: auto; }\n .data-table table {\n border-collapse: collapse; }\n .data-table table td {\n padding: 0;\n border: 1px solid #d1d8dd; }\n .data-table thead td {\n border-bottom-width: 2px; }\n .data-table .freeze-container {\n display: flex;\n justify-content: center;\n align-content: center;\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n background-color: #f5f7fa;\n opacity: 0.5;\n font-size: 2em; }\n .data-table .freeze-container span {\n position: absolute;\n top: 50%;\n transform: translateY(-50%); }\n .data-table .trash-container {\n position: absolute;\n bottom: 0;\n left: 30%;\n right: 30%;\n height: 70px;\n background: palevioletred;\n opacity: 0.5; }\n\n.body-scrollable {\n max-height: 500px;\n overflow: auto;\n border-bottom: 1px solid #d1d8dd; }\n .body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) {\n background-color: #f5f7fa; }\n\n.data-table-header {\n position: absolute;\n top: 0;\n left: 0;\n background-color: white;\n font-weight: bold; }\n .data-table-header .content span:not(.column-resizer) {\n cursor: pointer; }\n .data-table-header .column-resizer {\n display: none;\n position: absolute;\n right: 0;\n top: 0;\n width: 4px;\n height: 100%;\n background-color: #5292f7;\n cursor: col-resize; }\n .data-table-header .data-table-dropdown {\n position: absolute;\n right: 10px;\n display: inline-flex;\n vertical-align: top;\n text-align: left; }\n .data-table-header .data-table-dropdown.is-active .data-table-dropdown-list {\n display: block; }\n .data-table-header .data-table-dropdown.is-active .data-table-dropdown-toggle {\n display: block; }\n .data-table-header .data-table-dropdown-toggle {\n display: none;\n background-color: transparent;\n border: none; }\n .data-table-header .data-table-dropdown-list {\n display: none;\n font-weight: normal;\n position: absolute;\n min-width: 8rem;\n top: 100%;\n right: 0;\n z-index: 1;\n background-color: white;\n border-radius: 3px;\n box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);\n padding-bottom: 0.5rem;\n padding-top: 0.5rem; }\n .data-table-header .data-table-dropdown-list > div {\n padding: 5px 10px; }\n .data-table-header .data-table-dropdown-list > div:hover {\n background-color: #f5f7fa; }\n .data-table-header .data-table-col.remove-column {\n background-color: #FD8B8B;\n transition: 300ms background-color ease-in-out; }\n .data-table-header .data-table-col.sortable-chosen {\n background-color: #f5f7fa; }\n\n.data-table-col {\n position: relative; }\n .data-table-col .content {\n padding: 8px;\n border: 2px solid transparent; }\n .data-table-col .content.ellipsis {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden; }\n .data-table-col .edit-cell {\n display: none;\n padding: 8px;\n background: #fff;\n z-index: 1;\n height: 100%; }\n .data-table-col .edit-cell input {\n outline: none;\n width: 100%;\n border: none;\n height: 1em; }\n .data-table-col.selected .content {\n border: 2px solid #5292f7; }\n .data-table-col.editing .content {\n display: none; }\n .data-table-col.editing .edit-cell {\n border: 2px solid #5292f7;\n display: block; }\n .data-table-col.highlight {\n background-color: #f5f7fa; }\n .data-table-col:hover .column-resizer {\n display: inline-block; }\n .data-table-col:hover .data-table-dropdown-toggle {\n display: block; }\n\n.data-table-row.row-highlight {\n background-color: #f5f7fa; }\n\n.noselect {\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none; }\n\nbody.data-table-resize {\n cursor: col-resize; }\n",""])},function(e,t){function n(e,t){var n=e[1]||"",a=e[3];if(!a)return n;if(t&&"function"==typeof btoa){var r=o(a);return[n].concat(a.sources.map(function(e){return"/*# sourceURL="+a.sourceRoot+e+" */"})).concat([r]).join("\n")}return[n].join("\n")}function o(e){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(e))))+" */"}e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var o=n(t,e);return t[2]?"@media "+t[2]+"{"+o+"}":o}).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var o={},a=0;a=0&&y.splice(t,1)}function l(e){var t=document.createElement("style");return e.attrs.type="text/css",u(t,e.attrs),r(e,t),t}function s(e){var t=document.createElement("link");return e.attrs.type="text/css",e.attrs.rel="stylesheet",u(t,e.attrs),r(e,t),t}function u(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function c(e,t){var n,o,a,r;if(t.transform&&e.css){if(!(r=t.transform(e.css)))return function(){};e.css=r}if(t.singleton){var u=m++;n=b||(b=l(t)),o=d.bind(null,n,u,!1),a=d.bind(null,n,u,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=s(t),o=h.bind(null,n,t),a=function(){i(n),n.href&&URL.revokeObjectURL(n.href)}):(n=l(t),o=f.bind(null,n),a=function(){i(n)});return o(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;o(e=t)}else a()}}function d(e,t,n,o){var a=n?"":o.css;if(e.styleSheet)e.styleSheet.cssText=C(t,a);else{var r=document.createTextNode(a),i=e.childNodes;i[t]&&e.removeChild(i[t]),i.length?e.insertBefore(r,i[t]):e.appendChild(r)}}function f(e,t){var n=t.css,o=t.media;if(o&&e.setAttribute("media",o),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}function h(e,t,n){var o=n.css,a=n.sourceMap,r=void 0===t.convertToAbsoluteUrls&&a;(t.convertToAbsoluteUrls||r)&&(o=w(o)),a&&(o+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(a))))+" */");var i=new Blob([o],{type:"text/css"}),l=e.href;e.href=URL.createObjectURL(i),l&&URL.revokeObjectURL(l)}var p={},g=function(e){var t;return function(){return void 0===t&&(t=e.apply(this,arguments)),t}}(function(){return window&&document&&document.all&&!window.atob}),v=function(e){var t={};return function(n){return void 0===t[n]&&(t[n]=e.call(this,n)),t[n]}}(function(e){return document.querySelector(e)}),b=null,m=0,y=[],w=n(17);e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");t=t||{},t.attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||(t.singleton=g()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=a(e,t);return o(n,t),function(e){for(var r=[],i=0;i div[class*="col-sm-"] { padding-right: 0px; } - .filter_field { + .filter-field { width: 65% !important; .frappe-control { @@ -91,144 +112,108 @@ } } -.list-row { - padding: 9px 15px; +.list-row-container { border-bottom: 1px solid @border-color; + display: flex; + flex-direction: column; +} + +.list-row { + padding: 12px 15px; + height: 40px; cursor: pointer; transition: color 0.2s; -webkit-transition: color 0.2s; -} -.list-row .h6 { - margin-top: 0px; - margin-bottom: 0px; + &:hover { + background-color: @panel-bg; + } + &:last-child { + border-bottom: 0px; + } + + .level-left { + flex: 3; + } + .level-right { + flex: 1; + } } .list-row-head { background-color: @panel-bg; border-bottom: 1px solid @border-color !important; -} -.list-row:hover, .grid-row:hover { - background-color: @panel-bg; -} - -.no-hover:hover { - background-color: transparent !important; -} + .list-subject { + font-weight: normal; + } -.list-row:last-child { - border-bottom: 0px; + .checkbox-actions { + display: none; + } } -.list-row-head { - background-color: @panel-bg; - border-bottom: 1px solid @border-color !important; +.list-row-col { + flex: 1; + margin-right: 15px; } -.list-row .h6 { - margin-top: 0px; - margin-bottom: 0px; -} +.list-subject { + flex: 2; + font-weight: bold; + justify-content: start; -.list-item-col { - white-space: nowrap; - text-overflow: ellipsis; - height: 30px; - padding-top: 3px; -} + .level-item { + margin-right: 8px; + } -.list-paging-area { - padding: 10px 15px; - border-top: 1px solid @border-color; + &.seen { + font-weight: normal; + } } -.list-value { - display: table; - vertical-align: middle; +.list-row-activity { + justify-content: flex-end; + min-width: 120px; - .list-row-checkbox, .liked-by, .list-id, .list-select-all { - display: table-cell; - vertical-align: middle; - } - - .list-row-checkbox, .list-select-all { + .avatar:not(.avatar-empty) { margin: 0; - margin-right: 7px; } - .liked-by { - padding-top: 2px; + &> span { + display: inline-block; + margin-left: 10px; + margin-right: 0; } +} - .list-col-title { - vertical-align: middle; - } +.list-paging-area, .footnote-area { + padding: 10px 15px; + border-top: 1px solid @border-color; + overflow: auto; } + .progress { height: 10px; } -.doclist-row { - font-size: 12px; -} - .likes-count { - display: inline-block; - width: 15px; - margin-left: -5px; - color: @text-muted; - font-size: 12px; + display: none; } -.doclist-row .docstatus .octicon { - font-size: 12px; +.list-liked-by-me { + margin-bottom: 1px; } -.doclist-row .progress { - margin-top: 12px; +input.list-check-all, input.list-row-checkbox { + margin-top: 0px; } .filterable { cursor: pointer; } - -.doclist-row .label { - margin-right: 8px; -} - -.list-info-row { - float: left; - margin-top: 1px; -} - -.list-row-right .modified { - margin-top: 3px; -} - -.list-row-right .list-row-modified { - margin-right: 9px; - margin-top: 3px; -} - -.list-row-right { - margin-top: -2px; - margin-bottom: -4px; -} - -.list-row-right .indicator { - margin-left: 10px; - margin-right: -5px; -} - -.side-panel { - border-bottom: 1px solid @border-color; - margin: 0px -15px; - padding: 5px 15px; -} - .listview-main-section { .octicon-heart { cursor: pointer; @@ -256,36 +241,6 @@ color: @heart-color; } -.list-id { - font-weight: bold; -} - -.list-id.seen { - font-weight: normal; -} - -.list-col { - height: 20px; -} - -.list-value { - vertical-align: middle; -} - -@media(max-width: 767px) { - .doclist-row { - font-size: @text-regular; - } - - .doclist-row [type='checkbox'] { - display: none; - } - - .doclist-row .list-row-right .list-row-modified { - display: none; - } -} - .list-comment-count { display: inline-block; width: 37px; @@ -294,10 +249,15 @@ // tags +.result.tags-shown { + .tag-row { + display: block; + } +} + .tag-row { - padding-left: 55px; - margin-bottom: 0px; - margin-top: -5px; + display: none; + margin-left: 50px; } .taggle_placeholder { @@ -374,6 +334,7 @@ padding: 15px; border-bottom: 1px solid @light-border-color; border-right: 1px solid @light-border-color; + max-width: 100%/4; } .image-view-item:nth-child(4n) { @@ -410,6 +371,10 @@ img { max-height: 100%; } + + &.no-image { + background-color: @light-bg; + } } .placeholder-text { @@ -457,6 +422,7 @@ .image-view-container.three-column { .image-view-item { flex: 0 0 100%/3; + max-width: 100%/3; } .image-view-item:nth-child(3n) { @@ -510,6 +476,11 @@ } // gantt +.list-paging-area .gantt-view-mode { + margin-left: 15px; + margin-right: 15px; +} + .gantt { .details-container { .heading { diff --git a/frappe/public/less/report.less b/frappe/public/less/report.less index 82c894a159..32613d61ad 100644 --- a/frappe/public/less/report.less +++ b/frappe/public/less/report.less @@ -67,3 +67,16 @@ margin-bottom: 2px; } } + +// datatable +.data-table { + .edit-popup { + .frappe-control { + padding: 0; + + .form-group { + margin: 0; + } + } + } +} diff --git a/frappe/tests/ui/test_calendar_view.js b/frappe/tests/ui/test_calendar_view.js index f50ceb807d..914f6174da 100644 --- a/frappe/tests/ui/test_calendar_view.js +++ b/frappe/tests/ui/test_calendar_view.js @@ -22,34 +22,31 @@ QUnit.test("Calendar View Tests", function(assert) { {event_type: 'Private'} ]), + () => frappe.timeout(1), + () => frappe.tests.make("Event", [ {subject: random_text + ':Pub'}, {starts_on: today}, {event_type: 'Public'} ]), + () => frappe.timeout(1), + // Goto Calendar view () => frappe.set_route(["List", "Event", "Calendar"]), - () => { - // clear filter - $('[data-fieldname="event_type"]').val('').trigger('change'); - }, + + // clear filter + () => cur_list.filter_area.remove('event_type'), () => frappe.timeout(2), // Check if event is created () => { // Check if the event exists and if its title matches with the one created assert.ok(event_title_text().includes(random_text + ':Pri'), "Event title verified"); - // Check if time of event created is correct - - // assert.ok(visible_time().includes("4:20"), - // "Event start time verified"); }, // check filter - () => { - $('[data-fieldname="event_type"]').val('Public').trigger('change'); - }, + () => cur_list.filter_area.add('Event', 'event_type', '=', 'Public'), () => frappe.timeout(1), () => { // private event should be hidden diff --git a/frappe/tests/ui/test_kanban/test_kanban_creation.js b/frappe/tests/ui/test_kanban/test_kanban_creation.js index 2ef7127f1f..3e1afbefdf 100644 --- a/frappe/tests/ui/test_kanban/test_kanban_creation.js +++ b/frappe/tests/ui/test_kanban/test_kanban_creation.js @@ -4,17 +4,21 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { assert.expect(2); let done = assert.async(); + const board_name = 'Kanban test'; + frappe.run_serially([ () => frappe.set_route("List", "ToDo", "List"), + // wait for cur_list to initialize + () => cur_list.init(), // click kanban in side bar - () => frappe.click_link('Kanban'), - () => frappe.click_link('New Kanban Board'), + () => frappe.tests.click_link('Kanban'), + () => frappe.tests.click_link('New Kanban Board'), () => frappe.timeout(0.5), // create new kanban () => { assert.equal(cur_dialog.title, 'New Kanban Board', "Dialog for new kanban opened."); - cur_dialog.set_value('board_name', 'Kanban test'); + cur_dialog.set_value('board_name', board_name); cur_dialog.set_value('field_name', 'Priority'); }, () => frappe.timeout(0.5), @@ -23,7 +27,7 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { () => frappe.set_route("List", "Kanban Board", "List"), () => frappe.timeout(0.5), // check in kanban list if new kanban is created - () => assert.equal(cur_list.data[0].name, 'Kanban test', + () => assert.equal(cur_list.data[0].name, board_name, "Added kanban is visible in kanban list."), () => done() ]); diff --git a/frappe/tests/ui/test_kanban/test_kanban_filters.js b/frappe/tests/ui/test_kanban/test_kanban_filters.js index b4d4ca1222..5e9af9f0fa 100644 --- a/frappe/tests/ui/test_kanban/test_kanban_filters.js +++ b/frappe/tests/ui/test_kanban/test_kanban_filters.js @@ -10,9 +10,9 @@ QUnit.test("Test: Filters [Kanban view]", function(assert) { () => { assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), "Kanban view opened successfully."); - // set filter values - return frappe.set_control('priority', 'Low'); }, + // set filter values + () => cur_list.filter_area.add('ToDo', 'priority', '=', 'Low'), () => frappe.timeout(1), () => cur_list.page.btn_secondary.click(), () => frappe.timeout(1), diff --git a/frappe/tests/ui/test_kanban/test_kanban_view.js b/frappe/tests/ui/test_kanban/test_kanban_view.js index 9cf7787d2b..4ed9597cf0 100644 --- a/frappe/tests/ui/test_kanban/test_kanban_view.js +++ b/frappe/tests/ui/test_kanban/test_kanban_view.js @@ -1,25 +1,28 @@ QUnit.module('views'); QUnit.test("Test: Kanban view", function(assert) { - assert.expect(3); + assert.expect(4); let done = assert.async(); - let total_elements; frappe.run_serially([ () => frappe.set_route("List", "ToDo", "List"), // calculate number of element in list () => frappe.timeout(1), - () => total_elements = cur_list.data.length, () => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), - () => frappe.timeout(1), + () => frappe.timeout(2), () => { - assert.equal('Kanban', cur_list.current_view, + assert.equal('Kanban', cur_list.view_name, "Current view is kanban."); - assert.equal("Kanban test", cur_list.list_renderer.page_title, + assert.equal("Kanban test", cur_list.page_title, "Kanban view opened successfully."); // check if all elements are visible in kanban view - assert.equal(total_elements, cur_list.data.length, - "All elements are visible in kanban view."); + const $high_priority_cards = + $('.kanban-column[data-column-value="High"] .kanban-card-wrapper'); + const $low_priority_cards = + $('.kanban-column[data-column-value="Low"] .kanban-card-wrapper'); + + assert.equal($high_priority_cards.length, 1); + assert.equal($low_priority_cards.length, 1); }, () => done() ]); diff --git a/frappe/tests/ui/test_list/test_list_paging.js b/frappe/tests/ui/test_list/test_list_paging.js index 569b4083b6..b760a11370 100644 --- a/frappe/tests/ui/test_list/test_list_paging.js +++ b/frappe/tests/ui/test_list/test_list_paging.js @@ -14,10 +14,10 @@ QUnit.test("Test paging in list view", function(assert) { () => frappe.click_button('More'), () => frappe.timeout(2), () => assert.equal(cur_list.data.length, 40, 'show more items'), - () => frappe.click_button('100', '.btn-group-paging'), + () => frappe.tests.click_button('100'), () => frappe.timeout(2), () => assert.ok(cur_list.data.length > 40, 'show 100 items'), - () => frappe.click_button('20', '.btn-group-paging'), + () => frappe.tests.click_button('20'), () => frappe.timeout(2), () => assert.equal(cur_list.data.length, 20, 'show 20 items again'), () => done() diff --git a/frappe/tests/ui/test_list_count.js b/frappe/tests/ui/test_list_count.js index 0261555840..31f73964d2 100644 --- a/frappe/tests/ui/test_list_count.js +++ b/frappe/tests/ui/test_list_count.js @@ -8,24 +8,23 @@ QUnit.test("Test List Count", function(assert) { () => frappe.set_route('List', 'DocType'), () => frappe.timeout(0.5), () => { - let count = $('.list-row-right').text().split(' ')[0]; + let count = $('.list-count').text().split(' ')[0]; assert.equal(cur_list.data.length, count, "Correct Count"); }, () => frappe.timeout(1), - () => cur_list.filter_list.add_filter('Doctype', 'module', '=', 'Desk'), + () => cur_list.filter_area.add('Doctype', 'module', '=', 'Desk'), () => frappe.click_button('Refresh'), () => { - let count = $('.list-row-right').text().split(' ')[0]; + let count = $('.list-count').text().split(' ')[0]; assert.equal(cur_list.data.length, count, "Correct Count"); }, - () => cur_list.filter_list.clear_filters(), + () => cur_list.filter_area.clear(), () => frappe.timeout(1), () => { - cur_list.filter_list.push_new_filter('DocField', 'fieldname', 'like', 'owner'); - frappe.click_button('Apply'); - let count = $('.list-row-right').text().split(' ')[0]; + cur_list.filter_area.add('DocField', 'fieldname', 'like', 'owner'); + let count = $('.list-count').text().split(' ')[0]; assert.equal(cur_list.data.length, count, "Correct Count"); },