* first cut * Code refactoring, styling * Added Sorting * Revert query_report to use slickgrid * cleanup * Edit cell working * Add regrid, remove datatable * Add clusterize * Update lib, fix get_checked_items * New ReportView * wip * Enable editing, fix styles * update lib * wip * fix refresh rows and editable cells * Refresh list_view every 3s, decouple refreshing logic * Report editing fixes * Cleanup loading fields, add column then refresh list * [wip] New List View * [working] Render results * ListView is now BaseList, add new ListView and GanttView * Create new page for each ListView * GanttView working * CalendarView working * KanbanView working * Cache list_view based on page_name * Gantt view buttons on mobile * Add ReportView * Refresh datatable on render * Setup like * [start][filters] clean up FilterList * [filters] refactor FilterList * [filters] minor fix * [filters] fix remove filter * filter utils * more utils, remove apply * rewrite as class, remove 'me' references * [filter] implement on_change to decouple parent functions * Integrate new filters with new BaseList * Setup freeze area for ListView * Set breadcrumbs on setup_page * Trigger list update from events * Setup footnote area * Fix Kanban Board filters * Add filters to standard filters, then filter_list * Remove old files * Fix ImageView * Some more fixes for BaseList.init * Fix order_by on load * Report View: remember columns * Fix for hidden filters * Fix for delete items * InboxView * Shift select checkboxes * Fix ESLint errors * More refactoring - Move ListMenu to Listview - New FileView - Ability to add custom breadcrumbs * FileManager working * Tags, set filters from route options * Custom Reports Working * List Sidebar reports * Report Name as title * Fix ESLint errors * Fix UI tests * Fix Kanban test * Format ID column * [fix] Kanban cards title * Checkbox fix * Fix Activity Page * Update rows in Report in place * Child Table columns in Report Viewversion-14
@@ -120,6 +120,7 @@ | |||||
"QUnit": true, | "QUnit": true, | ||||
"JsBarcode": true, | "JsBarcode": true, | ||||
"L": true, | "L": true, | ||||
"Chart": true | |||||
"Chart": true, | |||||
"DataTable": true | |||||
} | } | ||||
} | } |
@@ -355,7 +355,7 @@ | |||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Folder", | "label": "Folder", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
@@ -653,7 +653,7 @@ | |||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"menu_index": 0, | "menu_index": 0, | ||||
"modified": "2017-10-27 13:27:43.882914", | |||||
"modified": "2017-12-07 17:01:54.860204", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "File", | "name": "File", | ||||
@@ -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 += '<i class="fa fa-folder-close-alt fa-fw"></i> '; | |||||
} else if(frappe.utils.is_image_file(data.file_name)) { | |||||
icon += '<i class="fa fa-picture fa-fw"></i> '; | |||||
} else { | |||||
icon += '<i class="fa fa-file-alt fa-fw"></i> '; | |||||
} | |||||
data._title = icon + (data.file_name ? data.file_name : data.file_url); | |||||
if (data.is_private) { | |||||
data._title += ' <i class="fa fa-lock fa-fw text-warning"></i>'; | |||||
} | |||||
}, | |||||
onload: function(doclist) { | |||||
doclist.filter_area = doclist.wrapper.find(".show_filters"); | |||||
doclist.breadcrumb = $('<ol class="breadcrumb for-file-list"></ol>') | |||||
.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) { | |||||
$('<li><a href="#List/File/'+folder.name+'">' | |||||
+ folder.file_name+'</a></li>') | |||||
.appendTo(doclist.breadcrumb); | |||||
}); | |||||
} | |||||
$('<li class="active">'+ doclist.current_folder_name+'</li>') | |||||
.appendTo(doclist.breadcrumb); | |||||
} | |||||
}); | |||||
} | |||||
}; |
@@ -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() | |||||
]); | |||||
}); |
@@ -22,7 +22,7 @@ cur_frm.cscript.refresh = function(doc) { | |||||
cur_frm.add_custom_button("Show Report", function() { | cur_frm.add_custom_button("Show Report", function() { | ||||
switch(doc.report_type) { | switch(doc.report_type) { | ||||
case "Report Builder": | case "Report Builder": | ||||
frappe.set_route("Report", doc.ref_doctype, doc.name); | |||||
frappe.set_route('List', doc.ref_doctype, 'Report', doc.name); | |||||
break; | break; | ||||
case "Query Report": | case "Query Report": | ||||
frappe.set_route("query-report", doc.name); | frappe.set_route("query-report", doc.name); | ||||
@@ -262,6 +262,7 @@ $.extend(frappe.desktop, { | |||||
} | } | ||||
new Sortable($("#icon-grid").get(0), { | new Sortable($("#icon-grid").get(0), { | ||||
animation: 150, | |||||
onUpdate: function(event) { | onUpdate: function(event) { | ||||
var new_order = []; | var new_order = []; | ||||
$("#icon-grid .case-wrapper").each(function(i, e) { | $("#icon-grid .case-wrapper").each(function(i, e) { | ||||
@@ -32,7 +32,7 @@ QUnit.test("test: Event", function (assert) { | |||||
() => frappe.set_route('List', 'Event', 'Calendar'), | () => frappe.set_route('List', 'Event', 'Calendar'), | ||||
() => frappe.timeout(2), | () => 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'); | .css('background-color'); | ||||
assert.equal(bg_color, rgb, 'Event background color is set correctly'); | assert.equal(bg_color, rgb, 'Event background color is set correctly'); | ||||
}, | }, | ||||
@@ -32,7 +32,7 @@ frappe.ui.form.on('Kanban Board', { | |||||
field_name: function(frm) { | field_name: function(frm) { | ||||
var field = frappe.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name); | var field = frappe.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name); | ||||
frm.doc.columns = []; | 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(); | o = o.trim(); | ||||
if(!o) return; | if(!o) return; | ||||
var d = frm.add_child('columns'); | var d = frm.add_child('columns'); | ||||
@@ -6,6 +6,7 @@ | |||||
#page-activity .list-row { | #page-activity .list-row { | ||||
border: none; | border: none; | ||||
padding: 0px; | padding: 0px; | ||||
height: auto; | |||||
cursor: pointer; | cursor: pointer; | ||||
} | } | ||||
@@ -15,41 +15,10 @@ frappe.pages['activity'].on_page_load = function(wrapper) { | |||||
me.page.set_title(__("Activity")); | me.page.set_title(__("Activity")); | ||||
frappe.model.with_doctype("Communication", function() { | 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: $("<div></div>").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 = $('<div class="list-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); | frappe.activity.render_heatmap(me.page); | ||||
@@ -90,7 +59,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { | |||||
frappe.route_options = { | frappe.route_options = { | ||||
show_likes: true | show_likes: true | ||||
}; | }; | ||||
me.page.list.run(); | |||||
me.page.list.refresh(); | |||||
}, 'octicon octicon-heart'); | }, 'octicon octicon-heart'); | ||||
}; | }; | ||||
@@ -194,4 +163,48 @@ frappe.activity.render_heatmap = function(page) { | |||||
} | } | ||||
} | } | ||||
}) | }) | ||||
} | |||||
} | |||||
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 = $('<div class="list-row">').data("data", value).appendTo(this.$result).get(0); | |||||
new frappe.activity.Feed(row, value); | |||||
}); | |||||
} | |||||
}; |
@@ -215,6 +215,8 @@ def delete_items(): | |||||
il = json.loads(frappe.form_dict.get('items')) | il = json.loads(frappe.form_dict.get('items')) | ||||
doctype = frappe.form_dict.get('doctype') | doctype = frappe.form_dict.get('doctype') | ||||
failed = [] | |||||
for i, d in enumerate(il): | for i, d in enumerate(il): | ||||
try: | try: | ||||
frappe.delete_doc(doctype, d) | frappe.delete_doc(doctype, d) | ||||
@@ -223,7 +225,9 @@ def delete_items(): | |||||
dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype)), | dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype)), | ||||
user=frappe.session.user) | user=frappe.session.user) | ||||
except Exception: | except Exception: | ||||
pass | |||||
failed.append(d) | |||||
return failed | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_sidebar_stats(stats, doctype, filters=[]): | def get_sidebar_stats(stats, doctype, filters=[]): | ||||
@@ -925,7 +925,12 @@ class Document(BaseDocument): | |||||
if not self.meta.get("read_only") and not self.meta.get("issingle") and \ | if not self.meta.get("read_only") and not self.meta.get("issingle") and \ | ||||
not self.meta.get("istable"): | 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): | 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. | '''Set a value in the document object, update the timestamp and update the database. | ||||
@@ -112,6 +112,7 @@ | |||||
"public/css/font-awesome.css", | "public/css/font-awesome.css", | ||||
"public/css/octicons/octicons.css", | "public/css/octicons/octicons.css", | ||||
"public/css/desk.css", | "public/css/desk.css", | ||||
"public/css/flex.css", | |||||
"public/css/indicator.css", | "public/css/indicator.css", | ||||
"public/css/avatar.css", | "public/css/avatar.css", | ||||
"public/css/navbar.css", | "public/css/navbar.css", | ||||
@@ -212,6 +213,7 @@ | |||||
"public/js/frappe/misc/help_links.js", | "public/js/frappe/misc/help_links.js", | ||||
"public/js/frappe/misc/address_and_contact.js", | "public/js/frappe/misc/address_and_contact.js", | ||||
"public/js/frappe/misc/preview_email.js", | "public/js/frappe/misc/preview_email.js", | ||||
"public/js/frappe/misc/file_manager.js", | |||||
"public/js/frappe/ui/upload.html", | "public/js/frappe/ui/upload.html", | ||||
"public/js/frappe/upload.js", | "public/js/frappe/upload.js", | ||||
@@ -299,10 +301,10 @@ | |||||
"js/list.min.js": [ | "js/list.min.js": [ | ||||
"public/js/frappe/ui/listing.html", | "public/js/frappe/ui/listing.html", | ||||
"public/js/frappe/ui/base_list.js", | |||||
"public/js/frappe/model/indicator.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/filters/edit_filter.html", | ||||
"public/js/frappe/ui/tags.js", | "public/js/frappe/ui/tags.js", | ||||
"public/js/frappe/ui/tag_editor.js", | "public/js/frappe/ui/tag_editor.js", | ||||
@@ -310,7 +312,9 @@ | |||||
"public/js/frappe/ui/liked_by.html", | "public/js/frappe/ui/liked_by.html", | ||||
"public/html/print_template.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_view.js", | ||||
"public/js/frappe/list/list_factory.js", | |||||
"public/js/frappe/list/list_sidebar.js", | "public/js/frappe/list/list_sidebar.js", | ||||
"public/js/frappe/list/list_sidebar.html", | "public/js/frappe/list/list_sidebar.html", | ||||
@@ -322,12 +326,12 @@ | |||||
"public/js/frappe/list/list_item_subject.html", | "public/js/frappe/list/list_item_subject.html", | ||||
"public/js/frappe/list/list_permission_footer.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/gantt/gantt_view.js", | ||||
"public/js/frappe/views/calendar/calendar.js", | "public/js/frappe/views/calendar/calendar.js", | ||||
"public/js/frappe/views/image/image_view.js", | "public/js/frappe/views/image/image_view.js", | ||||
"public/js/frappe/views/kanban/kanban_view.js", | "public/js/frappe/views/kanban/kanban_view.js", | ||||
"public/js/frappe/views/inbox/inbox_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/header_select_all_like_filter.html", | ||||
"public/js/frappe/list/item_assigned_to_comment_count.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/image_view_item_row.html", | ||||
"public/js/frappe/views/image/photoswipe_dom.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_board.html", | ||||
"public/js/frappe/views/kanban/kanban_column.html", | "public/js/frappe/views/kanban/kanban_column.html", | ||||
"public/js/frappe/views/kanban/kanban_card.html" | "public/js/frappe/views/kanban/kanban_card.html" | ||||
@@ -347,13 +347,17 @@ | |||||
"css/report.min.css": [ | "css/report.min.css": [ | ||||
"public/css/report.css", | "public/css/report.css", | ||||
"public/css/tree_grid.css", | "public/css/tree_grid.css", | ||||
"public/css/frappe-datatable.css", | |||||
"public/js/lib/slickgrid/slick.grid.css", | "public/js/lib/slickgrid/slick.grid.css", | ||||
"public/js/lib/slickgrid/slick-default-theme.css", | "public/js/lib/slickgrid/slick-default-theme.css", | ||||
"public/css/slickgrid.css" | "public/css/slickgrid.css" | ||||
], | ], | ||||
"js/report.min.js": [ | "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/reportview.js", | ||||
"public/js/frappe/views/reports/report_view.js", | |||||
"public/js/frappe/views/reports/reportview_footer.html", | "public/js/frappe/views/reports/reportview_footer.html", | ||||
"public/js/frappe/views/reports/query_report.js", | "public/js/frappe/views/reports/query_report.js", | ||||
"public/js/frappe/views/reports/grid_report.js", | "public/js/frappe/views/reports/grid_report.js", | ||||
@@ -216,6 +216,18 @@ a.no-decoration:active { | |||||
.margin { | .margin { | ||||
margin: 15px; | 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) { | @media (max-width: 767px) { | ||||
.text-center-xs { | .text-center-xs { | ||||
text-align: center; | text-align: center; | ||||
@@ -3,6 +3,12 @@ | |||||
margin-top: 5px; | margin-top: 5px; | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
} | } | ||||
.unit-checkbox label { | |||||
position: relative; | |||||
} | |||||
.unit-checkbox input[type=checkbox] { | |||||
margin-left: 0; | |||||
} | |||||
.unit-checkbox + .checkbox { | .unit-checkbox + .checkbox { | ||||
margin-top: 5px; | margin-top: 5px; | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
@@ -216,6 +216,18 @@ a.no-decoration:active { | |||||
.margin { | .margin { | ||||
margin: 15px; | 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) { | @media (max-width: 767px) { | ||||
.text-center-xs { | .text-center-xs { | ||||
text-align: center; | text-align: center; | ||||
@@ -486,6 +498,9 @@ fieldset[disabled] .form-control { | |||||
.form-control input { | .form-control input { | ||||
padding: 6px 10px 8px; | padding: 6px 10px 8px; | ||||
} | } | ||||
.input-area { | |||||
position: relative; | |||||
} | |||||
.link-field.ui-front { | .link-field.ui-front { | ||||
z-index: inherit; | z-index: inherit; | ||||
} | } | ||||
@@ -587,10 +602,6 @@ li.user-progress .progress-bar { | |||||
.intro-area { | .intro-area { | ||||
padding: 15px 30px; | padding: 15px 30px; | ||||
} | } | ||||
.footnote-area { | |||||
padding: 0px 15px; | |||||
border-top: 1px solid #d1d8dd; | |||||
} | |||||
.file-upload .input-group-addon { | .file-upload .input-group-addon { | ||||
color: #8D99A6; | color: #8D99A6; | ||||
font-size: 12px; | font-size: 12px; | ||||
@@ -972,7 +983,7 @@ li.user-progress .progress-bar { | |||||
} | } | ||||
input[type="checkbox"] { | input[type="checkbox"] { | ||||
position: relative; | position: relative; | ||||
height: 16px; | |||||
left: -999999px; | |||||
} | } | ||||
input[type="checkbox"]:before { | input[type="checkbox"]:before { | ||||
position: absolute; | position: absolute; | ||||
@@ -990,9 +1001,7 @@ input[type="checkbox"]:before { | |||||
-webkit-transition: 150ms color; | -webkit-transition: 150ms color; | ||||
-o-transition: 150ms color; | -o-transition: 150ms color; | ||||
transition: 150ms color; | transition: 150ms color; | ||||
background-color: white; | |||||
padding: 1px; | |||||
margin: -1px; | |||||
left: 999999px; | |||||
} | } | ||||
input[type="checkbox"]:focus:before { | input[type="checkbox"]:focus:before { | ||||
color: #8D99A6; | color: #8D99A6; | ||||
@@ -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; | |||||
} |
@@ -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; | |||||
} | |||||
} |
@@ -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 { | .sort-selector .dropdown:hover { | ||||
text-decoration: underline; | text-decoration: underline; | ||||
} | } | ||||
.list-filters { | |||||
.filter-list { | |||||
position: relative; | position: relative; | ||||
} | } | ||||
.list-filters .sort-selector { | |||||
.filter-list .sort-selector { | |||||
position: absolute; | position: absolute; | ||||
top: 15px; | top: 15px; | ||||
right: 15px; | right: 15px; | ||||
} | } | ||||
.show_filters { | |||||
.tag-filters-area { | |||||
padding: 15px 15px 0px; | padding: 15px 15px 0px; | ||||
border-bottom: 1px solid #d1d8dd; | border-bottom: 1px solid #d1d8dd; | ||||
} | } | ||||
.set-filters { | |||||
.active-tag-filters { | |||||
padding-bottom: 4px; | padding-bottom: 4px; | ||||
padding-right: 120px; | padding-right: 120px; | ||||
} | } | ||||
@media (max-width: 767px) { | @media (max-width: 767px) { | ||||
.set-filters { | |||||
.active-tag-filters { | |||||
padding-right: 80px; | padding-right: 80px; | ||||
} | } | ||||
} | } | ||||
.set-filters .btn { | |||||
.active-tag-filters .btn { | |||||
margin-bottom: 10px; | margin-bottom: 10px; | ||||
} | } | ||||
.set-filters .btn-group { | |||||
margin-right: 10px; | |||||
.active-tag-filters .btn-group { | |||||
margin-left: 10px; | |||||
white-space: nowrap; | white-space: nowrap; | ||||
font-size: 0; | font-size: 0; | ||||
} | } | ||||
.set-filters .btn-group .btn-default { | |||||
.active-tag-filters .btn-group .btn-default { | |||||
background-color: transparent; | background-color: transparent; | ||||
border: 1px solid #d1d8dd; | border: 1px solid #d1d8dd; | ||||
color: #8D99A6; | color: #8D99A6; | ||||
@@ -51,138 +71,107 @@ | |||||
margin-top: 6px; | margin-top: 6px; | ||||
margin-left: 15px; | margin-left: 15px; | ||||
} | } | ||||
.filter-box .filter_field { | |||||
.filter-box .filter-field { | |||||
padding-right: 15px; | padding-right: 15px; | ||||
width: calc(64%); | width: calc(64%); | ||||
} | } | ||||
.filter-box .filter_field .frappe-control { | |||||
.filter-box .filter-field .frappe-control { | |||||
position: relative; | position: relative; | ||||
} | } | ||||
@media (min-width: 768px) { | |||||
@media (min-width: 767px) { | |||||
.filter-box .row > div[class*="col-sm-"] { | .filter-box .row > div[class*="col-sm-"] { | ||||
padding-right: 0px; | padding-right: 0px; | ||||
} | } | ||||
.filter_field { | |||||
.filter-field { | |||||
width: 65% !important; | width: 65% !important; | ||||
} | } | ||||
.filter_field .frappe-control { | |||||
.filter-field .frappe-control { | |||||
position: relative; | position: relative; | ||||
} | } | ||||
} | } | ||||
.list-row { | |||||
padding: 9px 15px; | |||||
.list-row-container { | |||||
border-bottom: 1px solid #d1d8dd; | border-bottom: 1px solid #d1d8dd; | ||||
display: flex; | |||||
flex-direction: column; | |||||
} | |||||
.list-row { | |||||
padding: 12px 15px; | |||||
height: 40px; | |||||
cursor: pointer; | cursor: pointer; | ||||
transition: color 0.2s; | transition: color 0.2s; | ||||
-webkit-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; | 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 { | .list-row:last-child { | ||||
border-bottom: 0px; | border-bottom: 0px; | ||||
} | } | ||||
.list-row .level-left { | |||||
flex: 3; | |||||
} | |||||
.list-row .level-right { | |||||
flex: 1; | |||||
} | |||||
.list-row-head { | .list-row-head { | ||||
background-color: #F7FAFC; | background-color: #F7FAFC; | ||||
border-bottom: 1px solid #d1d8dd !important; | 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: 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 { | .progress { | ||||
height: 10px; | height: 10px; | ||||
} | } | ||||
.doclist-row { | |||||
font-size: 12px; | |||||
} | |||||
.likes-count { | .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 { | .filterable { | ||||
cursor: pointer; | 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 { | .listview-main-section .octicon-heart { | ||||
cursor: pointer; | cursor: pointer; | ||||
} | } | ||||
@@ -208,38 +197,17 @@ | |||||
.like-action.octicon-heart { | .like-action.octicon-heart { | ||||
color: #ff5858; | 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 { | .list-comment-count { | ||||
display: inline-block; | display: inline-block; | ||||
width: 37px; | width: 37px; | ||||
text-align: left; | text-align: left; | ||||
} | } | ||||
.result.tags-shown .tag-row { | |||||
display: block; | |||||
} | |||||
.tag-row { | .tag-row { | ||||
padding-left: 55px; | |||||
margin-bottom: 0px; | |||||
margin-top: -5px; | |||||
display: none; | |||||
margin-left: 50px; | |||||
} | } | ||||
.taggle_placeholder { | .taggle_placeholder { | ||||
top: 0; | top: 0; | ||||
@@ -300,6 +268,7 @@ | |||||
padding: 15px; | padding: 15px; | ||||
border-bottom: 1px solid #EBEFF2; | border-bottom: 1px solid #EBEFF2; | ||||
border-right: 1px solid #EBEFF2; | border-right: 1px solid #EBEFF2; | ||||
max-width: 25%; | |||||
} | } | ||||
.image-view-container .image-view-item:nth-child(4n) { | .image-view-container .image-view-item:nth-child(4n) { | ||||
border-right: none; | border-right: none; | ||||
@@ -329,6 +298,9 @@ | |||||
.image-view-container .image-field img { | .image-view-container .image-field img { | ||||
max-height: 100%; | max-height: 100%; | ||||
} | } | ||||
.image-view-container .image-field.no-image { | |||||
background-color: #fafbfc; | |||||
} | |||||
.image-view-container .placeholder-text { | .image-view-container .placeholder-text { | ||||
font-size: 72px; | font-size: 72px; | ||||
color: #d1d8dd; | color: #d1d8dd; | ||||
@@ -351,6 +323,7 @@ | |||||
@media (max-width: 991px) { | @media (max-width: 991px) { | ||||
.image-view-container .image-view-item { | .image-view-container .image-view-item { | ||||
flex: 0 0 33.33333333%; | flex: 0 0 33.33333333%; | ||||
max-width: 33.33333333%; | |||||
} | } | ||||
.image-view-container .image-view-item:nth-child(3n) { | .image-view-container .image-view-item:nth-child(3n) { | ||||
border-right: none; | border-right: none; | ||||
@@ -381,6 +354,7 @@ | |||||
} | } | ||||
.image-view-container.three-column .image-view-item { | .image-view-container.three-column .image-view-item { | ||||
flex: 0 0 33.33333333%; | flex: 0 0 33.33333333%; | ||||
max-width: 33.33333333%; | |||||
} | } | ||||
.image-view-container.three-column .image-view-item:nth-child(3n) { | .image-view-container.three-column .image-view-item:nth-child(3n) { | ||||
border-right: none; | border-right: none; | ||||
@@ -424,6 +398,10 @@ | |||||
.pswp__more-item img { | .pswp__more-item img { | ||||
max-height: 100%; | max-height: 100%; | ||||
} | } | ||||
.list-paging-area .gantt-view-mode { | |||||
margin-left: 15px; | |||||
margin-right: 15px; | |||||
} | |||||
.gantt .details-container .heading { | .gantt .details-container .heading { | ||||
margin-bottom: 10px; | margin-bottom: 10px; | ||||
font-size: 12px; | font-size: 12px; | ||||
@@ -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; | |||||
} |
@@ -51,3 +51,9 @@ | |||||
.column-picker-dialog .add-btn { | .column-picker-dialog .add-btn { | ||||
margin-bottom: 2px; | margin-bottom: 2px; | ||||
} | } | ||||
.data-table .edit-popup .frappe-control { | |||||
padding: 0; | |||||
} | |||||
.data-table .edit-popup .frappe-control .form-group { | |||||
margin: 0; | |||||
} |
@@ -216,6 +216,18 @@ a.no-decoration:active { | |||||
.margin { | .margin { | ||||
margin: 15px; | 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) { | @media (max-width: 767px) { | ||||
.text-center-xs { | .text-center-xs { | ||||
text-align: center; | text-align: center; | ||||
@@ -56,5 +56,14 @@ frappe.db = { | |||||
callback && callback(r.message); | 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) | |||||
}); | |||||
}); | |||||
} | } | ||||
} | |||||
}; |
@@ -8,9 +8,12 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ | |||||
set_options() { | set_options() { | ||||
if (this.df.options) { | if (this.df.options) { | ||||
let options = this.df.options || []; | let options = this.df.options || []; | ||||
if(typeof options === 'string') { | |||||
if (typeof options === 'string') { | |||||
options = options.split('\n'); | options = options.split('\n'); | ||||
} | } | ||||
if (typeof options[0] === 'string') { | |||||
options = options.map(o => ({label: o, value: o})); | |||||
} | |||||
this._data = options; | this._data = options; | ||||
} | } | ||||
}, | }, | ||||
@@ -20,13 +23,14 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ | |||||
minChars: 0, | minChars: 0, | ||||
maxItems: 99, | maxItems: 99, | ||||
autoFirst: true, | autoFirst: true, | ||||
list: this.get_data() | |||||
list: this.get_data(), | |||||
sort: () => { | |||||
return 0; | |||||
} | |||||
}; | }; | ||||
}, | }, | ||||
setup_awesomplete() { | setup_awesomplete() { | ||||
var me = this; | |||||
this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); | this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); | ||||
$(this.input_area).find('.awesomplete ul').css('min-width', '100%'); | $(this.input_area).find('.awesomplete ul').css('min-width', '100%'); | ||||
@@ -79,7 +79,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ | |||||
if(is_valid) { | if(is_valid) { | ||||
return value; | return value; | ||||
} | } | ||||
frappe.msgprint(__("{0} is not a valid hex color", [value])); | |||||
return null; | return null; | ||||
} | } | ||||
}); | }); |
@@ -179,8 +179,8 @@ frappe.ui.form.AssignToDialog = Class.extend({ | |||||
}); | }); | ||||
frappe.ui.add_assignment = function(opts, dialog) { | 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) { | if(args && assign_to) { | ||||
return frappe.call({ | return frappe.call({ | ||||
method: opts.method, | method: opts.method, | ||||
@@ -228,6 +228,14 @@ frappe.form.formatters = { | |||||
}, | }, | ||||
Email: function(value) { | Email: function(value) { | ||||
return $("<div></div>").text(value).html(); | return $("<div></div>").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; | |||||
} | } | ||||
} | } | ||||
@@ -22,7 +22,6 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
} | } | ||||
make_dialog() { | make_dialog() { | ||||
var me = this; | |||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
hide_on_page_refresh: true, | hide_on_page_refresh: true, | ||||
@@ -38,38 +37,29 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
.then(() => this.load_doctypes()) | .then(() => this.load_doctypes()) | ||||
.then(() => this.links_not_permitted_or_missing()) | .then(() => this.links_not_permitted_or_missing()) | ||||
.then(() => this.get_linked_docs()) | .then(() => this.get_linked_docs()) | ||||
.then(() => this.make_html()) | |||||
} | |||||
.then(() => this.make_html()); | |||||
}; | |||||
} | } | ||||
make_html() { | make_html() { | ||||
const linked_docs = this.frm.__linked_docs; | 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"); | html = __("Not Linked to any record"); | ||||
} else { | } else { | ||||
html = Object.keys(linked_docs).map(dt => { | |||||
const list_renderer = new frappe.views.ListRenderer({ | |||||
doctype: dt, | |||||
list_view: this | |||||
}); | |||||
return `<div class="list-item-table" style="margin-bottom: 15px"> | |||||
${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 = $('<div class="list-item-container">').append($item); | |||||
return $item_container[0].outerHTML; | |||||
}).join("")} | |||||
</div>`; | |||||
}); | |||||
html = linked_doctypes.map(doctype => { | |||||
const docs = linked_docs[doctype]; | |||||
return ` | |||||
<div class="list-item-table margin-bottom"> | |||||
${this.make_doc_head(doctype)} | |||||
${docs.map(doc => this.make_doc_row(doc, doctype)).join('')} | |||||
</div> | |||||
`; | |||||
}).join(''); | |||||
} | } | ||||
$(this.dialog.body).html(html); | $(this.dialog.body).html(html); | ||||
@@ -82,7 +72,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
if (this.frm.__linked_doctypes) { | if (this.frm.__linked_doctypes) { | ||||
doctypes_to_load = | doctypes_to_load = | ||||
Object.keys(this.frm.__linked_doctypes) | 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 | // load all doctypes asynchronously using with_doctype | ||||
@@ -100,19 +90,17 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
} | } | ||||
links_not_permitted_or_missing() { | links_not_permitted_or_missing() { | ||||
var me = this; | |||||
let links = null; | let links = null; | ||||
if (this.frm.__linked_doctypes) { | if (this.frm.__linked_doctypes) { | ||||
links = | links = | ||||
Object.keys(this.frm.__linked_doctypes) | Object.keys(this.frm.__linked_doctypes) | ||||
.filter(frappe.model.can_get_report); | |||||
.filter(frappe.model.can_get_report); | |||||
} | } | ||||
let flag; | let flag; | ||||
if(!links) { | 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 enough permission to see links") | ||||
: __("Not Linked to any record")}`); | : __("Not Linked to any record")}`); | ||||
flag = true; | flag = true; | ||||
@@ -126,7 +114,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
} | } | ||||
get_linked_doctypes() { | get_linked_doctypes() { | ||||
return new Promise((resolve, reject) => { | |||||
return new Promise((resolve) => { | |||||
if (this.frm.__linked_doctypes) { | if (this.frm.__linked_doctypes) { | ||||
resolve(); | resolve(); | ||||
} | } | ||||
@@ -160,19 +148,20 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
} | } | ||||
make_doc_head(heading) { | make_doc_head(heading) { | ||||
return `<div class="list-item list-item--head"> | |||||
<div class="list-item__content"> | |||||
${heading} | |||||
</div></div>`; | |||||
return ` | |||||
<header class="level list-row list-row-head text-muted small"> | |||||
<div>${__(heading)}</div> | |||||
</header> | |||||
`; | |||||
} | } | ||||
make_doc_row(doc, doctype) { | make_doc_row(doc, doctype) { | ||||
return `<div class="list-item-container"> | |||||
<div class="list-item"> | |||||
<div class="list-item__content bold"> | |||||
return `<div class="list-row-container"> | |||||
<div class="level list-row small"> | |||||
<div class="level-left bold"> | |||||
<a href="#Form/${doctype}/${doc.name}">${doc.name}</a> | <a href="#Form/${doctype}/${doc.name}">${doc.name}</a> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div>`; | </div>`; | ||||
} | } | ||||
} | |||||
}; |
@@ -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 = $('<div class="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 = $(`<div class="result">`).hide(); | |||||
this.$frappe_list.append(this.$result); | |||||
} | |||||
setup_no_result_area() { | |||||
this.$no_result = $(` | |||||
<div class="no-result text-muted flex justify-center align-center"> | |||||
${this.get_no_result_message()} | |||||
</div> | |||||
`).hide(); | |||||
this.$frappe_list.append(this.$no_result); | |||||
} | |||||
setup_freeze_area() { | |||||
this.$freeze = $('<div class="freeze"></div>').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 = $( | |||||
`<div class="list-paging-area level"> | |||||
<div class="level-left"> | |||||
<div class="btn-group"> | |||||
${paging_values.map(value => ` | |||||
<button type="button" class="btn btn-default btn-sm btn-paging" | |||||
data-value="${value}"> | |||||
${value} | |||||
</button> | |||||
`).join('')} | |||||
</div> | |||||
</div> | |||||
<div class="level-right"> | |||||
<button class="btn btn-default btn-more btn-sm"> | |||||
${__("More")}... | |||||
</button> | |||||
</div> | |||||
</div>` | |||||
).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 = $('<div class="filter-list">').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() { | |||||
$( | |||||
`<div class="flex justify-center align-center"> | |||||
<span class="octicon octicon-search text-muted small"></span> | |||||
</div>` | |||||
) | |||||
.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); |
@@ -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; | |||||
} | |||||
} | |||||
}); |
@@ -1,8 +1,8 @@ | |||||
<div style="padding-left: 20px;"> | |||||
<i class="octicon octicon-lock" style="float: left; margin-left: -20px;"></i> | |||||
<div class="level"> | |||||
<i class="octicon octicon-lock level-item" style="margin-right: 5px;"></i> | |||||
{% for(var i=0; i < condition_list.length; i++) { | {% for(var i=0; i < condition_list.length; i++) { | ||||
var conditions = condition_list[i]; %} | var conditions = condition_list[i]; %} | ||||
<div style="margin-bottom: 5px;"> | |||||
<div class="level-item"> | |||||
{% if (i > 0) { %}<span style="margin-right: 10px;">{{ __("Or") }}</span>{% } %} | {% if (i > 0) { %}<span style="margin-right: 10px;">{{ __("Or") }}</span>{% } %} | ||||
{% for(key in conditions) { %} | {% for(key in conditions) { %} | ||||
<span class="label label-default" style="margin-right: 10px;"> | <span class="label label-default" style="margin-right: 10px;"> | ||||
@@ -10,7 +10,7 @@ | |||||
{{ __("Reports") }} <span class="caret"></span> | {{ __("Reports") }} <span class="caret"></span> | ||||
</a> | </a> | ||||
<ul class="dropdown-menu reports-dropdown" style="max-height: 300px; overflow-y: auto;"> | <ul class="dropdown-menu reports-dropdown" style="max-height: 300px; overflow-y: auto;"> | ||||
<li><a href="#Report/{{ doctype }}">{{ __("Report Builder") }}</a></li> | |||||
<li><a href="#List/{{ doctype }}/Report">{{ __("Report Builder") }}</a></li> | |||||
</ul> | </ul> | ||||
</div> | </div> | ||||
</li> | </li> | ||||
@@ -17,7 +17,7 @@ frappe.views.ListSidebar = Class.extend({ | |||||
this.cat_tags = []; | this.cat_tags = []; | ||||
}, | }, | ||||
make: function() { | 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 = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>') | this.sidebar = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>') | ||||
.html(sidebar_content) | .html(sidebar_content) | ||||
@@ -96,7 +96,8 @@ frappe.views.ListSidebar = Class.extend({ | |||||
$.each(reports, function(name, r) { | $.each(reports, function(name, r) { | ||||
if(!r.ref_doctype || r.ref_doctype==me.doctype) { | if(!r.ref_doctype || r.ref_doctype==me.doctype) { | ||||
var report_type = r.report_type==='Report Builder' | 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); | var route = r.route || report_type + '/' + (r.title || r.name); | ||||
if(added.indexOf(route)===-1) { | if(added.indexOf(route)===-1) { | ||||
@@ -113,11 +114,11 @@ frappe.views.ListSidebar = Class.extend({ | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
} | |||||
}; | |||||
// from reference doctype | // 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 | // from specially tagged reports | ||||
@@ -175,7 +176,7 @@ frappe.views.ListSidebar = Class.extend({ | |||||
fieldname: 'custom_column', | fieldname: 'custom_column', | ||||
label: __('Custom Column'), | label: __('Custom Column'), | ||||
default: 0, | default: 0, | ||||
onchange: function(e) { | |||||
onchange: function() { | |||||
var checked = d.get_value('custom_column'); | var checked = d.get_value('custom_column'); | ||||
if(checked) { | if(checked) { | ||||
$(d.body).find('.frappe-control[data-fieldname="field_name"]').hide(); | $(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 ? | var custom_column = values.custom_column !== undefined ? | ||||
values.custom_column : 1; | values.custom_column : 1; | ||||
var field_name; | |||||
if(custom_column) { | if(custom_column) { | ||||
var field_name = 'kanban_column'; | |||||
field_name = 'kanban_column'; | |||||
} else { | } else { | ||||
var field_name = | |||||
field_name = | |||||
select_fields | select_fields | ||||
.find(df => df.label === values.field_name) | .find(df => df.label === values.field_name) | ||||
.fieldname; | .fieldname; | ||||
} | } | ||||
me.add_custom_column_field(custom_column) | 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() { | .then(function() { | ||||
d.hide(); | d.hide(); | ||||
@@ -321,7 +322,7 @@ frappe.views.ListSidebar = Class.extend({ | |||||
if(has_common(frappe.user_roles, ["System Manager", "Administrator"])) { | if(has_common(frappe.user_roles, ["System Manager", "Administrator"])) { | ||||
$(`<li class="new-email-account"><a>${__("New Email Account")}</a></li>`) | $(`<li class="new-email-account"><a>${__("New Email Account")}</a></li>`) | ||||
.appendTo($dropdown) | |||||
.appendTo($dropdown); | |||||
} | } | ||||
let accounts = frappe.boot.email_accounts; | let accounts = frappe.boot.email_accounts; | ||||
@@ -334,7 +335,7 @@ frappe.views.ListSidebar = Class.extend({ | |||||
} | } | ||||
$(`<li><a href="#${route}">${account.email_id}</a></li>`).appendTo($dropdown); | $(`<li><a href="#${route}">${account.email_id}</a></li>`).appendTo($dropdown); | ||||
if(account.email_id === "Sent Mail") | if(account.email_id === "Sent Mail") | ||||
divider = false | |||||
divider = false; | |||||
}); | }); | ||||
$dropdown.find('.new-email-account').click(function() { | $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 is holding one user free plan or | ||||
// if account's expiry date within range of 30 days from today's date | // 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) { | if (frappe.boot.limits.users === 1 || upgrade_date >= frappe.boot.limits.expiry) { | ||||
let upgrade_box = $(`<div class="border" style=" | let upgrade_box = $(`<div class="border" style=" | ||||
padding: 0px 10px; | padding: 0px 10px; | ||||
@@ -386,12 +387,12 @@ frappe.views.ListSidebar = Class.extend({ | |||||
args: { | args: { | ||||
stats: me.stats, | stats: me.stats, | ||||
doctype: me.doctype, | doctype: me.doctype, | ||||
filters:me.default_filters | |||||
filters: me.default_filters || [] | |||||
}, | }, | ||||
callback: function(r) { | callback: function(r) { | ||||
me.defined_category = r.message; | me.defined_category = r.message; | ||||
if (r.message.defined_cat ){ | if (r.message.defined_cat ){ | ||||
me.defined_category = r.message.defined_cat | |||||
me.defined_category = r.message.defined_cat; | |||||
me.cats = {}; | me.cats = {}; | ||||
//structure the tag categories | //structure the tag categories | ||||
for (var i in me.defined_category){ | for (var i in me.defined_category){ | ||||
@@ -400,10 +401,10 @@ frappe.views.ListSidebar = Class.extend({ | |||||
}else{ | }else{ | ||||
me.cats[me.defined_category[i].category].push(me.defined_category[i].tag); | me.cats[me.defined_category[i].category].push(me.defined_category[i].tag); | ||||
} | } | ||||
me.cat_tags[i]=me.defined_category[i].tag | |||||
me.cat_tags[i]=me.defined_category[i].tag; | |||||
} | } | ||||
me.tempstats =r.message.stats | |||||
var len = me.cats.length; | |||||
me.tempstats =r.message.stats; | |||||
$.each(me.cats, function (i, v) { | $.each(me.cats, function (i, v) { | ||||
me.render_stat(i, (me.tempstats || {})["_user_tags"],v); | me.render_stat(i, (me.tempstats || {})["_user_tags"],v); | ||||
}); | }); | ||||
@@ -414,19 +415,18 @@ frappe.views.ListSidebar = Class.extend({ | |||||
//render normal stats | //render normal stats | ||||
me.render_stat("_user_tags", (r.message.stats|| {})["_user_tags"]); | me.render_stat("_user_tags", (r.message.stats|| {})["_user_tags"]); | ||||
} | } | ||||
me.list_view.set_sidebar_height(); | |||||
} | } | ||||
}); | }); | ||||
}, | }, | ||||
render_stat: function(field, stat, tags) { | render_stat: function(field, stat, tags) { | ||||
var me = this; | var me = this; | ||||
var sum = 0; | var sum = 0; | ||||
var stats = [] | |||||
var stats = []; | |||||
var label = frappe.meta.docfield_map[this.doctype][field] ? | var label = frappe.meta.docfield_map[this.doctype][field] ? | ||||
frappe.meta.docfield_map[this.doctype][field].label : field; | frappe.meta.docfield_map[this.doctype][field].label : field; | ||||
stat = (stat || []).sort(function(a, b) { return b[1] - a[1] }); | |||||
$.each(stat, function(i,v) { sum = sum + v[1]; }) | |||||
stat = (stat || []).sort(function(a, b) { return b[1] - a[1]; }); | |||||
$.each(stat, function(i,v) { sum = sum + v[1]; }); | |||||
if(tags) { | if(tags) { | ||||
for (var t in tags) { | for (var t in tags) { | ||||
@@ -454,12 +454,12 @@ frappe.views.ListSidebar = Class.extend({ | |||||
sum: sum, | sum: sum, | ||||
label: field==='_user_tags' ? (tags ? __(label) : __("Tags")) : __(label), | label: field==='_user_tags' ? (tags ? __(label) : __("Tags")) : __(label), | ||||
}; | }; | ||||
var sidebar_stat = $(frappe.render_template("list_sidebar_stat", context)) | |||||
$(frappe.render_template("list_sidebar_stat", context)) | |||||
.on("click", ".stat-link", function() { | .on("click", ".stat-link", function() { | ||||
var fieldname = $(this).attr('data-field'); | var fieldname = $(this).attr('data-field'); | ||||
var label = $(this).attr('data-label'); | var label = $(this).attr('data-label'); | ||||
if (label == "No Tags") { | if (label == "No Tags") { | ||||
me.list_view.filter_list.add_filter(me.list_view.doctype, fieldname, 'not like', '%,%') | |||||
me.list_view.filter_list.add_filter(me.list_view.doctype, fieldname, 'not like', '%,%'); | |||||
me.list_view.run(); | me.list_view.run(); | ||||
} else { | } else { | ||||
me.set_filter(fieldname, label); | me.set_filter(fieldname, label); | ||||
@@ -467,7 +467,7 @@ frappe.views.ListSidebar = Class.extend({ | |||||
}) | }) | ||||
.insertBefore(this.sidebar.find(".close-sidebar-button")); | .insertBefore(this.sidebar.find(".close-sidebar-button")); | ||||
}, | }, | ||||
set_fieldtype: function(df, fieldtype) { | |||||
set_fieldtype: function(df) { | |||||
// scrub | // scrub | ||||
if(df.fieldname=="docstatus") { | if(df.fieldname=="docstatus") { | ||||
@@ -476,11 +476,11 @@ frappe.views.ListSidebar = Class.extend({ | |||||
{value:0, label:"Draft"}, | {value:0, label:"Draft"}, | ||||
{value:1, label:"Submitted"}, | {value:1, label:"Submitted"}, | ||||
{value:2, label:"Cancelled"}, | {value:2, label:"Cancelled"}, | ||||
] | |||||
]; | |||||
} else if(df.fieldtype=='Check') { | } else if(df.fieldtype=='Check') { | ||||
df.fieldtype='Select'; | df.fieldtype='Select'; | ||||
df.options=[{value:0,label:'No'}, | df.options=[{value:0,label:'No'}, | ||||
{value:1,label:'Yes'}] | |||||
{value:1,label:'Yes'}]; | |||||
} else if(['Text','Small Text','Text Editor','Code','Tag','Comments', | } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', | ||||
'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { | 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { | ||||
df.fieldtype = 'Data'; | df.fieldtype = 'Data'; | ||||
@@ -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; | |||||
} | |||||
}; | |||||
}(); |
@@ -195,13 +195,11 @@ frappe.utils = { | |||||
}, | }, | ||||
set_footnote: function(footnote_area, wrapper, txt) { | set_footnote: function(footnote_area, wrapper, txt) { | ||||
if(!footnote_area) { | if(!footnote_area) { | ||||
footnote_area = $('<div class="text-muted footnote-area">') | |||||
footnote_area = $('<div class="text-muted footnote-area level">') | |||||
.appendTo(wrapper); | .appendTo(wrapper); | ||||
} | } | ||||
if(txt) { | if(txt) { | ||||
if(!txt.includes('<p>')) | |||||
txt = '<p>' + txt + '</p>'; | |||||
footnote_area.html(txt); | footnote_area.html(txt); | ||||
} else { | } else { | ||||
footnote_area.remove(); | footnote_area.remove(); | ||||
@@ -591,7 +589,47 @@ frappe.utils = { | |||||
catch (err) { | catch (err) { | ||||
return false; | 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 `<a href="${route}">${name}</a>`; | |||||
} | |||||
return route; | |||||
} | |||||
}; | }; | ||||
// String.prototype.includes polyfill | // String.prototype.includes polyfill | ||||
@@ -27,6 +27,8 @@ $.extend(frappe.model, { | |||||
{fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, | {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, | ||||
], | ], | ||||
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], | |||||
std_fields_table: [ | std_fields_table: [ | ||||
{fieldname:'parent', fieldtype:'Data', label:__('Parent')}, | {fieldname:'parent', fieldtype:'Data', label:__('Parent')}, | ||||
], | ], | ||||
@@ -39,8 +41,9 @@ $.extend(frappe.model, { | |||||
// setup refresh if the document is updated somewhere else | // setup refresh if the document is updated somewhere else | ||||
frappe.realtime.on("doc_update", function(data) { | frappe.realtime.on("doc_update", function(data) { | ||||
// set list dirty | // 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]; | var doc = locals[data.doctype] && locals[data.doctype][data.name]; | ||||
if(doc) { | if(doc) { | ||||
// current document is dirty, show message if its not me | // 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) { | 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.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; | 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) { | get_std_field: function(fieldname) { | ||||
var docfield = $.map([].concat(frappe.model.std_fields).concat(frappe.model.std_fields_table), | var docfield = $.map([].concat(frappe.model.std_fields).concat(frappe.model.std_fields_table), | ||||
function(d) { | function(d) { | ||||
@@ -576,6 +583,19 @@ $.extend(frappe.model, { | |||||
} | } | ||||
return all; | 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 | // legacy | ||||
@@ -5,6 +5,7 @@ $.extend(frappe.model.user_settings, { | |||||
var user_settings = frappe.model.user_settings[doctype] || {}; | var user_settings = frappe.model.user_settings[doctype] || {}; | ||||
if ($.isPlainObject(value)) { | if ($.isPlainObject(value)) { | ||||
user_settings[key] = user_settings[key] || {}; | |||||
$.extend(user_settings[key], value); | $.extend(user_settings[key], value); | ||||
} else { | } else { | ||||
user_settings[key] = value; | user_settings[key] = value; | ||||
@@ -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 $('<button class="btn btn-default"></button>') | |||||
.appendTo(this.wrapper.find('.list-toolbar')) | |||||
.html((icon ? ('<i class="' + icon + '"></i> ') : '') + 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) { | |||||
$(`<span class="octicon octicon-search text-muted small"></span>`) | |||||
.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); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
}); |
@@ -1,6 +1,6 @@ | |||||
<div class="filter-box"> | <div class="filter-box"> | ||||
<div class="list_filter row"> | <div class="list_filter row"> | ||||
<div class="fieldname_select_area col-sm-4 form-group ui-front"></div> | |||||
<div class="fieldname-select-area col-sm-4 form-group ui-front"></div> | |||||
<div class="col-sm-2 form-group"> | <div class="col-sm-2 form-group"> | ||||
<select class="condition form-control"> | <select class="condition form-control"> | ||||
<option value="=">{%= __("Equals") %}</option> | <option value="=">{%= __("Equals") %}</option> | ||||
@@ -17,7 +17,7 @@ | |||||
</select> | </select> | ||||
</div> | </div> | ||||
<div class="col-sm-6 col-xs-12"> | <div class="col-sm-6 col-xs-12"> | ||||
<div class="filter_field pull-left" style="width: calc(100% - 70px)"></div> | |||||
<div class="filter-field pull-left" style="width: calc(100% - 70px)"></div> | |||||
<div class="filter-actions pull-left"> | <div class="filter-actions pull-left"> | ||||
<a class="set-filter-and-run btn btn-sm btn-primary pull-left"> | <a class="set-filter-and-run btn btn-sm btn-primary pull-left"> | ||||
<i class=" fa fa-check visible-xs"></i> | <i class=" fa fa-check visible-xs"></i> | ||||
@@ -0,0 +1,155 @@ | |||||
// <select> widget with all fields of a doctype as options | |||||
frappe.ui.FieldSelect = Class.extend({ | |||||
// opts parent, doctype, filter_fields, with_blank, select | |||||
init(opts) { | |||||
var me = this; | |||||
$.extend(this, opts); | |||||
this.fields_by_name = {}; | |||||
this.options = []; | |||||
this.$input = $('<input class="form-control">') | |||||
.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('<li class="filter-field-select"><p>%(label)s</p></li>', 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; | |||||
} | |||||
}, | |||||
}); |
@@ -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 $(`<div class="filter-tag btn-group"> | |||||
<button class="btn btn-default btn-xs toggle-filter" | |||||
title="${ __("Edit Filter") }"> | |||||
</button> | |||||
<button class="btn btn-default btn-xs remove-filter" | |||||
title="${ __("Remove Filter") }"> | |||||
<i class="fa fa-remove text-muted"></i> | |||||
</button> | |||||
</div>`); | |||||
} | |||||
add_condition_help(condition) { | |||||
let $desc = this.field.desc_area; | |||||
if(!$desc) { | |||||
$desc = $('<div class="text-muted small">').appendTo(this.field.wrapper); | |||||
} | |||||
// set description | |||||
$desc.html((in_list(["in", "not in"], condition)==="in" | |||||
? __("values separated by commas") | |||||
: __("use % as wildcard"))+'</div>'); | |||||
} | |||||
}; | |||||
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'; | |||||
} | |||||
} | |||||
}; |
@@ -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 $(`<div class="tag-filters-area"> | |||||
<div class="active-tag-filters"> | |||||
<button class="btn btn-default btn-xs add-filter text-muted"> | |||||
${__("Add Filter")} | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<div class="filter-edit-area"></div>`); | |||||
} | |||||
}; |
@@ -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(` | |||||
<div class="show_filters"> | |||||
<div class="set-filters"> | |||||
<button | |||||
style="margin-right: 10px;" | |||||
class="btn btn-default btn-xs new-filter text-muted"> | |||||
${__("Add Filter")}</button> | |||||
</div> | |||||
</div> | |||||
<div class="filter_area"></div>`); | |||||
}, | |||||
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 = $('<div class="text-muted small">').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"))+'</div>'); | |||||
} 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 = $(`<div class="btn-group"> | |||||
<button class="btn btn-default btn-xs toggle-filter" | |||||
title="${ __("Edit Filter") }"> | |||||
</button> | |||||
<button class="btn btn-default btn-xs remove-filter" | |||||
title="${ __("Remove Filter") }"> | |||||
<i class="fa fa-remove text-muted"></i> | |||||
</button></div>`) | |||||
.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), | |||||
})); | |||||
} | |||||
}); | |||||
// <select> widget with all fields of a doctype as options | |||||
frappe.ui.FieldSelect = Class.extend({ | |||||
// opts parent, doctype, filter_fields, with_blank, select | |||||
init: function(opts) { | |||||
var me = this; | |||||
$.extend(this, opts); | |||||
this.fields_by_name = {}; | |||||
this.options = []; | |||||
this.$select = $('<input class="form-control">') | |||||
.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('<li class="filter-field-select"><p>%(label)s</p></li>', 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; | |||||
} | |||||
}, | |||||
}) |
@@ -97,7 +97,10 @@ frappe.ui.setup_like_popover = function($parent, selector) { | |||||
animation: true, | animation: true, | ||||
placement: "right", | placement: "right", | ||||
content: function() { | 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; | var user = frappe.session.user; | ||||
// hack | // hack | ||||
if ($wrapper.find(".not-liked").length) { | if ($wrapper.find(".not-liked").length) { | ||||
@@ -184,5 +184,9 @@ frappe.ui.SortSelector = Class.extend({ | |||||
return this.labels[fieldname] | return this.labels[fieldname] | ||||
|| frappe.meta.get_label(this.doctype, 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; | |||||
} | } | ||||
}) | }) |
@@ -377,7 +377,7 @@ frappe.upload = { | |||||
frappe.throw(__("File size exceeded the maximum allowed size of {0} MB", [max_file_size / 1048576])); | 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 | //loop through filenames and checkboxes then append to list | ||||
var fields = []; | var fields = []; | ||||
for (var i =0,j = fileobjs.length;i<j;i++) { | for (var i =0,j = fileobjs.length;i<j;i++) { | ||||
@@ -17,17 +17,23 @@ frappe.breadcrumbs = { | |||||
}, | }, | ||||
add: function(module, doctype, type) { | add: function(module, doctype, type) { | ||||
frappe.breadcrumbs.all[frappe.breadcrumbs.current_page()] = {module:module, doctype:doctype, type:type}; | |||||
let obj; | |||||
if (typeof module === 'object') { | |||||
obj = module; | |||||
} else { | |||||
obj = { | |||||
module:module, | |||||
doctype:doctype, | |||||
type:type | |||||
} | |||||
} | |||||
frappe.breadcrumbs.all[frappe.breadcrumbs.current_page()] = obj; | |||||
frappe.breadcrumbs.update(); | frappe.breadcrumbs.update(); | ||||
}, | }, | ||||
current_page: function() { | current_page: function() { | ||||
var route = frappe.get_route(); | |||||
// for List/DocType/{?} return List/DocType | |||||
if (route[0] === 'List') { | |||||
route = route.slice(0, 2); | |||||
} | |||||
return route.join("/"); | |||||
return frappe.get_route_str(); | |||||
}, | }, | ||||
update: function() { | update: function() { | ||||
@@ -38,11 +44,19 @@ frappe.breadcrumbs = { | |||||
} | } | ||||
var $breadcrumbs = $("#navbar-breadcrumbs").empty(); | var $breadcrumbs = $("#navbar-breadcrumbs").empty(); | ||||
if(!breadcrumbs) { | if(!breadcrumbs) { | ||||
$("body").addClass("no-breadcrumbs"); | $("body").addClass("no-breadcrumbs"); | ||||
return; | return; | ||||
} | } | ||||
if (breadcrumbs.type === 'Custom') { | |||||
const html = `<li><a href="${breadcrumbs.route}">${breadcrumbs.label}</a></li>`; | |||||
$breadcrumbs.append(html); | |||||
$("body").removeClass("no-breadcrumbs"); | |||||
return; | |||||
} | |||||
// get preferred module for breadcrumbs, based on sent via module | // get preferred module for breadcrumbs, based on sent via module | ||||
var from_module = frappe.breadcrumbs.get_doctype_module(breadcrumbs.doctype); | var from_module = frappe.breadcrumbs.get_doctype_module(breadcrumbs.doctype); | ||||
@@ -4,82 +4,63 @@ | |||||
frappe.provide("frappe.views.calendar"); | frappe.provide("frappe.views.calendar"); | ||||
frappe.provide("frappe.views.calendars"); | 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(); | 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.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 = { | const options = { | ||||
doctype: this.doctype, | 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 => { | return new Promise(resolve => { | ||||
if (calendar_view === 'Default') { | |||||
if (calendar_name === 'Default') { | |||||
Object.assign(options, frappe.views.calendar[this.doctype]); | Object.assign(options, frappe.views.calendar[this.doctype]); | ||||
resolve(options); | resolve(options); | ||||
} else { | } 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, { | Object.assign(options, { | ||||
field_map: { | field_map: { | ||||
id: "name", | id: "name", | ||||
@@ -88,18 +69,20 @@ frappe.views.CalendarView = frappe.views.ListRenderer.extend({ | |||||
title: doc.subject_field | title: doc.subject_field | ||||
} | } | ||||
}); | }); | ||||
resolve(options); | 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({ | frappe.views.Calendar = Class.extend({ | ||||
init: function(options) { | init: function(options) { | ||||
@@ -123,11 +106,10 @@ frappe.views.Calendar = Class.extend({ | |||||
$(this.parent).on("show", function() { | $(this.parent).on("show", function() { | ||||
me.$cal.fullCalendar("refetchEvents"); | me.$cal.fullCalendar("refetchEvents"); | ||||
}) | |||||
}); | |||||
}, | }, | ||||
make: function() { | make: function() { | ||||
var me = this; | |||||
this.$wrapper = this.parent; | this.$wrapper = this.parent; | ||||
this.$cal = $("<div>").appendTo(this.$wrapper); | this.$cal = $("<div>").appendTo(this.$wrapper); | ||||
this.footnote_area = frappe.utils.set_footnote(this.footnote_area, 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-button-group").addClass("btn-group"); | ||||
this.$wrapper.find('.fc-prev-button span') | 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') | 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"); | var btn_group = this.$wrapper.find(".fc-button-group"); | ||||
btn_group.find(".fc-state-active").addClass("active"); | btn_group.find(".fc-state-active").addClass("active"); | ||||
@@ -197,22 +179,22 @@ frappe.views.Calendar = Class.extend({ | |||||
events = me.prepare_events(events); | events = me.prepare_events(events); | ||||
callback(events); | callback(events); | ||||
} | } | ||||
}) | |||||
}); | |||||
}, | }, | ||||
eventRender: function(event, element) { | eventRender: function(event, element) { | ||||
element.attr('title', event.tooltip); | element.attr('title', event.tooltip); | ||||
}, | }, | ||||
eventClick: function(event, jsEvent, view) { | |||||
eventClick: function(event) { | |||||
// edit event description or delete | // edit event description or delete | ||||
var doctype = event.doctype || me.doctype; | var doctype = event.doctype || me.doctype; | ||||
if(frappe.model.can_read(doctype)) { | if(frappe.model.can_read(doctype)) { | ||||
frappe.set_route("Form", doctype, event.name); | 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); | me.update_event(event, revertFunc); | ||||
}, | }, | ||||
eventResize: function(event, delta, revertFunc, jsEvent, ui, view) { | |||||
eventResize: function(event, delta, revertFunc) { | |||||
me.update_event(event, revertFunc); | me.update_event(event, revertFunc); | ||||
}, | }, | ||||
select: function(startDate, endDate, jsEvent, view) { | select: function(startDate, endDate, jsEvent, view) { | ||||
@@ -269,7 +251,7 @@ frappe.views.Calendar = Class.extend({ | |||||
doctype: this.doctype, | doctype: this.doctype, | ||||
start: this.get_system_datetime(start), | start: this.get_system_datetime(start), | ||||
end: this.get_system_datetime(end), | 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 | field_map: this.field_map | ||||
}; | }; | ||||
return args; | return args; | ||||
@@ -384,4 +366,4 @@ frappe.views.Calendar = Class.extend({ | |||||
event.end = event.end ? $.fullCalendar.moment(event.end).add(1, "day").stripTime() : null; | event.end = event.end ? $.fullCalendar.moment(event.end).add(1, "day").stripTime() : null; | ||||
} | } | ||||
} | } | ||||
}) | |||||
}); |
@@ -418,8 +418,7 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
if(!form_values) return; | if(!form_values) return; | ||||
var selected_attachments = | 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"); | return $(element).attr("data-file-name"); | ||||
}); | }); | ||||
@@ -11,12 +11,9 @@ frappe.views.Factory = Class.extend({ | |||||
show: function() { | show: function() { | ||||
var page_name = frappe.get_route_str(), | var page_name = frappe.get_route_str(), | ||||
me = this; | 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/")) { | 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) { | if(me.on_show) { | ||||
me.on_show(); | me.on_show(); | ||||
} | } | ||||
@@ -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 = ` | |||||
<i class="${icon_class} text-muted" style="width: 16px;"></i> | |||||
<span>${title}</span> | |||||
${d.is_private ? '<i class="fa fa-lock fa-fw text-warning"></i>' : ''} | |||||
`; | |||||
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 = ` | |||||
<div class="list-row-col list-subject level"> | |||||
<input class="level-item list-check-all hidden-xs" type="checkbox" title="${__("Select All")}"> | |||||
<span class="level-item">${__('File Name')}</span> | |||||
</div> | |||||
<div class="list-row-col ellipsis hidden-xs text-right"> | |||||
<span>${__('File Size')}</span> | |||||
</div> | |||||
`; | |||||
return this.get_header_html_skeleton(subject_html, '<span class="list-count"></span>'); | |||||
} | |||||
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 ` | |||||
<div class="list-row-col ellipsis list-subject level"> | |||||
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" data-name="${file.name}"> | |||||
<span class="level-item ellipsis" title="${file.file_name}"> | |||||
<a class="ellipsis" href="${route_url}" title="${file.file_name}"> | |||||
${file._title} | |||||
</a> | |||||
</span> | |||||
</div> | |||||
<div class="list-row-col ellipsis hidden-xs text-muted text-right"> | |||||
<span>${file_size}</span> | |||||
</div> | |||||
`; | |||||
} | |||||
get_right_html(file) { | |||||
return ` | |||||
<div class="level-item list-row-activity"> | |||||
${comment_when(file.modified)} | |||||
</div> | |||||
`; | |||||
} | |||||
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'); | |||||
} | |||||
} | |||||
}; |
@@ -1,73 +1,117 @@ | |||||
frappe.provide('frappe.views'); | 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('<svg class="gantt-container" width="20" height="20"></svg>'); | |||||
} | |||||
update_data(data) { | |||||
super.update_data(data); | |||||
this.prepare_tasks(); | this.prepare_tasks(); | ||||
this.prepare_dom(); | |||||
}, | |||||
} | |||||
render_view: function(values) { | |||||
prepare_tasks() { | |||||
var me = this; | 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('<svg class="gantt-container" width="20" height="20"></svg>') | |||||
}, | |||||
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, { | this.gantt = new Gantt(".gantt-container", this.tasks, { | ||||
view_mode: this.gantt_view_mode, | |||||
view_mode: gantt_view_mode, | |||||
date_format: "YYYY-MM-DD", | date_format: "YYYY-MM-DD", | ||||
on_click: function (task) { | on_click: function (task) { | ||||
frappe.set_route('Form', task.doctype, task.id); | 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'; | var progress_fieldname = 'progress'; | ||||
if($.isFunction(field_map.progress)) { | |||||
if ($.isFunction(field_map.progress)) { | |||||
progress_fieldname = null; | progress_fieldname = null; | ||||
} else if(field_map.progress) { | |||||
} else if (field_map.progress) { | |||||
progress_fieldname = 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 | // save view mode | ||||
frappe.model.user_settings.save(me.doctype, 'Gantt', { | |||||
me.save_view_user_settings({ | |||||
gantt_view_mode: mode | gantt_view_mode: mode | ||||
}); | }); | ||||
}, | }, | ||||
custom_popup_html: function(task) { | |||||
custom_popup_html: function (task) { | |||||
var item = me.get_item(task.id); | var item = me.get_item(task.id); | ||||
var html = | var html = | ||||
@@ -76,48 +120,51 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ | |||||
// custom html in doctype settings | // custom html in doctype settings | ||||
var custom = me.settings.gantt_custom_popup_html; | var custom = me.settings.gantt_custom_popup_html; | ||||
if(custom && $.isFunction(custom)) { | |||||
if (custom && $.isFunction(custom)) { | |||||
var ganttobj = task; | var ganttobj = task; | ||||
html = custom(ganttobj, item); | html = custom(ganttobj, item); | ||||
} | } | ||||
return '<div class="details-container">' + html + '</div>'; | return '<div class="details-container">' + html + '</div>'; | ||||
} | } | ||||
}); | }); | ||||
this.render_dropdown(); | |||||
this.setup_view_mode_buttons(); | |||||
this.set_colors(); | this.set_colors(); | ||||
}, | |||||
render_dropdown: function() { | |||||
var me = this; | |||||
var view_modes = this.gantt.config.view_modes || []; | |||||
var dropdown = "<div class='dropdown pull-right'>" + | |||||
"<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" + | |||||
"<span class='dropdown-text'>"+__(this.gantt_view_mode)+"</span><i class='caret'></i></a>" + | |||||
"<ul class='dropdown-menu'></ul>" + | |||||
"</div>"; | |||||
} | |||||
setup_view_mode_buttons() { | |||||
// view modes (for translation) __("Day"), __("Week"), __("Month"), | // view modes (for translation) __("Day"), __("Week"), __("Month"), | ||||
//__("Half Day"), __("Quarter Day") | //__("Half Day"), __("Quarter Day") | ||||
var dropdown_list = ""; | |||||
view_modes.forEach(function(view_mode) { | |||||
dropdown_list += "<li>" + | |||||
"<a class='option' data-value='" + view_mode + "'>" + | |||||
__(view_mode) + "</a></li>"; | |||||
}); | |||||
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 = | |||||
`<div class="btn-group gantt-view-mode"> | |||||
${view_modes.map(value => `<button type="button" | |||||
class="btn btn-default btn-sm btn-view-mode ${active_class(value)}" | |||||
data-value="${value}"> | |||||
${__(value)} | |||||
</button>`).join('')} | |||||
</div>`; | |||||
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 | const classes = this.tasks | ||||
.map(t => t.custom_class) | .map(t => t.custom_class) | ||||
.filter(c => c && c.startsWith('color-')); | .filter(c => c && c.startsWith('color-')); | ||||
@@ -137,102 +184,19 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ | |||||
}).join(""); | }).join(""); | ||||
style = `<style>${style}</style>`; | style = `<style>${style}</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; | 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" | |||||
] | |||||
}); | |||||
} | |||||
get required_libs() { | |||||
return [ | |||||
"assets/frappe/js/lib/snap.svg-min.js", | |||||
"assets/frappe/js/lib/frappe-gantt/frappe-gantt.js" | |||||
]; | |||||
} | |||||
}; |
@@ -3,11 +3,27 @@ | |||||
*/ | */ | ||||
frappe.provide("frappe.views"); | 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() | this.get_attached_images() | ||||
.then(() => { | .then(() => { | ||||
this.render_image_view(); | 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); | 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 | // absolute url if cordova, else relative | ||||
data._image_url = this.get_image_url(data); | data._image_url = this.get_image_url(data); | ||||
return 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 = $('<div>') | |||||
.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()} | |||||
<div class="image-view-container small"> | |||||
${html} | |||||
</div> | |||||
`); | |||||
} | |||||
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 ? | |||||
`<img data-name="${encoded_name}" src="${ item._image_url }" alt="${ title }">` : | |||||
`<span class="placeholder-text"> | |||||
${ frappe.get_abbr(title) } | |||||
</span>`; | |||||
return ` | |||||
<div class="image-view-item"> | |||||
<div class="image-view-header"> | |||||
<div class="list-row-col list-subject ellipsis level"> | |||||
${this.get_subject_html(item)} | |||||
</div> | |||||
</div> | |||||
<div class="image-view-body"> | |||||
<a data-name="${encoded_name}" | |||||
title="${encoded_name}" | |||||
href="${this.get_form_link(item)}" | |||||
> | |||||
<div class="image-field ${_class}" | |||||
data-name="${encoded_name}" | |||||
> | |||||
${_html} | |||||
<button class="btn btn-default zoom-view" data-name="${encoded_name}"> | |||||
<i class="fa fa-search-plus"></i> | |||||
</button> | |||||
</div> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
`; | |||||
} | |||||
get_image_url(data) { | |||||
var url; | 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 | // absolute url for mobile | ||||
if (window.cordova && !frappe.utils.is_url(url)) { | if (window.cordova && !frappe.utils.is_url(url)) { | ||||
url = frappe.base_url + url; | url = frappe.base_url + url; | ||||
} | } | ||||
if (url) { | if (url) { | ||||
return url | |||||
return url; | |||||
} | } | ||||
return null; | return null; | ||||
}, | |||||
get_attached_images: function () { | |||||
} | |||||
get_attached_images() { | |||||
return frappe.call({ | return frappe.call({ | ||||
method: 'frappe.core.doctype.file.file.get_attached_images', | 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 => { | }).then(r => { | ||||
this.images_map = Object.assign(this.images_map || {}, r.message); | 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(` | |||||
<div class="list-row-col list-subject level "> | |||||
<input class="level-item list-check-all hidden-xs" type="checkbox" title="Select All"> | |||||
<span class="level-item list-liked-by-me"> | |||||
<i class="octicon octicon-heart text-extra-muted" title="Likes"></i> | |||||
</span> | |||||
<span class="level-item"></span> | |||||
</div> | |||||
`); | |||||
} | |||||
setup_gallery() { | |||||
var me = this; | var me = this; | ||||
this.gallery = new frappe.views.GalleryView({ | this.gallery = new frappe.views.GalleryView({ | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
items: this.items, | items: this.items, | ||||
wrapper: this.container, | |||||
wrapper: this.$result, | |||||
images_map: this.images_map | 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.preventDefault(); | ||||
e.stopPropagation(); | e.stopPropagation(); | ||||
var name = $(this).data().name; | var name = $(this).data().name; | ||||
name = decodeURIComponent(name); | |||||
me.gallery.show(name); | me.gallery.show(name); | ||||
return false; | return false; | ||||
}); | }); | ||||
} | } | ||||
}); | |||||
}; | |||||
frappe.views.GalleryView = Class.extend({ | frappe.views.GalleryView = Class.extend({ | ||||
init: function(opts) { | |||||
init: function (opts) { | |||||
$.extend(this, opts); | $.extend(this, opts); | ||||
var me = this; | var me = this; | ||||
this.lib_ready = this.load_lib(); | this.lib_ready = this.load_lib(); | ||||
this.lib_ready.then(function() { | |||||
this.lib_ready.then(function () { | |||||
me.prepare(); | me.prepare(); | ||||
}); | }); | ||||
}, | }, | ||||
prepare: function() { | |||||
prepare: function () { | |||||
// keep only one pswp dom element | // keep only one pswp dom element | ||||
this.pswp_root = $('body > .pswp'); | this.pswp_root = $('body > .pswp'); | ||||
if(this.pswp_root.length === 0) { | |||||
if (this.pswp_root.length === 0) { | |||||
var pswp = frappe.render_template('photoswipe_dom'); | var pswp = frappe.render_template('photoswipe_dom'); | ||||
this.pswp_root = $(pswp).appendTo('body'); | this.pswp_root = $(pswp).appendTo('body'); | ||||
} | } | ||||
}, | }, | ||||
prepare_pswp_items: function(_items, _images_map) { | |||||
prepare_pswp_items: function (_items, _images_map) { | |||||
var me = this; | var me = this; | ||||
if (_items) { | if (_items) { | ||||
@@ -126,18 +175,18 @@ frappe.views.GalleryView = Class.extend({ | |||||
} | } | ||||
return new Promise(resolve => { | 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 el = me.wrapper.find(query).get(0); | ||||
let width, height; | let width, height; | ||||
if(el) { | |||||
if (el) { | |||||
width = el.naturalWidth; | width = el.naturalWidth; | ||||
height = el.naturalHeight; | 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; | width = el.getBoundingClientRect().width; | ||||
height = el.getBoundingClientRect().height; | height = el.getBoundingClientRect().height; | ||||
} | } | ||||
@@ -149,26 +198,26 @@ frappe.views.GalleryView = Class.extend({ | |||||
w: width, | w: width, | ||||
h: height, | h: height, | ||||
el: el | el: el | ||||
} | |||||
}; | |||||
}); | }); | ||||
this.pswp_items = items; | this.pswp_items = items; | ||||
resolve(); | resolve(); | ||||
}); | }); | ||||
}, | }, | ||||
show: function(docname) { | |||||
show: function (docname) { | |||||
this.lib_ready | this.lib_ready | ||||
.then(() => this.prepare_pswp_items()) | .then(() => this.prepare_pswp_items()) | ||||
.then(() => this._show(docname)); | .then(() => this._show(docname)); | ||||
}, | }, | ||||
_show: function(docname) { | |||||
_show: function (docname) { | |||||
const me = this; | const me = this; | ||||
const items = this.pswp_items; | const items = this.pswp_items; | ||||
const item_index = items.findIndex(item => item.name === docname); | const item_index = items.findIndex(item => item.name === docname); | ||||
var options = { | var options = { | ||||
index: item_index, | 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); | let thumbnail = me.wrapper.find(query).get(0); | ||||
if (!thumbnail) { | if (!thumbnail) { | ||||
@@ -178,12 +227,16 @@ frappe.views.GalleryView = Class.extend({ | |||||
var pageYScroll = window.pageYOffset || document.documentElement.scrollTop, | var pageYScroll = window.pageYOffset || document.documentElement.scrollTop, | ||||
rect = thumbnail.getBoundingClientRect(); | 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, | history: false, | ||||
shareEl: false, | shareEl: false, | ||||
showHideOpacity: true | showHideOpacity: true | ||||
} | |||||
}; | |||||
// init | // init | ||||
this.pswp = new PhotoSwipe( | this.pswp = new PhotoSwipe( | ||||
@@ -195,12 +248,12 @@ frappe.views.GalleryView = Class.extend({ | |||||
this.browse_images(); | this.browse_images(); | ||||
this.pswp.init(); | this.pswp.init(); | ||||
}, | }, | ||||
browse_images: function() { | |||||
browse_images: function () { | |||||
const $more_items = this.pswp_root.find('.pswp__more-items'); | const $more_items = this.pswp_root.find('.pswp__more-items'); | ||||
const images_map = this.images_map; | const images_map = this.images_map; | ||||
let last_hide_timeout = null; | let last_hide_timeout = null; | ||||
this.pswp.listen('afterChange', function() { | |||||
this.pswp.listen('afterChange', function () { | |||||
const images = images_map[this.currItem.name]; | const images = images_map[this.currItem.name]; | ||||
if (!images || images.length === 1) { | if (!images || images.length === 1) { | ||||
$more_items.html(''); | $more_items.html(''); | ||||
@@ -214,7 +267,9 @@ frappe.views.GalleryView = Class.extend({ | |||||
this.pswp.listen('beforeChange', hide_more_items); | this.pswp.listen('beforeChange', hide_more_items); | ||||
this.pswp.listen('initialZoomOut', 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 | // Replace current image on click | ||||
$more_items.on('click', '.pswp__more-item', (e) => { | $more_items.on('click', '.pswp__more-item', (e) => { | ||||
@@ -255,7 +310,7 @@ frappe.views.GalleryView = Class.extend({ | |||||
</div>`; | </div>`; | ||||
} | } | ||||
}, | }, | ||||
load_lib: function() { | |||||
load_lib: function () { | |||||
return new Promise(resolve => { | return new Promise(resolve => { | ||||
var asset_dir = 'assets/frappe/js/lib/photoswipe/'; | var asset_dir = 'assets/frappe/js/lib/photoswipe/'; | ||||
frappe.require([ | frappe.require([ | ||||
@@ -266,4 +321,4 @@ frappe.views.GalleryView = Class.extend({ | |||||
], resolve); | ], resolve); | ||||
}); | }); | ||||
} | } | ||||
}); | |||||
}); |
@@ -32,13 +32,4 @@ | |||||
</div> | </div> | ||||
</a> | </a> | ||||
</div> | </div> | ||||
<div class="image-view-footer hide"> | |||||
<div class="row"> | |||||
<div class="col-xs-4">{%= indicator %}</div> | |||||
<div class="col-xs-8 text-right"> | |||||
<!-- comments count and assigned to section --> | |||||
{%= frappe.render_template("item_assigned_to_comment_count", { data: data }) %} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | </div> |
@@ -1,12 +0,0 @@ | |||||
<div class="msg-box no-border"> | |||||
{% if(!frappe.model.can_create(doctype) && doctype == "Email Account") { %} | |||||
<p>{{ __("No Email Accounts Assigned") }}</p> | |||||
{% } else { %} | |||||
<p>{{ msg }}</p> | |||||
<p> | |||||
<button class="btn btn-primary btn-sm btn-no-result" list_view_doc="{{ doctype }}"> | |||||
{{ label }} | |||||
</button> | |||||
</p> | |||||
{% } %} | |||||
</div> |
@@ -1,168 +1,204 @@ | |||||
/** | /** | ||||
* frappe.views.EmailInboxView | |||||
* frappe.views.InboxView | |||||
*/ | */ | ||||
frappe.provide("frappe.views"); | 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 | // 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 | 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 = $('<div>') | |||||
.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(); | 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(` | |||||
<div class="list-row-col list-subject level"> | |||||
<input class="level-item list-check-all hidden-xs" type="checkbox" title="Select All"> | |||||
<span class="level-item">${__('Subject')}</span> | |||||
</div> | |||||
<div class="list-row-col hidden-xs"> | |||||
<span>${this.is_sent_emails ? __("To") : __("From")}</span> | |||||
</div> | |||||
`); | |||||
} | |||||
render_email_row(email) { | |||||
if (!email.css_seen && email.seen) | |||||
email.css_seen = "seen"; | |||||
const columns_html = ` | |||||
<div class="list-row-col list-subject level"> | |||||
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" data-name="${email.name}"> | |||||
<span class="level-item"> | |||||
<a class="${ email.seen ? 'seen' : ''} ellipsis" href="${this.get_form_link(email)}"> | |||||
${email.subject} | |||||
</a> | |||||
</span> | |||||
</div> | |||||
<div class="list-row-col hidden-xs"> | |||||
<span>${this.is_sent_emails ? email.recipients : email.sender }</span> | |||||
</div> | |||||
`; | |||||
return this.get_list_row_html_skeleton(columns_html, this.get_meta_html(email)); | |||||
} | |||||
get_meta_html(email) { | |||||
const attachment = email.has_attachment ? | |||||
`<span class="fa fa-paperclip fa-large" title="${__('Has Attachments')}"></span>` : ''; | |||||
const form_link = frappe.utils.get_form_link(email.reference_doctype, email.reference_name); | |||||
const link = email.reference_doctype && email.reference_doctype !== this.doctype ? | |||||
`<a class="text-muted grey" href="${form_link}" | |||||
title="${__('Linked with {0}', [email.reference_doctype])}"> | |||||
<i class="fa fa-link fa-large"></i> | |||||
</a>` : ''; | |||||
const modified = comment_when(email.modified, true); | |||||
return ` | |||||
<div class="level-item hidden-xs list-row-activity"> | |||||
${link} | |||||
${attachment} | |||||
${modified} | |||||
</div> | |||||
`; | |||||
} | |||||
get_inbox_filters() { | |||||
var email_account = this.email_account; | |||||
var default_filters = [ | var default_filters = [ | ||||
["Communication", "communication_type", "=", "Communication", true], | ["Communication", "communication_type", "=", "Communication", true], | ||||
["Communication", "communication_medium", "=", "Email", true], | ["Communication", "communication_medium", "=", "Email", true], | ||||
] | |||||
var filters = [] | |||||
]; | |||||
var filters = []; | |||||
if (email_account === "Sent") { | if (email_account === "Sent") { | ||||
filters = default_filters.concat([ | filters = default_filters.concat([ | ||||
["Communication", "sent_or_received", "=", "Sent", true], | ["Communication", "sent_or_received", "=", "Sent", true], | ||||
["Communication", "email_status", "not in", "Spam,Trash", 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([ | filters = default_filters.concat([ | ||||
["Communication", "email_status", "=", email_account, true], | ["Communication", "email_status", "=", email_account, true], | ||||
["Communication", "email_account", "in", frappe.boot.all_accounts, true] | ["Communication", "email_account", "in", frappe.boot.all_accounts, true] | ||||
]) | |||||
} | |||||
else { | |||||
var op = "=" | |||||
]); | |||||
} else { | |||||
var op = "="; | |||||
if (email_account == "All Accounts") { | if (email_account == "All Accounts") { | ||||
op = "in"; | op = "in"; | ||||
email_account = frappe.boot.all_accounts | |||||
email_account = frappe.boot.all_accounts; | |||||
} | } | ||||
filters = default_filters.concat([ | filters = default_filters.concat([ | ||||
["Communication", "sent_or_received", "=", "Received", true], | ["Communication", "sent_or_received", "=", "Received", true], | ||||
["Communication", "email_account", op, email_account, true], | ["Communication", "email_account", op, email_account, true], | ||||
["Communication", "email_status", "not in", "Spam,Trash", 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 <b>${route[3] || ''}</b> 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; | var args; | ||||
if (in_list(["Spam", "Trash"], email_account)) { | 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 | // email account is not configured | ||||
this.no_result_doctype = "Email Account" | |||||
args = { | args = { | ||||
doctype: "Email Account", | doctype: "Email Account", | ||||
msg: __("No Email Account"), | msg: __("No Email Account"), | ||||
label: __("New Email Account"), | label: __("New Email Account"), | ||||
} | |||||
}; | |||||
} else { | } else { | ||||
// no sent mail | // no sent mail | ||||
this.no_result_doctype = "Communication"; | |||||
args = { | args = { | ||||
doctype: "Communication", | doctype: "Communication", | ||||
msg: __("No Emails"), | msg: __("No Emails"), | ||||
label: __("Compose Email") | 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) ? | |||||
`<p>${args.msg}</p> | |||||
<p> | |||||
<button class="btn btn-primary btn-sm btn-new-doc"> | |||||
${args.label} | |||||
</button> | |||||
</p> | |||||
` : | |||||
`<p>${ __("No Email Accounts Assigned") }</p>`; | |||||
return ` | |||||
<div class="msg-box no-border"> | |||||
${html} | |||||
</div> | |||||
`; | |||||
} | |||||
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({ | new frappe.views.CommunicationComposer({ | ||||
doc: {} | doc: {} | ||||
}) | |||||
} else { | |||||
frappe.route_options = { 'email_id': frappe.session.user_email } | |||||
frappe.new_doc(this.no_result_doctype) | |||||
}); | |||||
} | } | ||||
} | } | ||||
}); | |||||
}; |
@@ -1,24 +0,0 @@ | |||||
<div class="list-row list-row-head" data-list-renderer="Inbox"> | |||||
<div class="row doclist-row"> | |||||
<div class="col-sm-10 list-row-left"> | |||||
<!-- title + columns --> | |||||
<div class="row"> | |||||
<div class="col-sm-8 col-xs-12 list-col ellipsis h6 text-muted"> | |||||
<div class="list-value"> | |||||
{% if (_checkbox) { %} | |||||
<input class="list-select-all" type="checkbox" | |||||
title="{%= __("Select All") %}"> | |||||
{% } %} | |||||
<span class="list-col-title">{%= __("Subject") %}</span> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-4 hidden-xs list-col ellipsis h6 text-muted"> | |||||
<div class="list-value"> | |||||
<span class="list-col-title">{%= __(is_sent_emails ? "To": "From") %}</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-2 hidden-xs list-row-right"></div> | |||||
</div> | |||||
</div> |
@@ -1,49 +0,0 @@ | |||||
<div class="list-row"> | |||||
<div class="row doclist-row {% if (data._checkbox) { %} has-checkbox {% } %}"> | |||||
<div class="col-sm-10 col-xs-10 list-row-left"> | |||||
<div class="row"> | |||||
<div class="col-sm-8 list-col ellipsis h6 text-muted"> | |||||
<span class="list-value"> | |||||
{% if (data._checkbox) { %} | |||||
<input class="list-row-checkbox" type="checkbox" data-name="{{data.name}}"> | |||||
{% } %} | |||||
<a class="grey list-id {{ data.css_seen }} inbox-value" href="#Form/{%= data._doctype_encoded %}/{%= data._name_encoded %}"> | |||||
{%= data.subject %} | |||||
</a> | |||||
</span> | |||||
</div> | |||||
<div class="col-sm-4 hidden-xs list-col ellipsis h6 text-muted"> | |||||
<span class="filterable text-muted" data-filter="sender,=,{%= data.sender %} inbox-value"> | |||||
{%= is_sent_emails? data.recipients: data.sender %} | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-2 col-xs-2 text-right list-row-right" style="padding-left:0px"> | |||||
<div class="visible-xs"> | |||||
<span class="text-muted inbox-attachment inbox-value"> | |||||
{% if(data.has_attachment) { %} | |||||
<i class="fa fa-paperclip fa-large"></i> | |||||
{% } %} | |||||
</span> | |||||
</div> | |||||
<div class="hidden-xs"> | |||||
<span class="text-muted inbox-attachment inbox-value"> | |||||
{% if(data.reference_doctype && data.reference_name) { %} | |||||
<a class="text-muted grey" href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}"> | |||||
<i class="fa fa-link fa-large"></i> | |||||
</a> | |||||
{% } %} | |||||
</span> | |||||
<span class="text-muted inbox-attachment inbox-value"> | |||||
{% if(data.has_attachment) { %} | |||||
<i class="fa fa-paperclip fa-large"></i> | |||||
{% } %} | |||||
</span> | |||||
<span class="list-row-modified text-muted inbox-value"> | |||||
{%= comment_when(data.modified, true) %} | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> |
@@ -33,11 +33,6 @@ frappe.provide("frappe.views"); | |||||
}); | }); | ||||
var columns = prepare_columns(board.columns); | 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({ | updater.set({ | ||||
doctype: opts.doctype, | doctype: opts.doctype, | ||||
board: board, | board: board, | ||||
@@ -57,10 +52,8 @@ frappe.provide("frappe.views"); | |||||
}, | }, | ||||
update_cards: function (updater, cards) { | update_cards: function (updater, cards) { | ||||
var state = this; | 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) | .concat(this.cards) | ||||
.uniqBy(card => card.name); | .uniqBy(card => card.name); | ||||
@@ -90,18 +83,18 @@ frappe.provide("frappe.views"); | |||||
var board = this.board; | var board = this.board; | ||||
fetch_customization(doctype) | fetch_customization(doctype) | ||||
.then(function (doc) { | .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(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) { | }).then(function (r) { | ||||
var cols = r.message; | var cols = r.message; | ||||
updater.set({ | updater.set({ | ||||
columns: prepare_columns(cols) | columns: prepare_columns(cols) | ||||
}); | }); | ||||
}, function (err) { | }, function (err) { | ||||
console.error(err); | |||||
console.error(err); // eslint-disable-line | |||||
}); | }); | ||||
}, | }, | ||||
set_filter_state: function (updater) { | set_filter_state: function (updater) { | ||||
@@ -115,14 +108,14 @@ frappe.provide("frappe.views"); | |||||
save_filters: function (updater) { | save_filters: function (updater) { | ||||
if(saving_filters) return; | if(saving_filters) return; | ||||
saving_filters = true; | 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({ | frappe.call({ | ||||
method: method_prefix + 'save_filters', | method: method_prefix + 'save_filters', | ||||
args: { | args: { | ||||
board_name: this.board.name, | board_name: this.board.name, | ||||
filters: filters | filters: filters | ||||
} | } | ||||
}).then(function(r) { | |||||
}).then(function() { | |||||
saving_filters = false; | saving_filters = false; | ||||
updater.set({ filters_modified: false }); | updater.set({ filters_modified: false }); | ||||
frappe.show_alert({ | frappe.show_alert({ | ||||
@@ -135,7 +128,6 @@ frappe.provide("frappe.views"); | |||||
var doc = frappe.model.get_new_doc(this.doctype); | var doc = frappe.model.get_new_doc(this.doctype); | ||||
var field = this.card_meta.title_field; | var field = this.card_meta.title_field; | ||||
var quick_entry = this.card_meta.quick_entry; | var quick_entry = this.card_meta.quick_entry; | ||||
var board = this.board; | |||||
var state = this; | var state = this; | ||||
var doc_fields = {}; | var doc_fields = {}; | ||||
@@ -198,7 +190,6 @@ frappe.provide("frappe.views"); | |||||
order: order | order: order | ||||
}, | }, | ||||
callback: (r) => { | callback: (r) => { | ||||
var state = this; | |||||
var board = r.message[0]; | var board = r.message[0]; | ||||
var updated_cards = r.message[1]; | var updated_cards = r.message[1]; | ||||
var cards = update_cards_column(updated_cards); | var cards = update_cards_column(updated_cards); | ||||
@@ -208,8 +199,7 @@ frappe.provide("frappe.views"); | |||||
columns: columns | columns: columns | ||||
}); | }); | ||||
} | } | ||||
}) | |||||
.fail(function(e) { | |||||
}).fail(function() { | |||||
// revert original order | // revert original order | ||||
updater.set({ | updater.set({ | ||||
cards: _cards, | cards: _cards, | ||||
@@ -246,7 +236,7 @@ frappe.provide("frappe.views"); | |||||
updater.set({ | updater.set({ | ||||
columns: columns | columns: columns | ||||
}); | }); | ||||
}) | |||||
}); | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
@@ -262,12 +252,12 @@ frappe.provide("frappe.views"); | |||||
// update cards internally | // update cards internally | ||||
opts.cards = cards; | 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); | fluxify.doAction('update_cards', cards); | ||||
} else { | } else { | ||||
init(); | init(); | ||||
} | } | ||||
} | |||||
}; | |||||
function init() { | function init() { | ||||
fluxify.doAction('init', opts); | fluxify.doAction('init', opts); | ||||
@@ -286,7 +276,7 @@ frappe.provide("frappe.views"); | |||||
self.$kanban_board.appendTo(self.wrapper); | 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(); | bind_events(); | ||||
setup_sortable(); | setup_sortable(); | ||||
} | } | ||||
@@ -312,7 +302,7 @@ frappe.provide("frappe.views"); | |||||
dataIdAttr: 'data-column-value', | dataIdAttr: 'data-column-value', | ||||
filter: '.add-new-column', | filter: '.add-new-column', | ||||
handle: '.kanban-column-title', | handle: '.kanban-column-title', | ||||
onEnd: function(evt) { | |||||
onEnd: function() { | |||||
var order = sortable.toArray(); | var order = sortable.toArray(); | ||||
order = order.slice(1); | order = order.slice(1); | ||||
fluxify.doAction('update_column_order', order); | fluxify.doAction('update_column_order', order); | ||||
@@ -322,7 +312,6 @@ frappe.provide("frappe.views"); | |||||
function bind_add_column() { | function bind_add_column() { | ||||
var wrapper = self.$kanban_board; | |||||
var $add_new_column = self.$kanban_board.find(".add-new-column"), | var $add_new_column = self.$kanban_board.find(".add-new-column"), | ||||
$compose_column = $add_new_column.find(".compose-column"), | $compose_column = $add_new_column.find(".compose-column"), | ||||
$compose_column_form = $add_new_column.find(".compose-column-form").hide(); | $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 title = $compose_column_form.serializeArray()[0].value; | ||||
var col = { | var col = { | ||||
title: title.trim() | title: title.trim() | ||||
} | |||||
}; | |||||
fluxify.doAction('add_column', col); | fluxify.doAction('add_column', col); | ||||
$compose_column_form.find('input').val(''); | $compose_column_form.find('input').val(''); | ||||
$compose_column.show(); | $compose_column.show(); | ||||
@@ -352,7 +341,7 @@ frappe.provide("frappe.views"); | |||||
}); | }); | ||||
// on form blur | // on form blur | ||||
$compose_column_form.find('input').on("blur", function (e) { | |||||
$compose_column_form.find('input').on("blur", function () { | |||||
$(this).val(''); | $(this).val(''); | ||||
$compose_column.show(); | $compose_column.show(); | ||||
$compose_column_form.hide(); | $compose_column_form.hide(); | ||||
@@ -362,7 +351,7 @@ frappe.provide("frappe.views"); | |||||
function bind_save_filter() { | function bind_save_filter() { | ||||
var set_filter_state = function () { | var set_filter_state = function () { | ||||
fluxify.doAction('set_filter_state'); | fluxify.doAction('set_filter_state'); | ||||
} | |||||
}; | |||||
if(isBound(self.$kanban_board, 'after-refresh', set_filter_state)) return; | if(isBound(self.$kanban_board, 'after-refresh', set_filter_state)) return; | ||||
@@ -375,8 +364,8 @@ frappe.provide("frappe.views"); | |||||
function setup_restore_columns() { | function setup_restore_columns() { | ||||
var cur_list = store.getState().cur_list; | var cur_list = store.getState().cur_list; | ||||
var columns = store.getState().columns; | 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'); | .css('margin-right', '15px'); | ||||
list_row_right.empty(); | list_row_right.empty(); | ||||
@@ -398,16 +387,16 @@ frappe.provide("frappe.views"); | |||||
"<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" + | "<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" + | ||||
"<span class='dropdown-text'>" + __('Archived Columns') + "</span><i class='caret'></i></a>" + | "<span class='dropdown-text'>" + __('Archived Columns') + "</span><i class='caret'></i></a>" + | ||||
"<ul class='dropdown-menu'>" + options + "</ul>" + | "<ul class='dropdown-menu'>" + options + "</ul>" + | ||||
"</div>") | |||||
"</div>"); | |||||
list_row_right.html($dropdown); | 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 column_title = $(this).data().column; | ||||
var col = { | var col = { | ||||
title: column_title, | title: column_title, | ||||
status: 'Archived' | status: 'Archived' | ||||
} | |||||
}; | |||||
fluxify.doAction('restore_column', col); | fluxify.doAction('restore_column', col); | ||||
}); | }); | ||||
} | } | ||||
@@ -427,7 +416,7 @@ frappe.provide("frappe.views"); | |||||
init(); | init(); | ||||
return self; | return self; | ||||
} | |||||
}; | |||||
frappe.views.KanbanBoardColumn = function (column, wrapper) { | frappe.views.KanbanBoardColumn = function (column, wrapper) { | ||||
var self = {}; | var self = {}; | ||||
@@ -455,7 +444,6 @@ frappe.provide("frappe.views"); | |||||
function make_cards() { | function make_cards() { | ||||
self.$kanban_cards.empty(); | self.$kanban_cards.empty(); | ||||
var cards = store.getState().cards; | var cards = store.getState().cards; | ||||
var board = store.getState().board; | |||||
filtered_cards = get_cards_for_column(cards, column); | filtered_cards = get_cards_for_column(cards, column); | ||||
var filtered_cards_names = filtered_cards.map(card => card.name); | var filtered_cards_names = filtered_cards.map(card => card.name); | ||||
@@ -480,20 +468,20 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
function setup_sortable() { | function setup_sortable() { | ||||
var sortable = Sortable.create(self.$kanban_cards.get(0), { | |||||
Sortable.create(self.$kanban_cards.get(0), { | |||||
group: "cards", | group: "cards", | ||||
animation: 150, | animation: 150, | ||||
dataIdAttr: 'data-name', | dataIdAttr: 'data-name', | ||||
onStart: function (evt) { | |||||
onStart: function () { | |||||
wrapper.find('.kanban-card.add-card').fadeOut(200, function () { | wrapper.find('.kanban-card.add-card').fadeOut(200, function () { | ||||
wrapper.find('.kanban-cards').height('100vh'); | wrapper.find('.kanban-cards').height('100vh'); | ||||
}); | }); | ||||
}, | }, | ||||
onEnd: function (evt) { | |||||
onEnd: function () { | |||||
wrapper.find('.kanban-card.add-card').fadeIn(100); | wrapper.find('.kanban-card.add-card').fadeIn(100); | ||||
wrapper.find('.kanban-cards').height('auto'); | wrapper.find('.kanban-cards').height('auto'); | ||||
// update order | // update order | ||||
var order = {} | |||||
var order = {}; | |||||
wrapper.find('.kanban-column[data-column-value]') | wrapper.find('.kanban-column[data-column-value]') | ||||
.each(function() { | .each(function() { | ||||
var col_name = $(this).data().columnValue; | var col_name = $(this).data().columnValue; | ||||
@@ -505,7 +493,7 @@ frappe.provide("frappe.views"); | |||||
}); | }); | ||||
fluxify.doAction('update_order', order); | fluxify.doAction('update_order', order); | ||||
}, | }, | ||||
onAdd: function (evt) { | |||||
onAdd: function () { | |||||
}, | }, | ||||
}); | }); | ||||
} | } | ||||
@@ -543,7 +531,7 @@ frappe.provide("frappe.views"); | |||||
}); | }); | ||||
// on textarea blur | // on textarea blur | ||||
$textarea.on("blur", function (e) { | |||||
$textarea.on("blur", function () { | |||||
$(this).val(''); | $(this).val(''); | ||||
$btn_add.show(); | $btn_add.show(); | ||||
$new_card_area.hide(); | $new_card_area.hide(); | ||||
@@ -552,7 +540,7 @@ frappe.provide("frappe.views"); | |||||
function bind_options() { | function bind_options() { | ||||
self.$kanban_column.find(".column-options .dropdown-menu") | self.$kanban_column.find(".column-options .dropdown-menu") | ||||
.on("click", "[data-action]", function (e) { | |||||
.on("click", "[data-action]", function () { | |||||
var $btn = $(this); | var $btn = $(this); | ||||
var action = $btn.data().action; | var action = $btn.data().action; | ||||
@@ -564,11 +552,11 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
}); | }); | ||||
get_column_indicators(function(indicators) { | get_column_indicators(function(indicators) { | ||||
var html = '<li class="button-group">' | |||||
var html = '<li class="button-group">'; | |||||
html += indicators.reduce(function(prev, curr) { | html += indicators.reduce(function(prev, curr) { | ||||
return prev + '<div \ | return prev + '<div \ | ||||
data-action="indicator" data-indicator="'+curr+'"\ | data-action="indicator" data-indicator="'+curr+'"\ | ||||
class="btn btn-default btn-xs indicator ' + curr + '"></div>' | |||||
class="btn btn-default btn-xs indicator ' + curr + '"></div>'; | |||||
}, ""); | }, ""); | ||||
html += '</li>'; | html += '</li>'; | ||||
self.$kanban_column.find(".column-options .dropdown-menu") | self.$kanban_column.find(".column-options .dropdown-menu") | ||||
@@ -577,7 +565,7 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
init(); | init(); | ||||
} | |||||
}; | |||||
frappe.views.KanbanBoardCard = function (card, wrapper) { | frappe.views.KanbanBoardCard = function (card, wrapper) { | ||||
var self = {}; | 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() { | function refresh_dialog() { | ||||
set_dialog_fields(); | set_dialog_fields(); | ||||
make_assignees(); | 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() { | function make_assignees() { | ||||
var d = self.edit_dialog; | var d = self.edit_dialog; | ||||
var html = get_assignees_html() + '<a class="add-assignment avatar avatar-small avatar-empty">\ | var html = get_assignees_html() + '<a class="add-assignment avatar avatar-small avatar-empty">\ | ||||
@@ -751,7 +659,7 @@ frappe.provide("frappe.views"); | |||||
method: 'frappe.desk.form.assign_to.add', | method: 'frappe.desk.form.assign_to.add', | ||||
doctype: card.doctype, | doctype: card.doctype, | ||||
docname: card.name, | docname: card.name, | ||||
callback: function(r) { | |||||
callback: function() { | |||||
var user = self.assign_to_dialog.get_values().assign_to; | var user = self.assign_to_dialog.get_values().assign_to; | ||||
card.assigned_list.push(user); | card.assigned_list.push(user); | ||||
fluxify.doAction('update_card', card); | fluxify.doAction('update_card', card); | ||||
@@ -762,100 +670,8 @@ frappe.provide("frappe.views"); | |||||
self.assign_to_dialog.show(); | self.assign_to_dialog.show(); | ||||
} | } | ||||
function make_timeline() { | |||||
var d = self.edit_dialog; | |||||
// timeline wrapper | |||||
d.$wrapper.find('.modal-body').append('<div class="form-comments" style="padding:7px">'); | |||||
// edit in full page button | |||||
$('<div class="text-muted small" style="padding-left: 10px; padding-top: 15px;">\ | |||||
<a class="edit-full">'+ __('Edit in full page') + '</a></div>') | |||||
.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('<div class="h4">\ | |||||
<span>%(card_title)s</span>\ | |||||
<input type="text">\ | |||||
</div>', { 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(); | init(); | ||||
} | |||||
}; | |||||
// Helpers | // Helpers | ||||
function get_board(board_name) { | function get_board(board_name) { | ||||
@@ -874,7 +690,7 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
return prepare_board(board); | return prepare_board(board); | ||||
}, function(e) { | }, function(e) { | ||||
console.log(e) | |||||
console.log(e); // eslint-disable-line | |||||
}); | }); | ||||
} | } | ||||
@@ -897,7 +713,11 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
meta.fields.forEach(function (df) { | 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 | // can be mapped to textarea | ||||
title_field = df; | title_field = df; | ||||
} | } | ||||
@@ -926,7 +746,7 @@ frappe.provide("frappe.views"); | |||||
title_field: title_field, | title_field: title_field, | ||||
description_field: description_field, | description_field: description_field, | ||||
due_date_field: due_date_field, | due_date_field: due_date_field, | ||||
} | |||||
}; | |||||
} | } | ||||
function get_date_field(fields) { | function get_date_field(fields) { | ||||
@@ -993,7 +813,7 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
function fetch_customization(doctype) { | function fetch_customization(doctype) { | ||||
return new Promise(function (resolve, reject) { | |||||
return new Promise(function (resolve) { | |||||
frappe.model.with_doc("Customize Form", "Customize Form", function () { | frappe.model.with_doc("Customize Form", "Customize Form", function () { | ||||
var doc = frappe.get_doc("Customize Form"); | var doc = frappe.get_doc("Customize Form"); | ||||
doc.doc_type = doctype; | doc.doc_type = doctype; | ||||
@@ -1023,7 +843,7 @@ frappe.provide("frappe.views"); | |||||
args: { | args: { | ||||
doc: doc | doc: doc | ||||
}, | }, | ||||
callback: function (r) { | |||||
callback: function () { | |||||
frappe.model.clear_doc(doc.doctype, doc.name); | frappe.model.clear_doc(doc.doctype, doc.name); | ||||
frappe.show_alert({ message: __("Saved"), indicator: 'green' }, 1); | frappe.show_alert({ message: __("Saved"), indicator: 'green' }, 1); | ||||
} | } | ||||
@@ -1049,24 +869,27 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
function is_filters_modified(board, cur_list) { | function is_filters_modified(board, cur_list) { | ||||
return new Promise(function(resolve, reject) { | |||||
return new Promise(function(resolve) { | |||||
setTimeout(function() { | 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); | }, 2000); | ||||
}) | |||||
}); | |||||
} | } | ||||
function is_active_column(col) { | function is_active_column(col) { | ||||
return col.status !== 'Archived' | |||||
return col.status !== 'Archived'; | |||||
} | } | ||||
function get_cards_for_column(cards, column) { | function get_cards_for_column(cards, column) { | ||||
return cards.filter(function (card) { | 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) { | if(!indicators) { | ||||
// | // | ||||
indicators = ['green', 'blue', 'orange', 'grey'] | |||||
indicators = ['green', 'blue', 'orange', 'grey']; | |||||
} | } | ||||
callback(indicators); | callback(indicators); | ||||
}); | }); | ||||
@@ -1118,7 +941,7 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
function remove_img_tags(html) { | function remove_img_tags(html) { | ||||
const $temp = $(`<div>${html}</div>`) | |||||
const $temp = $(`<div>${html}</div>`); | |||||
$temp.find('img').remove(); | $temp.find('img').remove(); | ||||
return $temp.html(); | return $temp.html(); | ||||
} | } | ||||
@@ -1,77 +1,63 @@ | |||||
frappe.provide('frappe.views'); | 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) { | 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; | return; | ||||
} | } | ||||
this.kanban = new frappe.views.KanbanBoard({ | this.kanban = new frappe.views.KanbanBoard({ | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
board_name: board_name, | 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' | |||||
] | |||||
}); | |||||
get required_libs() { | |||||
return [ | |||||
'assets/frappe/js/lib/fluxify.min.js', | |||||
'assets/frappe/js/frappe/views/kanban/kanban_board.js' | |||||
]; | |||||
} | |||||
}; |
@@ -10,7 +10,7 @@ | |||||
{% for col in columns %} | {% for col in columns %} | ||||
{% if col.name && col._id !== "_check" %} | {% if col.name && col._id !== "_check" %} | ||||
<th style="min-width: {{ col.minWidth }}px" | <th style="min-width: {{ col.minWidth }}px" | ||||
{% if col.docfield && in_list(["Float", "Currency", "Int"], col.docfield.fieldtype) %} | |||||
{% if col.docfield && frappe.model.is_numeric_field(col.docfield) %} | |||||
class="text-right" | class="text-right" | ||||
{% endif %}>{{ __(col.name) }}</th> | {% endif %}>{{ __(col.name) }}</th> | ||||
{% endif %} | {% endif %} | ||||
@@ -24,11 +24,13 @@ | |||||
{% for col in columns %} | {% for col in columns %} | ||||
{% if col.name && col._id !== "_check" %} | {% 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]; %} | |||||
<td>{{ col.formatter | |||||
<td> | |||||
{{ col.formatter | |||||
? col.formatter(row._index, col._index, value, col, row, true) | ? col.formatter(row._index, col._index, value, col, row, true) | ||||
: value }}</td> | |||||
: (col.docfield ? frappe.format(value, col.docfield) : value) }} | |||||
</td> | |||||
{% endif %} | {% endif %} | ||||
{% endfor %} | {% endfor %} | ||||
</tr> | </tr> | ||||
@@ -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 })); | |||||
} | |||||
}; |
@@ -54,889 +54,10 @@ frappe.views.ReportViewPage = Class.extend({ | |||||
var module = locals.DocType[this.doctype].module; | var module = locals.DocType[this.doctype].module; | ||||
frappe.breadcrumbs.add(module, this.doctype); | frappe.breadcrumbs.add(module, this.doctype); | ||||
this.parent.reportview = new frappe.views.ReportView({ | |||||
this.parent.reportview = new frappe.views.ReportView2({ | |||||
doctype: this.doctype, | doctype: this.doctype, | ||||
docname: this.docname, | docname: this.docname, | ||||
parent: this.parent | 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 = $('<div>').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("<input type='checkbox' \ | |||||
data-row='%(row)s' %(checked)s>", { | |||||
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('<p class="help">'+__('Sort By')+'</p>\ | |||||
<div class="sort-column"></div>\ | |||||
<div><select class="sort-order form-control" style="margin-top: 10px; width: 60%;">\ | |||||
<option value="asc">'+__('Ascending')+'</option>\ | |||||
<option value="desc">'+__('Descending')+'</option>\ | |||||
</select></div>\ | |||||
<hr><p class="help">'+__('Then By (optional)')+'</p>\ | |||||
<div class="sort-column-1"></div>\ | |||||
<div><select class="sort-order-1 form-control" style="margin-top: 10px; width: 60%;">\ | |||||
<option value="asc">'+__('Ascending')+'</option>\ | |||||
<option value="desc">'+__('Descending')+'</option>\ | |||||
</select></div><hr>\ | |||||
<div><button class="btn btn-primary">'+__('Update')+'</div>'); | |||||
// 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('<div class="text-muted">'+__("Drag to sort columns")+'</div>\ | |||||
<div class="column-list"></div>\ | |||||
<div><button class="btn btn-default btn-sm btn-add">'+__("Add Column")+'</button></div>'); | |||||
}, | |||||
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 = $('<div class="column-list-item"><div class="row">\ | |||||
<div class="col-xs-1">\ | |||||
<i class="fa fa-sort text-muted"></i></div>\ | |||||
<div class="col-xs-10"></div>\ | |||||
<div class="col-xs-1"><a class="close">×</a></div>\ | |||||
</div></div>') | |||||
.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(); | |||||
} | |||||
}); |
@@ -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+"]><i><![endif]--\x3e",c[0];);return 4<b?b:document.documentMode}(),x=navigator.platform.toLowerCase().indexOf("mac")+1,p=function(b){if(!(this instanceof p))return new p(b);var a=this,c={rows_in_block:50,blocks_in_cluster:4,tag:null,show_no_data_row:!0,no_data_class:"clusterize-no-data",no_data_text:"No data",keep_parity:!0,callbacks:{}};a.options={};for(var d="rows_in_block blocks_in_cluster show_no_data_row no_data_class no_data_text keep_parity tag callbacks".split(" "), | |||||
f=0,h;h=d[f];f++)a.options[h]="undefined"!=typeof b[h]&&null!=b[h]?b[h]:c[h];c=["scroll","content"];for(f=0;d=c[f];f++)if(a[d+"_elem"]=b[d+"Id"]?document.getElementById(b[d+"Id"]):b[d+"Elem"],!a[d+"_elem"])throw Error("Error! Could not find "+d+" element");a.content_elem.hasAttribute("tabindex")||a.content_elem.setAttribute("tabindex",0);var e=r(b.rows)?b.rows:a.fetchMarkup(),g={};b=a.scroll_elem.scrollTop;a.insertToDOM(e,g);a.scroll_elem.scrollTop=b;var k=!1,m=0,l=!1,t=function(){x&&(l||(a.content_elem.style.pointerEvents= | |||||
"none"),l=!0,clearTimeout(m),m=setTimeout(function(){a.content_elem.style.pointerEvents="auto";l=!1},50));k!=(k=a.getClusterNum())&&a.insertToDOM(e,g);a.options.callbacks.scrollingProgress&&a.options.callbacks.scrollingProgress(a.getScrollProgress())},u=0,v=function(){clearTimeout(u);u=setTimeout(a.refresh,100)};q("scroll",a.scroll_elem,t);q("resize",window,v);a.destroy=function(b){n("scroll",a.scroll_elem,t);n("resize",window,v);a.html((b?a.generateEmptyRow():e).join(""))};a.refresh=function(b){(a.getRowsHeight(e)|| | |||||
b)&&a.update(e)};a.update=function(b){e=r(b)?b:[];b=a.scroll_elem.scrollTop;e.length*a.options.item_height<b&&(k=a.scroll_elem.scrollTop=0);a.insertToDOM(e,g);a.scroll_elem.scrollTop=b};a.clear=function(){a.update([])};a.getRowsAmount=function(){return e.length};a.getScrollProgress=function(){return this.options.scroll_top/(e.length*this.options.item_height)*100||0};var w=function(b,c){var d=r(c)?c:[];d.length&&(e="append"==b?e.concat(d):d.concat(e),a.insertToDOM(e,g))};a.append=function(a){w("append", | |||||
a)};a.prepend=function(a){w("prepend",a)}};p.prototype={constructor:p,fetchMarkup:function(){for(var b=[],a=this.getChildNodes(this.content_elem);a.length;)b.push(a.shift().outerHTML);return b},exploreEnvironment:function(b,a){var c=this.options;c.content_tag=this.content_elem.tagName.toLowerCase();b.length&&(l&&9>=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(d<c.rows_in_block)return{top_offset:0,bottom_offset:0,rows_above:0,rows:d?b:this.generateEmptyRow()};var f=Math.max((c.rows_in_cluster-c.rows_in_block)*a,0),h=f+c.rows_in_cluster,e=Math.max(f*c.item_height,0),c=Math.max((d-h)*c.item_height,0),d=[],g=f;for(1>e&&g++;f<h;f++)b[f]&&d.push(b[f]);return{top_offset:e,bottom_offset:c, | |||||
rows_above:g,rows:d}},renderExtraTag:function(b,a){var c=document.createElement(this.options.tag);c.className=["clusterize-extra-row","clusterize-"+b].join(" ");a&&(c.style.height=a+"px");return c.outerHTML},insertToDOM:function(b,a){this.options.cluster_height||this.exploreEnvironment(b,a);var c=this.generate(b,this.getClusterNum()),d=c.rows.join(""),f=this.checkChanges("data",d,a),h=this.checkChanges("top",c.top_offset,a),e=this.checkChanges("bottom",c.bottom_offset,a),g=this.options.callbacks, | |||||
k=[];f||h?(c.top_offset&&(this.options.keep_parity&&k.push(this.renderExtraTag("keep-parity")),k.push(this.renderExtraTag("top-space",c.top_offset))),k.push(d),c.bottom_offset&&k.push(this.renderExtraTag("bottom-space",c.bottom_offset)),g.clusterWillChange&&g.clusterWillChange(),this.html(k.join("")),"ol"==this.options.content_tag&&this.content_elem.setAttribute("start",c.rows_above),g.clusterChanged&&g.clusterChanged()):e&&(this.content_elem.lastChild.style.height=c.bottom_offset+"px")},html:function(b){var a= | |||||
this.content_elem;if(l&&9>=l&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML="<table><tbody>"+b+"</tbody></table>";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<d;c++)a.push(b[c]);return a},checkChanges:function(b,a,c){var d=a!=c[b];c[b]=a;return d}};return p}); |
@@ -93,8 +93,6 @@ frappe.render_grid = function(opts) { | |||||
} else if(opts.grid) { | } else if(opts.grid) { | ||||
opts.data = opts.grid.getData().getItems(); | opts.data = opts.grid.getData().getItems(); | ||||
} | } | ||||
} else { | |||||
opts.columns = []; | |||||
} | } | ||||
// show landscape view if columns more than 10 | // show landscape view if columns more than 10 | ||||
@@ -237,6 +237,17 @@ a.no-decoration& { | |||||
margin: 15px; | margin: 15px; | ||||
} | } | ||||
.margin-(@position) { | |||||
.margin-@{position} { | |||||
margin-@{position}: 15px; | |||||
} | |||||
} | |||||
.margin-(top); | |||||
.margin-(bottom); | |||||
.margin-(left); | |||||
.margin-(right); | |||||
@media (max-width: 767px) { | @media (max-width: 767px) { | ||||
.text-center-xs { | .text-center-xs { | ||||
text-align: center; | text-align: center; | ||||
@@ -3,6 +3,14 @@ | |||||
margin-top: 5px; | margin-top: 5px; | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
label { | |||||
position: relative; | |||||
} | |||||
input[type=checkbox] { | |||||
margin-left: 0; | |||||
} | |||||
& + .checkbox { | & + .checkbox { | ||||
margin-top: 5px; | margin-top: 5px; | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
@@ -304,6 +304,10 @@ textarea.form-control { | |||||
} | } | ||||
} | } | ||||
.input-area { | |||||
position: relative; | |||||
} | |||||
.link-field.ui-front { | .link-field.ui-front { | ||||
z-index: inherit; | z-index: inherit; | ||||
} | } | ||||
@@ -434,11 +438,6 @@ li.user-progress { | |||||
padding: 15px 30px; | padding: 15px 30px; | ||||
} | } | ||||
.footnote-area { | |||||
padding: 0px 15px; | |||||
border-top: 1px solid @border-color; | |||||
} | |||||
.file-upload { | .file-upload { | ||||
.input-group-addon { | .input-group-addon { | ||||
color: @text-muted; | color: @text-muted; | ||||
@@ -897,7 +896,7 @@ li.user-progress { | |||||
// custom font awesome checkbox | // custom font awesome checkbox | ||||
input[type="checkbox"] { | input[type="checkbox"] { | ||||
position: relative; | position: relative; | ||||
height: 16px; | |||||
left: -999999px; | |||||
&:before { | &:before { | ||||
position: absolute; | position: absolute; | ||||
@@ -913,9 +912,7 @@ input[type="checkbox"] { | |||||
font-size: 14px; | font-size: 14px; | ||||
color: @text-extra-muted; | color: @text-extra-muted; | ||||
.transition(150ms color); | .transition(150ms color); | ||||
background-color: white; | |||||
padding: 1px; | |||||
margin: -1px; | |||||
left: 999999px; | |||||
} | } | ||||
&:focus:before { | &:focus:before { | ||||
@@ -0,0 +1,47 @@ | |||||
.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; | |||||
&.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; | |||||
} |
@@ -0,0 +1,71 @@ | |||||
@import "variables.less"; | |||||
.data-table { | |||||
margin-left: -1px; | |||||
margin-top: -1px; | |||||
font-size: @text-medium; | |||||
.data-table-col .edit-cell { | |||||
padding: 0; | |||||
input { | |||||
font-size: inherit; | |||||
height: 34px; | |||||
} | |||||
} | |||||
.frappe-control { | |||||
margin: 0; | |||||
} | |||||
.form-group { | |||||
margin: 0; | |||||
} | |||||
.form-control { | |||||
border-radius: 0px; | |||||
border: none; | |||||
} | |||||
.link-btn { | |||||
top: 6px; | |||||
} | |||||
select { | |||||
height: 34px; | |||||
} | |||||
.checkbox { | |||||
margin: 7px 0 7px 8px; | |||||
} | |||||
[data-fieldtype="Color"] .control-input { | |||||
overflow: hidden; | |||||
} | |||||
.body-scrollable { | |||||
&::-webkit-scrollbar { | |||||
display: none; | |||||
} | |||||
} | |||||
.data-table-header { | |||||
background-color: @panel-bg; | |||||
color: @text-muted; | |||||
} | |||||
.data-table-row.row-update { | |||||
animation: 500ms breathe forwards; | |||||
} | |||||
.data-table-row.row-highlight { | |||||
background-color: @extra-light-yellow; | |||||
} | |||||
} | |||||
@keyframes breathe { | |||||
0% { | |||||
background-color: transparent; | |||||
} | |||||
50% { | |||||
background-color: @extra-light-yellow; | |||||
} | |||||
100% { | |||||
background-color: transparent; | |||||
} | |||||
} |
@@ -1,12 +1,34 @@ | |||||
@import "variables.less"; | @import "variables.less"; | ||||
.no-result { | |||||
padding: 150px 15px; | |||||
color: @text-muted; | |||||
.result, .no-result, .freeze { | |||||
min-height: ~"calc(100vh - 284px)"; | |||||
} | |||||
.freeze-row { | |||||
.level-left, .level-right, .list-row-col { | |||||
height: 100%; | |||||
width: 100%; | |||||
} | |||||
.list-row-col { | |||||
background-color: @border-color; | |||||
border-radius: 2px; | |||||
animation: 2s breathe infinite; | |||||
} | |||||
} | } | ||||
.result-list { | |||||
min-height: 400px; | |||||
@keyframes breathe { | |||||
0% { | |||||
opacity: 0.2; | |||||
} | |||||
50% { | |||||
opacity: 0.5; | |||||
} | |||||
100% { | |||||
opacity: 0.2; | |||||
} | |||||
} | } | ||||
.sort-selector { | .sort-selector { | ||||
@@ -15,7 +37,7 @@ | |||||
} | } | ||||
} | } | ||||
.list-filters { | |||||
.filter-list{ | |||||
position: relative; | position: relative; | ||||
.sort-selector { | .sort-selector { | ||||
@@ -25,12 +47,12 @@ | |||||
} | } | ||||
} | } | ||||
.show_filters { | |||||
.tag-filters-area { | |||||
padding: 15px 15px 0px; | padding: 15px 15px 0px; | ||||
border-bottom: 1px solid @border-color; | border-bottom: 1px solid @border-color; | ||||
} | } | ||||
.set-filters { | |||||
.active-tag-filters { | |||||
padding-bottom: 4px; | padding-bottom: 4px; | ||||
padding-right: 120px; | padding-right: 120px; | ||||
@@ -39,17 +61,17 @@ | |||||
} | } | ||||
} | } | ||||
.set-filters .btn { | |||||
.active-tag-filters .btn { | |||||
margin-bottom: 10px; | margin-bottom: 10px; | ||||
} | } | ||||
.set-filters .btn-group { | |||||
margin-right: 10px; | |||||
.active-tag-filters .btn-group { | |||||
margin-left: 10px; | |||||
white-space: nowrap; | white-space: nowrap; | ||||
font-size: 0; | font-size: 0; | ||||
} | } | ||||
.set-filters .btn-group .btn-default { | |||||
.active-tag-filters .btn-group .btn-default { | |||||
background-color: transparent; | background-color: transparent; | ||||
border: 1px solid @border-color; | border: 1px solid @border-color; | ||||
color: @text-muted; | color: @text-muted; | ||||
@@ -58,7 +80,6 @@ | |||||
.filter-box { | .filter-box { | ||||
border-bottom: 1px solid @border-color; | border-bottom: 1px solid @border-color; | ||||
// margin: 0px -15px; | |||||
padding: 10px 15px 3px; | padding: 10px 15px 3px; | ||||
.remove-filter { | .remove-filter { | ||||
@@ -66,7 +87,7 @@ | |||||
margin-left: 15px; | margin-left: 15px; | ||||
} | } | ||||
.filter_field { | |||||
.filter-field { | |||||
padding-right: 15px; | padding-right: 15px; | ||||
width: calc(100% - 36px); | width: calc(100% - 36px); | ||||
@@ -77,12 +98,12 @@ | |||||
} | } | ||||
// for sm and above | // for sm and above | ||||
@media (min-width: 768px) { | |||||
@media (min-width: @screen-xs) { | |||||
.filter-box .row > div[class*="col-sm-"] { | .filter-box .row > div[class*="col-sm-"] { | ||||
padding-right: 0px; | padding-right: 0px; | ||||
} | } | ||||
.filter_field { | |||||
.filter-field { | |||||
width: 65% !important; | width: 65% !important; | ||||
.frappe-control { | .frappe-control { | ||||
@@ -91,144 +112,108 @@ | |||||
} | } | ||||
} | } | ||||
.list-row { | |||||
padding: 9px 15px; | |||||
.list-row-container { | |||||
border-bottom: 1px solid @border-color; | border-bottom: 1px solid @border-color; | ||||
display: flex; | |||||
flex-direction: column; | |||||
} | |||||
.list-row { | |||||
padding: 12px 15px; | |||||
height: 40px; | |||||
cursor: pointer; | cursor: pointer; | ||||
transition: color 0.2s; | transition: color 0.2s; | ||||
-webkit-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 { | .list-row-head { | ||||
background-color: @panel-bg; | background-color: @panel-bg; | ||||
border-bottom: 1px solid @border-color !important; | 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: 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 { | .progress { | ||||
height: 10px; | height: 10px; | ||||
} | } | ||||
.doclist-row { | |||||
font-size: 12px; | |||||
} | |||||
.likes-count { | .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 { | .filterable { | ||||
cursor: pointer; | 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 { | .listview-main-section { | ||||
.octicon-heart { | .octicon-heart { | ||||
cursor: pointer; | cursor: pointer; | ||||
@@ -256,36 +241,6 @@ | |||||
color: @heart-color; | 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 { | .list-comment-count { | ||||
display: inline-block; | display: inline-block; | ||||
width: 37px; | width: 37px; | ||||
@@ -294,10 +249,15 @@ | |||||
// tags | // tags | ||||
.result.tags-shown { | |||||
.tag-row { | |||||
display: block; | |||||
} | |||||
} | |||||
.tag-row { | .tag-row { | ||||
padding-left: 55px; | |||||
margin-bottom: 0px; | |||||
margin-top: -5px; | |||||
display: none; | |||||
margin-left: 50px; | |||||
} | } | ||||
.taggle_placeholder { | .taggle_placeholder { | ||||
@@ -374,6 +334,7 @@ | |||||
padding: 15px; | padding: 15px; | ||||
border-bottom: 1px solid @light-border-color; | border-bottom: 1px solid @light-border-color; | ||||
border-right: 1px solid @light-border-color; | border-right: 1px solid @light-border-color; | ||||
max-width: 100%/4; | |||||
} | } | ||||
.image-view-item:nth-child(4n) { | .image-view-item:nth-child(4n) { | ||||
@@ -410,6 +371,10 @@ | |||||
img { | img { | ||||
max-height: 100%; | max-height: 100%; | ||||
} | } | ||||
&.no-image { | |||||
background-color: @light-bg; | |||||
} | |||||
} | } | ||||
.placeholder-text { | .placeholder-text { | ||||
@@ -457,6 +422,7 @@ | |||||
.image-view-container.three-column { | .image-view-container.three-column { | ||||
.image-view-item { | .image-view-item { | ||||
flex: 0 0 100%/3; | flex: 0 0 100%/3; | ||||
max-width: 100%/3; | |||||
} | } | ||||
.image-view-item:nth-child(3n) { | .image-view-item:nth-child(3n) { | ||||
@@ -510,6 +476,11 @@ | |||||
} | } | ||||
// gantt | // gantt | ||||
.list-paging-area .gantt-view-mode { | |||||
margin-left: 15px; | |||||
margin-right: 15px; | |||||
} | |||||
.gantt { | .gantt { | ||||
.details-container { | .details-container { | ||||
.heading { | .heading { | ||||
@@ -67,3 +67,16 @@ | |||||
margin-bottom: 2px; | margin-bottom: 2px; | ||||
} | } | ||||
} | } | ||||
// datatable | |||||
.data-table { | |||||
.edit-popup { | |||||
.frappe-control { | |||||
padding: 0; | |||||
.form-group { | |||||
margin: 0; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -22,34 +22,31 @@ QUnit.test("Calendar View Tests", function(assert) { | |||||
{event_type: 'Private'} | {event_type: 'Private'} | ||||
]), | ]), | ||||
() => frappe.timeout(1), | |||||
() => frappe.tests.make("Event", [ | () => frappe.tests.make("Event", [ | ||||
{subject: random_text + ':Pub'}, | {subject: random_text + ':Pub'}, | ||||
{starts_on: today}, | {starts_on: today}, | ||||
{event_type: 'Public'} | {event_type: 'Public'} | ||||
]), | ]), | ||||
() => frappe.timeout(1), | |||||
// Goto Calendar view | // Goto Calendar view | ||||
() => frappe.set_route(["List", "Event", "Calendar"]), | () => 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), | () => frappe.timeout(2), | ||||
// Check if event is created | // Check if event is created | ||||
() => { | () => { | ||||
// Check if the event exists and if its title matches with the one created | // Check if the event exists and if its title matches with the one created | ||||
assert.ok(event_title_text().includes(random_text + ':Pri'), | assert.ok(event_title_text().includes(random_text + ':Pri'), | ||||
"Event title verified"); | "Event title verified"); | ||||
// Check if time of event created is correct | |||||
// assert.ok(visible_time().includes("4:20"), | |||||
// "Event start time verified"); | |||||
}, | }, | ||||
// check filter | // check filter | ||||
() => { | |||||
$('[data-fieldname="event_type"]').val('Public').trigger('change'); | |||||
}, | |||||
() => cur_list.filter_area.add('Event', 'event_type', '=', 'Public'), | |||||
() => frappe.timeout(1), | () => frappe.timeout(1), | ||||
() => { | () => { | ||||
// private event should be hidden | // private event should be hidden | ||||
@@ -4,17 +4,21 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { | |||||
assert.expect(2); | assert.expect(2); | ||||
let done = assert.async(); | let done = assert.async(); | ||||
const board_name = 'Kanban test'; | |||||
frappe.run_serially([ | frappe.run_serially([ | ||||
() => frappe.set_route("List", "ToDo", "List"), | () => frappe.set_route("List", "ToDo", "List"), | ||||
// wait for cur_list to initialize | |||||
() => cur_list.init(), | |||||
// click kanban in side bar | // 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), | () => frappe.timeout(0.5), | ||||
// create new kanban | // create new kanban | ||||
() => { | () => { | ||||
assert.equal(cur_dialog.title, 'New Kanban Board', | assert.equal(cur_dialog.title, 'New Kanban Board', | ||||
"Dialog for new kanban opened."); | "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'); | cur_dialog.set_value('field_name', 'Priority'); | ||||
}, | }, | ||||
() => frappe.timeout(0.5), | () => frappe.timeout(0.5), | ||||
@@ -23,7 +27,7 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { | |||||
() => frappe.set_route("List", "Kanban Board", "List"), | () => frappe.set_route("List", "Kanban Board", "List"), | ||||
() => frappe.timeout(0.5), | () => frappe.timeout(0.5), | ||||
// check in kanban list if new kanban is created | // 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."), | "Added kanban is visible in kanban list."), | ||||
() => done() | () => done() | ||||
]); | ]); |
@@ -10,9 +10,9 @@ QUnit.test("Test: Filters [Kanban view]", function(assert) { | |||||
() => { | () => { | ||||
assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), | assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), | ||||
"Kanban view opened successfully."); | "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), | () => frappe.timeout(1), | ||||
() => cur_list.page.btn_secondary.click(), | () => cur_list.page.btn_secondary.click(), | ||||
() => frappe.timeout(1), | () => frappe.timeout(1), | ||||
@@ -1,25 +1,28 @@ | |||||
QUnit.module('views'); | QUnit.module('views'); | ||||
QUnit.test("Test: Kanban view", function(assert) { | QUnit.test("Test: Kanban view", function(assert) { | ||||
assert.expect(3); | |||||
assert.expect(4); | |||||
let done = assert.async(); | let done = assert.async(); | ||||
let total_elements; | |||||
frappe.run_serially([ | frappe.run_serially([ | ||||
() => frappe.set_route("List", "ToDo", "List"), | () => frappe.set_route("List", "ToDo", "List"), | ||||
// calculate number of element in list | // calculate number of element in list | ||||
() => frappe.timeout(1), | () => frappe.timeout(1), | ||||
() => total_elements = cur_list.data.length, | |||||
() => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), | () => 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."); | "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."); | "Kanban view opened successfully."); | ||||
// check if all elements are visible in kanban view | // 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() | () => done() | ||||
]); | ]); |
@@ -14,10 +14,10 @@ QUnit.test("Test paging in list view", function(assert) { | |||||
() => frappe.click_button('More'), | () => frappe.click_button('More'), | ||||
() => frappe.timeout(2), | () => frappe.timeout(2), | ||||
() => assert.equal(cur_list.data.length, 40, 'show more items'), | () => 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), | () => frappe.timeout(2), | ||||
() => assert.ok(cur_list.data.length > 40, 'show 100 items'), | () => 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), | () => frappe.timeout(2), | ||||
() => assert.equal(cur_list.data.length, 20, 'show 20 items again'), | () => assert.equal(cur_list.data.length, 20, 'show 20 items again'), | ||||
() => done() | () => done() | ||||
@@ -8,24 +8,23 @@ QUnit.test("Test List Count", function(assert) { | |||||
() => frappe.set_route('List', 'DocType'), | () => frappe.set_route('List', 'DocType'), | ||||
() => frappe.timeout(0.5), | () => 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"); | assert.equal(cur_list.data.length, count, "Correct Count"); | ||||
}, | }, | ||||
() => frappe.timeout(1), | () => frappe.timeout(1), | ||||
() => cur_list.filter_list.add_filter('Doctype', 'module', '=', 'Desk'), | |||||
() => cur_list.filter_area.add('Doctype', 'module', '=', 'Desk'), | |||||
() => frappe.click_button('Refresh'), | () => 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"); | assert.equal(cur_list.data.length, count, "Correct Count"); | ||||
}, | }, | ||||
() => cur_list.filter_list.clear_filters(), | |||||
() => cur_list.filter_area.clear(), | |||||
() => frappe.timeout(1), | () => 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"); | assert.equal(cur_list.data.length, count, "Correct Count"); | ||||
}, | }, | ||||