* 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, | |||
"JsBarcode": true, | |||
"L": true, | |||
"Chart": true | |||
"Chart": true, | |||
"DataTable": true | |||
} | |||
} |
@@ -355,7 +355,7 @@ | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 1, | |||
"in_standard_filter": 0, | |||
"label": "Folder", | |||
"length": 0, | |||
"no_copy": 0, | |||
@@ -653,7 +653,7 @@ | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"menu_index": 0, | |||
"modified": "2017-10-27 13:27:43.882914", | |||
"modified": "2017-12-07 17:01:54.860204", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "File", | |||
@@ -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() { | |||
switch(doc.report_type) { | |||
case "Report Builder": | |||
frappe.set_route("Report", doc.ref_doctype, doc.name); | |||
frappe.set_route('List', doc.ref_doctype, 'Report', doc.name); | |||
break; | |||
case "Query Report": | |||
frappe.set_route("query-report", doc.name); | |||
@@ -262,6 +262,7 @@ $.extend(frappe.desktop, { | |||
} | |||
new Sortable($("#icon-grid").get(0), { | |||
animation: 150, | |||
onUpdate: function(event) { | |||
var new_order = []; | |||
$("#icon-grid .case-wrapper").each(function(i, e) { | |||
@@ -32,7 +32,7 @@ QUnit.test("test: Event", function (assert) { | |||
() => frappe.set_route('List', 'Event', 'Calendar'), | |||
() => frappe.timeout(2), | |||
() => { | |||
const bg_color = $(`.result-list:visible .fc-day-grid-event:contains("${subject}")`) | |||
const bg_color = $(`.result:visible .fc-day-grid-event:contains("${subject}")`) | |||
.css('background-color'); | |||
assert.equal(bg_color, rgb, 'Event background color is set correctly'); | |||
}, | |||
@@ -32,7 +32,7 @@ frappe.ui.form.on('Kanban Board', { | |||
field_name: function(frm) { | |||
var field = frappe.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name); | |||
frm.doc.columns = []; | |||
field.options && field.options.split('\n').forEach(function(o, i) { | |||
field.options && field.options.split('\n').forEach(function(o) { | |||
o = o.trim(); | |||
if(!o) return; | |||
var d = frm.add_child('columns'); | |||
@@ -6,6 +6,7 @@ | |||
#page-activity .list-row { | |||
border: none; | |||
padding: 0px; | |||
height: auto; | |||
cursor: pointer; | |||
} | |||
@@ -15,41 +15,10 @@ frappe.pages['activity'].on_page_load = function(wrapper) { | |||
me.page.set_title(__("Activity")); | |||
frappe.model.with_doctype("Communication", function() { | |||
me.page.list = new frappe.ui.BaseList({ | |||
hide_refresh: true, | |||
page: me.page, | |||
method: 'frappe.desk.page.activity.activity.get_feed', | |||
parent: $("<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); | |||
@@ -90,7 +59,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { | |||
frappe.route_options = { | |||
show_likes: true | |||
}; | |||
me.page.list.run(); | |||
me.page.list.refresh(); | |||
}, 'octicon octicon-heart'); | |||
}; | |||
@@ -194,4 +163,48 @@ frappe.activity.render_heatmap = function(page) { | |||
} | |||
} | |||
}) | |||
} | |||
} | |||
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')) | |||
doctype = frappe.form_dict.get('doctype') | |||
failed = [] | |||
for i, d in enumerate(il): | |||
try: | |||
frappe.delete_doc(doctype, d) | |||
@@ -223,7 +225,9 @@ def delete_items(): | |||
dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype)), | |||
user=frappe.session.user) | |||
except Exception: | |||
pass | |||
failed.append(d) | |||
return failed | |||
@frappe.whitelist() | |||
def get_sidebar_stats(stats, doctype, filters=[]): | |||
@@ -925,7 +925,12 @@ class Document(BaseDocument): | |||
if not self.meta.get("read_only") and not self.meta.get("issingle") and \ | |||
not self.meta.get("istable"): | |||
frappe.publish_realtime("list_update", {"doctype": self.doctype}, after_commit=True) | |||
data = { | |||
"doctype": self.doctype, | |||
"name": self.name, | |||
"user": frappe.session.user | |||
} | |||
frappe.publish_realtime("list_update", data, after_commit=True) | |||
def db_set(self, fieldname, value=None, update_modified=True, notify=False, commit=False): | |||
'''Set a value in the document object, update the timestamp and update the database. | |||
@@ -112,6 +112,7 @@ | |||
"public/css/font-awesome.css", | |||
"public/css/octicons/octicons.css", | |||
"public/css/desk.css", | |||
"public/css/flex.css", | |||
"public/css/indicator.css", | |||
"public/css/avatar.css", | |||
"public/css/navbar.css", | |||
@@ -212,6 +213,7 @@ | |||
"public/js/frappe/misc/help_links.js", | |||
"public/js/frappe/misc/address_and_contact.js", | |||
"public/js/frappe/misc/preview_email.js", | |||
"public/js/frappe/misc/file_manager.js", | |||
"public/js/frappe/ui/upload.html", | |||
"public/js/frappe/upload.js", | |||
@@ -299,10 +301,10 @@ | |||
"js/list.min.js": [ | |||
"public/js/frappe/ui/listing.html", | |||
"public/js/frappe/ui/base_list.js", | |||
"public/js/frappe/model/indicator.js", | |||
"public/js/frappe/ui/filters/filters.js", | |||
"public/js/frappe/ui/filters/filter.js", | |||
"public/js/frappe/ui/filters/filter_list.js", | |||
"public/js/frappe/ui/filters/field_select.js", | |||
"public/js/frappe/ui/filters/edit_filter.html", | |||
"public/js/frappe/ui/tags.js", | |||
"public/js/frappe/ui/tag_editor.js", | |||
@@ -310,7 +312,9 @@ | |||
"public/js/frappe/ui/liked_by.html", | |||
"public/html/print_template.html", | |||
"public/js/frappe/list/base_list.js", | |||
"public/js/frappe/list/list_view.js", | |||
"public/js/frappe/list/list_factory.js", | |||
"public/js/frappe/list/list_sidebar.js", | |||
"public/js/frappe/list/list_sidebar.html", | |||
@@ -322,12 +326,12 @@ | |||
"public/js/frappe/list/list_item_subject.html", | |||
"public/js/frappe/list/list_permission_footer.html", | |||
"public/js/frappe/list/list_renderer.js", | |||
"public/js/frappe/views/gantt/gantt_view.js", | |||
"public/js/frappe/views/calendar/calendar.js", | |||
"public/js/frappe/views/image/image_view.js", | |||
"public/js/frappe/views/kanban/kanban_view.js", | |||
"public/js/frappe/views/inbox/inbox_view.js", | |||
"public/js/frappe/views/file/file_view.js", | |||
"public/js/frappe/list/header_select_all_like_filter.html", | |||
"public/js/frappe/list/item_assigned_to_comment_count.html", | |||
@@ -336,10 +340,6 @@ | |||
"public/js/frappe/views/image/image_view_item_row.html", | |||
"public/js/frappe/views/image/photoswipe_dom.html", | |||
"public/js/frappe/views/inbox/inbox_no_result.html", | |||
"public/js/frappe/views/inbox/inbox_view_item_row.html", | |||
"public/js/frappe/views/inbox/inbox_view_item_main_head.html", | |||
"public/js/frappe/views/kanban/kanban_board.html", | |||
"public/js/frappe/views/kanban/kanban_column.html", | |||
"public/js/frappe/views/kanban/kanban_card.html" | |||
@@ -347,13 +347,17 @@ | |||
"css/report.min.css": [ | |||
"public/css/report.css", | |||
"public/css/tree_grid.css", | |||
"public/css/frappe-datatable.css", | |||
"public/js/lib/slickgrid/slick.grid.css", | |||
"public/js/lib/slickgrid/slick-default-theme.css", | |||
"public/css/slickgrid.css" | |||
], | |||
"js/report.min.js": [ | |||
"public/js/lib/clusterize.min.js", | |||
"public/js/lib/frappe-datatable.js", | |||
"public/js/frappe/views/reports/reportview.js", | |||
"public/js/frappe/views/reports/report_view.js", | |||
"public/js/frappe/views/reports/reportview_footer.html", | |||
"public/js/frappe/views/reports/query_report.js", | |||
"public/js/frappe/views/reports/grid_report.js", | |||
@@ -216,6 +216,18 @@ a.no-decoration:active { | |||
.margin { | |||
margin: 15px; | |||
} | |||
.margin-top { | |||
margin-top: 15px; | |||
} | |||
.margin-bottom { | |||
margin-bottom: 15px; | |||
} | |||
.margin-left { | |||
margin-left: 15px; | |||
} | |||
.margin-right { | |||
margin-right: 15px; | |||
} | |||
@media (max-width: 767px) { | |||
.text-center-xs { | |||
text-align: center; | |||
@@ -3,6 +3,12 @@ | |||
margin-top: 5px; | |||
margin-bottom: 5px; | |||
} | |||
.unit-checkbox label { | |||
position: relative; | |||
} | |||
.unit-checkbox input[type=checkbox] { | |||
margin-left: 0; | |||
} | |||
.unit-checkbox + .checkbox { | |||
margin-top: 5px; | |||
margin-bottom: 5px; | |||
@@ -216,6 +216,18 @@ a.no-decoration:active { | |||
.margin { | |||
margin: 15px; | |||
} | |||
.margin-top { | |||
margin-top: 15px; | |||
} | |||
.margin-bottom { | |||
margin-bottom: 15px; | |||
} | |||
.margin-left { | |||
margin-left: 15px; | |||
} | |||
.margin-right { | |||
margin-right: 15px; | |||
} | |||
@media (max-width: 767px) { | |||
.text-center-xs { | |||
text-align: center; | |||
@@ -486,6 +498,9 @@ fieldset[disabled] .form-control { | |||
.form-control input { | |||
padding: 6px 10px 8px; | |||
} | |||
.input-area { | |||
position: relative; | |||
} | |||
.link-field.ui-front { | |||
z-index: inherit; | |||
} | |||
@@ -587,10 +602,6 @@ li.user-progress .progress-bar { | |||
.intro-area { | |||
padding: 15px 30px; | |||
} | |||
.footnote-area { | |||
padding: 0px 15px; | |||
border-top: 1px solid #d1d8dd; | |||
} | |||
.file-upload .input-group-addon { | |||
color: #8D99A6; | |||
font-size: 12px; | |||
@@ -972,7 +983,7 @@ li.user-progress .progress-bar { | |||
} | |||
input[type="checkbox"] { | |||
position: relative; | |||
height: 16px; | |||
left: -999999px; | |||
} | |||
input[type="checkbox"]:before { | |||
position: absolute; | |||
@@ -990,9 +1001,7 @@ input[type="checkbox"]:before { | |||
-webkit-transition: 150ms color; | |||
-o-transition: 150ms color; | |||
transition: 150ms color; | |||
background-color: white; | |||
padding: 1px; | |||
margin: -1px; | |||
left: 999999px; | |||
} | |||
input[type="checkbox"]:focus:before { | |||
color: #8D99A6; | |||
@@ -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 { | |||
text-decoration: underline; | |||
} | |||
.list-filters { | |||
.filter-list { | |||
position: relative; | |||
} | |||
.list-filters .sort-selector { | |||
.filter-list .sort-selector { | |||
position: absolute; | |||
top: 15px; | |||
right: 15px; | |||
} | |||
.show_filters { | |||
.tag-filters-area { | |||
padding: 15px 15px 0px; | |||
border-bottom: 1px solid #d1d8dd; | |||
} | |||
.set-filters { | |||
.active-tag-filters { | |||
padding-bottom: 4px; | |||
padding-right: 120px; | |||
} | |||
@media (max-width: 767px) { | |||
.set-filters { | |||
.active-tag-filters { | |||
padding-right: 80px; | |||
} | |||
} | |||
.set-filters .btn { | |||
.active-tag-filters .btn { | |||
margin-bottom: 10px; | |||
} | |||
.set-filters .btn-group { | |||
margin-right: 10px; | |||
.active-tag-filters .btn-group { | |||
margin-left: 10px; | |||
white-space: nowrap; | |||
font-size: 0; | |||
} | |||
.set-filters .btn-group .btn-default { | |||
.active-tag-filters .btn-group .btn-default { | |||
background-color: transparent; | |||
border: 1px solid #d1d8dd; | |||
color: #8D99A6; | |||
@@ -51,138 +71,107 @@ | |||
margin-top: 6px; | |||
margin-left: 15px; | |||
} | |||
.filter-box .filter_field { | |||
.filter-box .filter-field { | |||
padding-right: 15px; | |||
width: calc(64%); | |||
} | |||
.filter-box .filter_field .frappe-control { | |||
.filter-box .filter-field .frappe-control { | |||
position: relative; | |||
} | |||
@media (min-width: 768px) { | |||
@media (min-width: 767px) { | |||
.filter-box .row > div[class*="col-sm-"] { | |||
padding-right: 0px; | |||
} | |||
.filter_field { | |||
.filter-field { | |||
width: 65% !important; | |||
} | |||
.filter_field .frappe-control { | |||
.filter-field .frappe-control { | |||
position: relative; | |||
} | |||
} | |||
.list-row { | |||
padding: 9px 15px; | |||
.list-row-container { | |||
border-bottom: 1px solid #d1d8dd; | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
.list-row { | |||
padding: 12px 15px; | |||
height: 40px; | |||
cursor: pointer; | |||
transition: color 0.2s; | |||
-webkit-transition: color 0.2s; | |||
} | |||
.list-row .h6 { | |||
margin-top: 0px; | |||
margin-bottom: 0px; | |||
} | |||
.list-row-head { | |||
.list-row:hover { | |||
background-color: #F7FAFC; | |||
border-bottom: 1px solid #d1d8dd !important; | |||
} | |||
.list-row:hover, | |||
.grid-row:hover { | |||
background-color: #F7FAFC; | |||
} | |||
.no-hover:hover { | |||
background-color: transparent !important; | |||
} | |||
.list-row:last-child { | |||
border-bottom: 0px; | |||
} | |||
.list-row .level-left { | |||
flex: 3; | |||
} | |||
.list-row .level-right { | |||
flex: 1; | |||
} | |||
.list-row-head { | |||
background-color: #F7FAFC; | |||
border-bottom: 1px solid #d1d8dd !important; | |||
} | |||
.list-row .h6 { | |||
margin-top: 0px; | |||
margin-bottom: 0px; | |||
.list-row-head .list-subject { | |||
font-weight: normal; | |||
} | |||
.list-item-col { | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
height: 30px; | |||
padding-top: 3px; | |||
.list-row-head .checkbox-actions { | |||
display: none; | |||
} | |||
.list-paging-area { | |||
padding: 10px 15px; | |||
border-top: 1px solid #d1d8dd; | |||
.list-row-col { | |||
flex: 1; | |||
margin-right: 15px; | |||
} | |||
.list-value { | |||
display: table; | |||
vertical-align: middle; | |||
.list-subject { | |||
flex: 2; | |||
font-weight: bold; | |||
justify-content: start; | |||
} | |||
.list-subject .level-item { | |||
margin-right: 8px; | |||
} | |||
.list-subject.seen { | |||
font-weight: normal; | |||
} | |||
.list-value .list-row-checkbox, | |||
.list-value .liked-by, | |||
.list-value .list-id, | |||
.list-value .list-select-all { | |||
display: table-cell; | |||
vertical-align: middle; | |||
.list-row-activity { | |||
justify-content: flex-end; | |||
min-width: 120px; | |||
} | |||
.list-value .list-row-checkbox, | |||
.list-value .list-select-all { | |||
.list-row-activity .avatar:not(.avatar-empty) { | |||
margin: 0; | |||
margin-right: 7px; | |||
} | |||
.list-value .liked-by { | |||
padding-top: 2px; | |||
.list-row-activity > span { | |||
display: inline-block; | |||
margin-left: 10px; | |||
margin-right: 0; | |||
} | |||
.list-value .list-col-title { | |||
vertical-align: middle; | |||
.list-paging-area, | |||
.footnote-area { | |||
padding: 10px 15px; | |||
border-top: 1px solid #d1d8dd; | |||
overflow: auto; | |||
} | |||
.progress { | |||
height: 10px; | |||
} | |||
.doclist-row { | |||
font-size: 12px; | |||
} | |||
.likes-count { | |||
display: inline-block; | |||
width: 15px; | |||
margin-left: -5px; | |||
color: #8D99A6; | |||
font-size: 12px; | |||
display: none; | |||
} | |||
.doclist-row .docstatus .octicon { | |||
font-size: 12px; | |||
.list-liked-by-me { | |||
margin-bottom: 1px; | |||
} | |||
.doclist-row .progress { | |||
margin-top: 12px; | |||
input.list-check-all, | |||
input.list-row-checkbox { | |||
margin-top: 0px; | |||
} | |||
.filterable { | |||
cursor: pointer; | |||
} | |||
.doclist-row .label { | |||
margin-right: 8px; | |||
} | |||
.list-info-row { | |||
float: left; | |||
margin-top: 1px; | |||
} | |||
.list-row-right .modified { | |||
margin-top: 3px; | |||
} | |||
.list-row-right .list-row-modified { | |||
margin-right: 9px; | |||
margin-top: 3px; | |||
} | |||
.list-row-right { | |||
margin-top: -2px; | |||
margin-bottom: -4px; | |||
} | |||
.list-row-right .indicator { | |||
margin-left: 10px; | |||
margin-right: -5px; | |||
} | |||
.side-panel { | |||
border-bottom: 1px solid #d1d8dd; | |||
margin: 0px -15px; | |||
padding: 5px 15px; | |||
} | |||
.listview-main-section .octicon-heart { | |||
cursor: pointer; | |||
} | |||
@@ -208,38 +197,17 @@ | |||
.like-action.octicon-heart { | |||
color: #ff5858; | |||
} | |||
.list-id { | |||
font-weight: bold; | |||
} | |||
.list-id.seen { | |||
font-weight: normal; | |||
} | |||
.list-col { | |||
height: 20px; | |||
} | |||
.list-value { | |||
vertical-align: middle; | |||
} | |||
@media (max-width: 767px) { | |||
.doclist-row { | |||
font-size: 14px; | |||
} | |||
.doclist-row [type='checkbox'] { | |||
display: none; | |||
} | |||
.doclist-row .list-row-right .list-row-modified { | |||
display: none; | |||
} | |||
} | |||
.list-comment-count { | |||
display: inline-block; | |||
width: 37px; | |||
text-align: left; | |||
} | |||
.result.tags-shown .tag-row { | |||
display: block; | |||
} | |||
.tag-row { | |||
padding-left: 55px; | |||
margin-bottom: 0px; | |||
margin-top: -5px; | |||
display: none; | |||
margin-left: 50px; | |||
} | |||
.taggle_placeholder { | |||
top: 0; | |||
@@ -300,6 +268,7 @@ | |||
padding: 15px; | |||
border-bottom: 1px solid #EBEFF2; | |||
border-right: 1px solid #EBEFF2; | |||
max-width: 25%; | |||
} | |||
.image-view-container .image-view-item:nth-child(4n) { | |||
border-right: none; | |||
@@ -329,6 +298,9 @@ | |||
.image-view-container .image-field img { | |||
max-height: 100%; | |||
} | |||
.image-view-container .image-field.no-image { | |||
background-color: #fafbfc; | |||
} | |||
.image-view-container .placeholder-text { | |||
font-size: 72px; | |||
color: #d1d8dd; | |||
@@ -351,6 +323,7 @@ | |||
@media (max-width: 991px) { | |||
.image-view-container .image-view-item { | |||
flex: 0 0 33.33333333%; | |||
max-width: 33.33333333%; | |||
} | |||
.image-view-container .image-view-item:nth-child(3n) { | |||
border-right: none; | |||
@@ -381,6 +354,7 @@ | |||
} | |||
.image-view-container.three-column .image-view-item { | |||
flex: 0 0 33.33333333%; | |||
max-width: 33.33333333%; | |||
} | |||
.image-view-container.three-column .image-view-item:nth-child(3n) { | |||
border-right: none; | |||
@@ -424,6 +398,10 @@ | |||
.pswp__more-item img { | |||
max-height: 100%; | |||
} | |||
.list-paging-area .gantt-view-mode { | |||
margin-left: 15px; | |||
margin-right: 15px; | |||
} | |||
.gantt .details-container .heading { | |||
margin-bottom: 10px; | |||
font-size: 12px; | |||
@@ -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 { | |||
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: 15px; | |||
} | |||
.margin-top { | |||
margin-top: 15px; | |||
} | |||
.margin-bottom { | |||
margin-bottom: 15px; | |||
} | |||
.margin-left { | |||
margin-left: 15px; | |||
} | |||
.margin-right { | |||
margin-right: 15px; | |||
} | |||
@media (max-width: 767px) { | |||
.text-center-xs { | |||
text-align: center; | |||
@@ -56,5 +56,14 @@ frappe.db = { | |||
callback && callback(r.message); | |||
} | |||
}); | |||
}, | |||
get_doc: function(doctype, name, filters = null) { | |||
return new Promise(resolve => { | |||
frappe.call({ | |||
method: "frappe.client.get", | |||
args: { doctype, name, filters }, | |||
callback: r => resolve(r.message) | |||
}); | |||
}); | |||
} | |||
} | |||
}; |
@@ -8,9 +8,12 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ | |||
set_options() { | |||
if (this.df.options) { | |||
let options = this.df.options || []; | |||
if(typeof options === 'string') { | |||
if (typeof options === 'string') { | |||
options = options.split('\n'); | |||
} | |||
if (typeof options[0] === 'string') { | |||
options = options.map(o => ({label: o, value: o})); | |||
} | |||
this._data = options; | |||
} | |||
}, | |||
@@ -20,13 +23,14 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ | |||
minChars: 0, | |||
maxItems: 99, | |||
autoFirst: true, | |||
list: this.get_data() | |||
list: this.get_data(), | |||
sort: () => { | |||
return 0; | |||
} | |||
}; | |||
}, | |||
setup_awesomplete() { | |||
var me = this; | |||
this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); | |||
$(this.input_area).find('.awesomplete ul').css('min-width', '100%'); | |||
@@ -79,7 +79,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ | |||
if(is_valid) { | |||
return value; | |||
} | |||
frappe.msgprint(__("{0} is not a valid hex color", [value])); | |||
return null; | |||
} | |||
}); |
@@ -179,8 +179,8 @@ frappe.ui.form.AssignToDialog = Class.extend({ | |||
}); | |||
frappe.ui.add_assignment = function(opts, dialog) { | |||
var assign_to = opts.obj.dialog.fields_dict.assign_to.get_value(); | |||
var args = opts.obj.dialog.get_values(); | |||
var assign_to = dialog.fields_dict.assign_to.get_value(); | |||
var args = dialog.get_values(); | |||
if(args && assign_to) { | |||
return frappe.call({ | |||
method: opts.method, | |||
@@ -228,6 +228,14 @@ frappe.form.formatters = { | |||
}, | |||
Email: function(value) { | |||
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() { | |||
var me = this; | |||
this.dialog = new frappe.ui.Dialog({ | |||
hide_on_page_refresh: true, | |||
@@ -38,38 +37,29 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
.then(() => this.load_doctypes()) | |||
.then(() => this.links_not_permitted_or_missing()) | |||
.then(() => this.get_linked_docs()) | |||
.then(() => this.make_html()) | |||
} | |||
.then(() => this.make_html()); | |||
}; | |||
} | |||
make_html() { | |||
const linked_docs = this.frm.__linked_docs; | |||
let html; | |||
let html = ''; | |||
const linked_doctypes = Object.keys(linked_docs); | |||
if(Object.keys(linked_docs).length === 0) { | |||
if (linked_doctypes.length === 0) { | |||
html = __("Not Linked to any record"); | |||
} else { | |||
html = Object.keys(linked_docs).map(dt => { | |||
const list_renderer = new frappe.views.ListRenderer({ | |||
doctype: dt, | |||
list_view: this | |||
}); | |||
return `<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); | |||
@@ -82,7 +72,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
if (this.frm.__linked_doctypes) { | |||
doctypes_to_load = | |||
Object.keys(this.frm.__linked_doctypes) | |||
.filter(doctype => !already_loaded.includes(doctype)); | |||
.filter(doctype => !already_loaded.includes(doctype)); | |||
} | |||
// load all doctypes asynchronously using with_doctype | |||
@@ -100,19 +90,17 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
} | |||
links_not_permitted_or_missing() { | |||
var me = this; | |||
let links = null; | |||
if (this.frm.__linked_doctypes) { | |||
links = | |||
Object.keys(this.frm.__linked_doctypes) | |||
.filter(frappe.model.can_get_report); | |||
.filter(frappe.model.can_get_report); | |||
} | |||
let flag; | |||
if(!links) { | |||
$(this.dialog.body).html( | |||
`${this.frm.__linked_doctypes | |||
$(this.dialog.body).html(`${this.frm.__linked_doctypes | |||
? __("Not enough permission to see links") | |||
: __("Not Linked to any record")}`); | |||
flag = true; | |||
@@ -126,7 +114,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
} | |||
get_linked_doctypes() { | |||
return new Promise((resolve, reject) => { | |||
return new Promise((resolve) => { | |||
if (this.frm.__linked_doctypes) { | |||
resolve(); | |||
} | |||
@@ -160,19 +148,20 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
} | |||
make_doc_head(heading) { | |||
return `<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) { | |||
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> | |||
</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++) { | |||
var conditions = condition_list[i]; %} | |||
<div style="margin-bottom: 5px;"> | |||
<div class="level-item"> | |||
{% if (i > 0) { %}<span style="margin-right: 10px;">{{ __("Or") }}</span>{% } %} | |||
{% for(key in conditions) { %} | |||
<span class="label label-default" style="margin-right: 10px;"> | |||
@@ -10,7 +10,7 @@ | |||
{{ __("Reports") }} <span class="caret"></span> | |||
</a> | |||
<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> | |||
</div> | |||
</li> | |||
@@ -17,7 +17,7 @@ frappe.views.ListSidebar = Class.extend({ | |||
this.cat_tags = []; | |||
}, | |||
make: function() { | |||
var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.list_view.doctype}); | |||
var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.doctype}); | |||
this.sidebar = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>') | |||
.html(sidebar_content) | |||
@@ -96,7 +96,8 @@ frappe.views.ListSidebar = Class.extend({ | |||
$.each(reports, function(name, r) { | |||
if(!r.ref_doctype || r.ref_doctype==me.doctype) { | |||
var report_type = r.report_type==='Report Builder' | |||
? 'Report/' + r.ref_doctype : 'query-report'; | |||
? `List/${r.ref_doctype}/Report` : 'query-report'; | |||
var route = r.route || report_type + '/' + (r.title || r.name); | |||
if(added.indexOf(route)===-1) { | |||
@@ -113,11 +114,11 @@ frappe.views.ListSidebar = Class.extend({ | |||
} | |||
} | |||
}); | |||
} | |||
}; | |||
// from reference doctype | |||
if(this.list_view.list_renderer.settings.reports) { | |||
add_reports(this.list_view.list_renderer.settings.reports) | |||
if(this.list_view.settings.reports) { | |||
add_reports(this.list_view.settings.reports); | |||
} | |||
// from specially tagged reports | |||
@@ -175,7 +176,7 @@ frappe.views.ListSidebar = Class.extend({ | |||
fieldname: 'custom_column', | |||
label: __('Custom Column'), | |||
default: 0, | |||
onchange: function(e) { | |||
onchange: function() { | |||
var checked = d.get_value('custom_column'); | |||
if(checked) { | |||
$(d.body).find('.frappe-control[data-fieldname="field_name"]').hide(); | |||
@@ -202,19 +203,19 @@ frappe.views.ListSidebar = Class.extend({ | |||
var custom_column = values.custom_column !== undefined ? | |||
values.custom_column : 1; | |||
var field_name; | |||
if(custom_column) { | |||
var field_name = 'kanban_column'; | |||
field_name = 'kanban_column'; | |||
} else { | |||
var field_name = | |||
field_name = | |||
select_fields | |||
.find(df => df.label === values.field_name) | |||
.fieldname; | |||
} | |||
me.add_custom_column_field(custom_column) | |||
.then(function(custom_column) { | |||
return me.make_kanban_board(values.board_name, field_name) | |||
.then(function() { | |||
return me.make_kanban_board(values.board_name, field_name); | |||
}) | |||
.then(function() { | |||
d.hide(); | |||
@@ -321,7 +322,7 @@ frappe.views.ListSidebar = Class.extend({ | |||
if(has_common(frappe.user_roles, ["System Manager", "Administrator"])) { | |||
$(`<li class="new-email-account"><a>${__("New Email Account")}</a></li>`) | |||
.appendTo($dropdown) | |||
.appendTo($dropdown); | |||
} | |||
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); | |||
if(account.email_id === "Sent Mail") | |||
divider = false | |||
divider = false; | |||
}); | |||
$dropdown.find('.new-email-account').click(function() { | |||
@@ -354,7 +355,7 @@ frappe.views.ListSidebar = Class.extend({ | |||
// if account is holding one user free plan or | |||
// if account's expiry date within range of 30 days from today's date | |||
let upgrade_date = frappe.datetime.add_days(get_today(), 30); | |||
let upgrade_date = frappe.datetime.add_days(frappe.datetime.get_today(), 30); | |||
if (frappe.boot.limits.users === 1 || upgrade_date >= frappe.boot.limits.expiry) { | |||
let upgrade_box = $(`<div class="border" style=" | |||
padding: 0px 10px; | |||
@@ -386,12 +387,12 @@ frappe.views.ListSidebar = Class.extend({ | |||
args: { | |||
stats: me.stats, | |||
doctype: me.doctype, | |||
filters:me.default_filters | |||
filters: me.default_filters || [] | |||
}, | |||
callback: function(r) { | |||
me.defined_category = r.message; | |||
if (r.message.defined_cat ){ | |||
me.defined_category = r.message.defined_cat | |||
me.defined_category = r.message.defined_cat; | |||
me.cats = {}; | |||
//structure the tag categories | |||
for (var i in me.defined_category){ | |||
@@ -400,10 +401,10 @@ frappe.views.ListSidebar = Class.extend({ | |||
}else{ | |||
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) { | |||
me.render_stat(i, (me.tempstats || {})["_user_tags"],v); | |||
}); | |||
@@ -414,19 +415,18 @@ frappe.views.ListSidebar = Class.extend({ | |||
//render normal stats | |||
me.render_stat("_user_tags", (r.message.stats|| {})["_user_tags"]); | |||
} | |||
me.list_view.set_sidebar_height(); | |||
} | |||
}); | |||
}, | |||
render_stat: function(field, stat, tags) { | |||
var me = this; | |||
var sum = 0; | |||
var stats = [] | |||
var stats = []; | |||
var label = frappe.meta.docfield_map[this.doctype][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) { | |||
for (var t in tags) { | |||
@@ -454,12 +454,12 @@ frappe.views.ListSidebar = Class.extend({ | |||
sum: sum, | |||
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() { | |||
var fieldname = $(this).attr('data-field'); | |||
var label = $(this).attr('data-label'); | |||
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(); | |||
} else { | |||
me.set_filter(fieldname, label); | |||
@@ -467,7 +467,7 @@ frappe.views.ListSidebar = Class.extend({ | |||
}) | |||
.insertBefore(this.sidebar.find(".close-sidebar-button")); | |||
}, | |||
set_fieldtype: function(df, fieldtype) { | |||
set_fieldtype: function(df) { | |||
// scrub | |||
if(df.fieldname=="docstatus") { | |||
@@ -476,11 +476,11 @@ frappe.views.ListSidebar = Class.extend({ | |||
{value:0, label:"Draft"}, | |||
{value:1, label:"Submitted"}, | |||
{value:2, label:"Cancelled"}, | |||
] | |||
]; | |||
} else if(df.fieldtype=='Check') { | |||
df.fieldtype='Select'; | |||
df.options=[{value:0,label:'No'}, | |||
{value:1,label:'Yes'}] | |||
{value:1,label:'Yes'}]; | |||
} else if(['Text','Small Text','Text Editor','Code','Tag','Comments', | |||
'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { | |||
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) { | |||
if(!footnote_area) { | |||
footnote_area = $('<div class="text-muted footnote-area">') | |||
footnote_area = $('<div class="text-muted footnote-area level">') | |||
.appendTo(wrapper); | |||
} | |||
if(txt) { | |||
if(!txt.includes('<p>')) | |||
txt = '<p>' + txt + '</p>'; | |||
footnote_area.html(txt); | |||
} else { | |||
footnote_area.remove(); | |||
@@ -591,7 +589,47 @@ frappe.utils = { | |||
catch (err) { | |||
return false; | |||
} | |||
}() | |||
}(), | |||
throttle: function (func, wait, options) { | |||
var context, args, result; | |||
var timeout = null; | |||
var previous = 0; | |||
if (!options) options = {}; | |||
let later = function () { | |||
previous = options.leading === false ? 0 : Date.now(); | |||
timeout = null; | |||
result = func.apply(context, args); | |||
if (!timeout) context = args = null; | |||
}; | |||
return function () { | |||
var now = Date.now(); | |||
if (!previous && options.leading === false) previous = now; | |||
let remaining = wait - (now - previous); | |||
context = this; | |||
args = arguments; | |||
if (remaining <= 0 || remaining > wait) { | |||
if (timeout) { | |||
clearTimeout(timeout); | |||
timeout = null; | |||
} | |||
previous = now; | |||
result = func.apply(context, args); | |||
if (!timeout) context = args = null; | |||
} else if (!timeout && options.trailing !== false) { | |||
timeout = setTimeout(later, remaining); | |||
} | |||
return result; | |||
}; | |||
}, | |||
get_form_link: function(doctype, name, html = false) { | |||
const route = ['#Form', doctype, name].join('/'); | |||
if (html) { | |||
return `<a href="${route}">${name}</a>`; | |||
} | |||
return route; | |||
} | |||
}; | |||
// String.prototype.includes polyfill | |||
@@ -27,6 +27,8 @@ $.extend(frappe.model, { | |||
{fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, | |||
], | |||
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], | |||
std_fields_table: [ | |||
{fieldname:'parent', fieldtype:'Data', label:__('Parent')}, | |||
], | |||
@@ -39,8 +41,9 @@ $.extend(frappe.model, { | |||
// setup refresh if the document is updated somewhere else | |||
frappe.realtime.on("doc_update", function(data) { | |||
// set list dirty | |||
frappe.views.set_list_as_dirty(data.doctype); | |||
frappe.views.ListView.trigger_list_update(data); | |||
var doc = locals[data.doctype] && locals[data.doctype][data.name]; | |||
if(doc) { | |||
// current document is dirty, show message if its not me | |||
if(frappe.get_route()[0]==="Form" && cur_frm.doc.doctype===doc.doctype && cur_frm.doc.name===doc.name) { | |||
@@ -61,7 +64,7 @@ $.extend(frappe.model, { | |||
}); | |||
frappe.realtime.on("list_update", function(data) { | |||
frappe.views.set_list_as_dirty(data.doctype); | |||
frappe.views.ListView.trigger_list_update(data); | |||
}); | |||
}, | |||
@@ -74,6 +77,10 @@ $.extend(frappe.model, { | |||
return frappe.model.no_value_type.indexOf(fieldtype)===-1; | |||
}, | |||
is_non_std_field: function(fieldname) { | |||
return !frappe.model.std_fields_list.includes(fieldname); | |||
}, | |||
get_std_field: function(fieldname) { | |||
var docfield = $.map([].concat(frappe.model.std_fields).concat(frappe.model.std_fields_table), | |||
function(d) { | |||
@@ -576,6 +583,19 @@ $.extend(frappe.model, { | |||
} | |||
return all; | |||
}, | |||
get_full_column_name: function(fieldname, doctype) { | |||
if (fieldname.includes('`tab')) return fieldname; | |||
return '`tab' + doctype + '`.`' + fieldname + '`'; | |||
}, | |||
is_numeric_field: function(fieldtype) { | |||
if (!fieldtype) return; | |||
if (typeof fieldtype === 'object') { | |||
fieldtype = fieldtype.fieldtype; | |||
} | |||
return frappe.model.numeric_fieldtypes.includes(fieldtype); | |||
} | |||
}); | |||
// legacy | |||
@@ -5,6 +5,7 @@ $.extend(frappe.model.user_settings, { | |||
var user_settings = frappe.model.user_settings[doctype] || {}; | |||
if ($.isPlainObject(value)) { | |||
user_settings[key] = user_settings[key] || {}; | |||
$.extend(user_settings[key], value); | |||
} else { | |||
user_settings[key] = value; | |||
@@ -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="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"> | |||
<select class="condition form-control"> | |||
<option value="=">{%= __("Equals") %}</option> | |||
@@ -17,7 +17,7 @@ | |||
</select> | |||
</div> | |||
<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"> | |||
<a class="set-filter-and-run btn btn-sm btn-primary pull-left"> | |||
<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, | |||
placement: "right", | |||
content: function() { | |||
var liked_by = JSON.parse($wrapper.attr('data-liked-by') || "[]"); | |||
var liked_by = $wrapper.attr('data-liked-by'); | |||
liked_by = liked_by ? decodeURI(liked_by) : '[]'; | |||
liked_by = JSON.parse(liked_by); | |||
var user = frappe.session.user; | |||
// hack | |||
if ($wrapper.find(".not-liked").length) { | |||
@@ -184,5 +184,9 @@ frappe.ui.SortSelector = Class.extend({ | |||
return this.labels[fieldname] | |||
|| frappe.meta.get_label(this.doctype, fieldname); | |||
} | |||
}, | |||
get_sql_string: function() { | |||
// build string like `tabTask`.`subject` desc | |||
return '`tab' + this.doctype + '`.`' + this.sort_by + '` ' + this.sort_order; | |||
} | |||
}) |
@@ -377,7 +377,7 @@ frappe.upload = { | |||
frappe.throw(__("File size exceeded the maximum allowed size of {0} MB", [max_file_size / 1048576])); | |||
} | |||
}, | |||
multifile_upload:function(fileobjs, args, opts) { | |||
multifile_upload:function(fileobjs, args, opts={}) { | |||
//loop through filenames and checkboxes then append to list | |||
var fields = []; | |||
for (var i =0,j = fileobjs.length;i<j;i++) { | |||
@@ -17,17 +17,23 @@ frappe.breadcrumbs = { | |||
}, | |||
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(); | |||
}, | |||
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() { | |||
@@ -38,11 +44,19 @@ frappe.breadcrumbs = { | |||
} | |||
var $breadcrumbs = $("#navbar-breadcrumbs").empty(); | |||
if(!breadcrumbs) { | |||
$("body").addClass("no-breadcrumbs"); | |||
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 | |||
var from_module = frappe.breadcrumbs.get_doctype_module(breadcrumbs.doctype); | |||
@@ -4,82 +4,63 @@ | |||
frappe.provide("frappe.views.calendar"); | |||
frappe.provide("frappe.views.calendars"); | |||
frappe.views.CalendarView = frappe.views.ListRenderer.extend({ | |||
name: 'Calendar', | |||
render_view: function() { | |||
var me = this; | |||
this.get_calendar_options() | |||
.then(options => { | |||
this.calendar = new frappe.views.Calendar(options); | |||
}); | |||
}, | |||
load_last_view: function() { | |||
frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||
static load_last_view() { | |||
const route = frappe.get_route(); | |||
if (!route[3]) { | |||
// routed to Calendar view, check last calendar_view | |||
let calendar_view = this.user_settings.last_calendar_view; | |||
if (calendar_view) { | |||
frappe.set_route('List', this.doctype, 'Calendar', calendar_view); | |||
return true; | |||
} | |||
if (route.length === 3) { | |||
const doctype = route[1]; | |||
const user_settings = frappe.get_user_settings(doctype)['Calendar'] || {}; | |||
route.push(user_settings.last_calendar_view || 'Default'); | |||
frappe.set_route(route); | |||
return true; | |||
} else { | |||
return false; | |||
} | |||
} | |||
return false; | |||
}, | |||
set_defaults: function() { | |||
this._super(); | |||
setup_defaults() { | |||
super.setup_defaults(); | |||
this.page_title = this.page_title + ' ' + __('Calendar'); | |||
this.no_realtime = true; | |||
this.show_no_result = false; | |||
this.hide_sort_selector = true; | |||
}, | |||
get_header_html: function() { | |||
return null; | |||
}, | |||
should_refresh: function() { | |||
var should_refresh = this._super(); | |||
if(!should_refresh) { | |||
this.last_calendar_view = this.current_calendar_view || ''; | |||
this.current_calendar_view = this.get_calendar_view(); | |||
if (this.current_calendar_view !== 'Default') { | |||
this.page_title = __(this.current_calendar_view); | |||
} else { | |||
this.page_title = this.doctype + ' ' + __('Calendar'); | |||
} | |||
this.calendar_settings = frappe.views.calendar[this.doctype] || {}; | |||
this.calendar_name = frappe.get_route()[3]; | |||
} | |||
before_render() { | |||
super.before_render(); | |||
this.save_view_user_settings({ | |||
last_calendar: this.calendar_name | |||
}); | |||
} | |||
should_refresh = this.current_calendar_view !== this.last_calendar_view; | |||
render() { | |||
if (this.calendar) { | |||
this.calendar.refresh(); | |||
return; | |||
} | |||
return should_refresh; | |||
}, | |||
get_calendar_view: function() { | |||
return frappe.get_route()[3]; | |||
}, | |||
get_calendar_options: function() { | |||
const calendar_view = frappe.get_route()[3] || 'Default'; | |||
// save in user_settings | |||
frappe.model.user_settings.save(this.doctype, 'Calendar', { | |||
last_calendar_view: calendar_view | |||
}); | |||
this.load_lib | |||
.then(() => this.get_calendar_options()) | |||
.then(options => { | |||
this.calendar = new frappe.views.Calendar(options); | |||
}); | |||
} | |||
get_calendar_options() { | |||
const options = { | |||
doctype: this.doctype, | |||
parent: this.wrapper, | |||
page: this.list_view.page, | |||
list_view: this.list_view | |||
} | |||
parent: this.$result, | |||
page: this.page, | |||
list_view: this | |||
}; | |||
const calendar_name = this.calendar_name; | |||
return new Promise(resolve => { | |||
if (calendar_view === 'Default') { | |||
if (calendar_name === 'Default') { | |||
Object.assign(options, frappe.views.calendar[this.doctype]); | |||
resolve(options); | |||
} else { | |||
frappe.model.with_doc('Calendar View', calendar_view, () => { | |||
const doc = frappe.get_doc('Calendar View', calendar_view); | |||
frappe.model.with_doc('Calendar View', calendar_name, () => { | |||
const doc = frappe.get_doc('Calendar View', calendar_name); | |||
Object.assign(options, { | |||
field_map: { | |||
id: "name", | |||
@@ -88,18 +69,20 @@ frappe.views.CalendarView = frappe.views.ListRenderer.extend({ | |||
title: doc.subject_field | |||
} | |||
}); | |||
resolve(options); | |||
}); | |||
} | |||
}) | |||
}, | |||
required_libs: [ | |||
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css', | |||
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js', | |||
'assets/frappe/js/lib/fullcalendar/locale-all.js' | |||
] | |||
}) | |||
}); | |||
} | |||
get required_libs() { | |||
return [ | |||
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css', | |||
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js', | |||
'assets/frappe/js/lib/fullcalendar/locale-all.js' | |||
]; | |||
} | |||
}; | |||
frappe.views.Calendar = Class.extend({ | |||
init: function(options) { | |||
@@ -123,11 +106,10 @@ frappe.views.Calendar = Class.extend({ | |||
$(this.parent).on("show", function() { | |||
me.$cal.fullCalendar("refetchEvents"); | |||
}) | |||
}); | |||
}, | |||
make: function() { | |||
var me = this; | |||
this.$wrapper = this.parent; | |||
this.$cal = $("<div>").appendTo(this.$wrapper); | |||
this.footnote_area = frappe.utils.set_footnote(this.footnote_area, this.$wrapper, | |||
@@ -146,9 +128,9 @@ frappe.views.Calendar = Class.extend({ | |||
this.$wrapper.find(".fc-button-group").addClass("btn-group"); | |||
this.$wrapper.find('.fc-prev-button span') | |||
.attr('class', '').addClass('fa fa-chevron-left') | |||
.attr('class', '').addClass('fa fa-chevron-left'); | |||
this.$wrapper.find('.fc-next-button span') | |||
.attr('class', '').addClass('fa fa-chevron-right') | |||
.attr('class', '').addClass('fa fa-chevron-right'); | |||
var btn_group = this.$wrapper.find(".fc-button-group"); | |||
btn_group.find(".fc-state-active").addClass("active"); | |||
@@ -197,22 +179,22 @@ frappe.views.Calendar = Class.extend({ | |||
events = me.prepare_events(events); | |||
callback(events); | |||
} | |||
}) | |||
}); | |||
}, | |||
eventRender: function(event, element) { | |||
element.attr('title', event.tooltip); | |||
}, | |||
eventClick: function(event, jsEvent, view) { | |||
eventClick: function(event) { | |||
// edit event description or delete | |||
var doctype = event.doctype || me.doctype; | |||
if(frappe.model.can_read(doctype)) { | |||
frappe.set_route("Form", doctype, event.name); | |||
} | |||
}, | |||
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) { | |||
eventDrop: function(event, delta, revertFunc) { | |||
me.update_event(event, revertFunc); | |||
}, | |||
eventResize: function(event, delta, revertFunc, jsEvent, ui, view) { | |||
eventResize: function(event, delta, revertFunc) { | |||
me.update_event(event, revertFunc); | |||
}, | |||
select: function(startDate, endDate, jsEvent, view) { | |||
@@ -269,7 +251,7 @@ frappe.views.Calendar = Class.extend({ | |||
doctype: this.doctype, | |||
start: this.get_system_datetime(start), | |||
end: this.get_system_datetime(end), | |||
filters: this.list_view.filter_list.get_filters(), | |||
filters: this.list_view.filter_area.get(), | |||
field_map: this.field_map | |||
}; | |||
return args; | |||
@@ -384,4 +366,4 @@ frappe.views.Calendar = Class.extend({ | |||
event.end = event.end ? $.fullCalendar.moment(event.end).add(1, "day").stripTime() : null; | |||
} | |||
} | |||
}) | |||
}); |
@@ -418,8 +418,7 @@ frappe.views.CommunicationComposer = Class.extend({ | |||
if(!form_values) return; | |||
var selected_attachments = | |||
$.map($(me.dialog.wrapper) | |||
.find("[data-file-name]:checked"), function (element) { | |||
$.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) { | |||
return $(element).attr("data-file-name"); | |||
}); | |||
@@ -11,12 +11,9 @@ frappe.views.Factory = Class.extend({ | |||
show: function() { | |||
var page_name = frappe.get_route_str(), | |||
me = this; | |||
if(page_name.substr(0, 4) === 'List') { | |||
page_name = frappe.get_route().slice(0, 2).join('/'); | |||
} | |||
if(frappe.pages[page_name] && !page_name.includes("Form/")) { | |||
frappe.container.change_to(frappe.pages[page_name]); | |||
frappe.container.change_to(page_name); | |||
if(me.on_show) { | |||
me.on_show(); | |||
} | |||
@@ -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.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_dom(); | |||
}, | |||
} | |||
render_view: function(values) { | |||
prepare_tasks() { | |||
var me = this; | |||
this.prepare(values); | |||
this.render_gantt(); | |||
}, | |||
var meta = this.meta; | |||
var field_map = this.calendar_settings.field_map; | |||
set_defaults: function() { | |||
this._super(); | |||
this.no_realtime = true; | |||
this.page_title = this.page_title + ' ' + __('Gantt'); | |||
}, | |||
this.tasks = this.data.map(function (item) { | |||
// set progress | |||
var progress = 0; | |||
if (field_map.progress && $.isFunction(field_map.progress)) { | |||
progress = field_map.progress(item); | |||
} else if (field_map.progress) { | |||
progress = item[field_map.progress]; | |||
} | |||
init_settings: function() { | |||
this._super(); | |||
this.field_map = frappe.views.calendar[this.doctype].field_map; | |||
this.order_by = this.order_by || this.field_map.start + ' asc'; | |||
}, | |||
// title | |||
var label; | |||
if (meta.title_field) { | |||
label = $.format("{0} ({1})", [item[meta.title_field], item.name]); | |||
} else { | |||
label = item[field_map.title]; | |||
} | |||
prepare_dom: function() { | |||
this.wrapper.css('overflow', 'auto') | |||
.append('<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, { | |||
view_mode: this.gantt_view_mode, | |||
view_mode: gantt_view_mode, | |||
date_format: "YYYY-MM-DD", | |||
on_click: function (task) { | |||
frappe.set_route('Form', task.doctype, task.id); | |||
}, | |||
on_date_change: function(task, start, end) { | |||
if(!me.can_write()) return; | |||
me.update_gantt_task(task, start, end); | |||
on_date_change: function (task, start, end) { | |||
if (!me.can_write) return; | |||
frappe.db.set_value(task.doctype, task.id, { | |||
[field_map.start]: start.format(date_format), | |||
[field_map.end]: end.format(date_format) | |||
}); | |||
}, | |||
on_progress_change: function(task, progress) { | |||
if(!me.can_write()) return; | |||
on_progress_change: function (task, progress) { | |||
if (!me.can_write) return; | |||
var progress_fieldname = 'progress'; | |||
if($.isFunction(field_map.progress)) { | |||
if ($.isFunction(field_map.progress)) { | |||
progress_fieldname = null; | |||
} else if(field_map.progress) { | |||
} else if (field_map.progress) { | |||
progress_fieldname = field_map.progress; | |||
} | |||
if(progress_fieldname) { | |||
frappe.db.set_value(task.doctype, task.id, | |||
progress_fieldname, parseInt(progress)); | |||
if (progress_fieldname) { | |||
frappe.db.set_value(task.doctype, task.id, { | |||
[progress_fieldname]: parseInt(progress) | |||
}); | |||
} | |||
}, | |||
on_view_change: function(mode) { | |||
on_view_change: function (mode) { | |||
// save view mode | |||
frappe.model.user_settings.save(me.doctype, 'Gantt', { | |||
me.save_view_user_settings({ | |||
gantt_view_mode: mode | |||
}); | |||
}, | |||
custom_popup_html: function(task) { | |||
custom_popup_html: function (task) { | |||
var item = me.get_item(task.id); | |||
var html = | |||
@@ -76,48 +120,51 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ | |||
// custom html in doctype settings | |||
var custom = me.settings.gantt_custom_popup_html; | |||
if(custom && $.isFunction(custom)) { | |||
if (custom && $.isFunction(custom)) { | |||
var ganttobj = task; | |||
html = custom(ganttobj, item); | |||
} | |||
return '<div class="details-container">' + html + '</div>'; | |||
} | |||
}); | |||
this.render_dropdown(); | |||
this.setup_view_mode_buttons(); | |||
this.set_colors(); | |||
}, | |||
render_dropdown: function() { | |||
var me = this; | |||
var view_modes = this.gantt.config.view_modes || []; | |||
var dropdown = "<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"), | |||
//__("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 | |||
.map(t => t.custom_class) | |||
.filter(c => c && c.startsWith('color-')); | |||
@@ -137,102 +184,19 @@ frappe.views.GanttView = frappe.views.ListRenderer.extend({ | |||
}).join(""); | |||
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; | |||
}); | |||
}, | |||
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.views.ImageView = frappe.views.ListRenderer.extend({ | |||
name: 'Image', | |||
render_view: function (values) { | |||
this.items = values; | |||
frappe.views.ImageView = class ImageView extends frappe.views.ListView { | |||
setup_defaults() { | |||
super.setup_defaults(); | |||
this.page_title = this.page_title + ' ' + __('Images'); | |||
} | |||
set_fields() { | |||
this._fields = [ | |||
'name', | |||
this.meta.title_field, | |||
this.meta.image_field | |||
]; | |||
} | |||
update_data(data) { | |||
super.update_data(data); | |||
this.items = this.data.map(this.prepare_data.bind(this)); | |||
} | |||
render() { | |||
this.get_attached_images() | |||
.then(() => { | |||
this.render_image_view(); | |||
@@ -18,105 +34,138 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ | |||
this.gallery.prepare_pswp_items(this.items, this.images_map); | |||
} | |||
}); | |||
}, | |||
set_defaults: function() { | |||
this._super(); | |||
this.page_title = this.page_title + ' ' + __('Images'); | |||
}, | |||
prepare_data: function(data) { | |||
data = this._super(data); | |||
} | |||
prepare_data(data) { | |||
// absolute url if cordova, else relative | |||
data._image_url = this.get_image_url(data); | |||
return data; | |||
}, | |||
render_image_view: function () { | |||
var html = this.items.map(this.render_item.bind(this)).join(""); | |||
this.container = this.wrapper.find('.image-view-container'); | |||
if (this.container.length === 0) { | |||
this.container = $('<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; | |||
url = item.image ? item.image : item[this.meta.image_field]; | |||
url = data.image ? data.image : data[this.meta.image_field]; | |||
// absolute url for mobile | |||
if (window.cordova && !frappe.utils.is_url(url)) { | |||
url = frappe.base_url + url; | |||
} | |||
if (url) { | |||
return url | |||
return url; | |||
} | |||
return null; | |||
}, | |||
get_attached_images: function () { | |||
} | |||
get_attached_images() { | |||
return frappe.call({ | |||
method: 'frappe.core.doctype.file.file.get_attached_images', | |||
args: { doctype: this.doctype, names: this.items.map(i => i.name) } | |||
args: { | |||
doctype: this.doctype, | |||
names: this.items.map(i => i.name) | |||
} | |||
}).then(r => { | |||
this.images_map = Object.assign(this.images_map || {}, r.message); | |||
}); | |||
}, | |||
get_header_html: function () { | |||
var main = frappe.render_template('list_item_main_head', { | |||
col: { type: "Subject" }, | |||
_checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) | |||
&& !this.no_delete) | |||
}); | |||
return frappe.render_template('list_item_row_head', { main: main, list: this }); | |||
}, | |||
setup_gallery: function() { | |||
} | |||
get_header_html() { | |||
return this.get_header_html_skeleton(` | |||
<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; | |||
this.gallery = new frappe.views.GalleryView({ | |||
doctype: this.doctype, | |||
items: this.items, | |||
wrapper: this.container, | |||
wrapper: this.$result, | |||
images_map: this.images_map | |||
}); | |||
this.container.on('click', '.btn.zoom-view', function(e) { | |||
this.$result.on('click', '.btn.zoom-view', function (e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
var name = $(this).data().name; | |||
name = decodeURIComponent(name); | |||
me.gallery.show(name); | |||
return false; | |||
}); | |||
} | |||
}); | |||
}; | |||
frappe.views.GalleryView = Class.extend({ | |||
init: function(opts) { | |||
init: function (opts) { | |||
$.extend(this, opts); | |||
var me = this; | |||
this.lib_ready = this.load_lib(); | |||
this.lib_ready.then(function() { | |||
this.lib_ready.then(function () { | |||
me.prepare(); | |||
}); | |||
}, | |||
prepare: function() { | |||
prepare: function () { | |||
// keep only one pswp dom element | |||
this.pswp_root = $('body > .pswp'); | |||
if(this.pswp_root.length === 0) { | |||
if (this.pswp_root.length === 0) { | |||
var pswp = frappe.render_template('photoswipe_dom'); | |||
this.pswp_root = $(pswp).appendTo('body'); | |||
} | |||
}, | |||
prepare_pswp_items: function(_items, _images_map) { | |||
prepare_pswp_items: function (_items, _images_map) { | |||
var me = this; | |||
if (_items) { | |||
@@ -126,18 +175,18 @@ frappe.views.GalleryView = Class.extend({ | |||
} | |||
return new Promise(resolve => { | |||
const items = this.items.map(function(i) { | |||
const query = 'img[data-name="'+i.name+'"]'; | |||
const items = this.items.map(function (i) { | |||
const query = 'img[data-name="' + i._name + '"]'; | |||
let el = me.wrapper.find(query).get(0); | |||
let width, height; | |||
if(el) { | |||
if (el) { | |||
width = el.naturalWidth; | |||
height = el.naturalHeight; | |||
} | |||
if(!el) { | |||
el = me.wrapper.find('.image-field[data-name="'+i.name+'"]').get(0); | |||
if (!el) { | |||
el = me.wrapper.find('.image-field[data-name="' + i._name + '"]').get(0); | |||
width = el.getBoundingClientRect().width; | |||
height = el.getBoundingClientRect().height; | |||
} | |||
@@ -149,26 +198,26 @@ frappe.views.GalleryView = Class.extend({ | |||
w: width, | |||
h: height, | |||
el: el | |||
} | |||
}; | |||
}); | |||
this.pswp_items = items; | |||
resolve(); | |||
}); | |||
}, | |||
show: function(docname) { | |||
show: function (docname) { | |||
this.lib_ready | |||
.then(() => this.prepare_pswp_items()) | |||
.then(() => this._show(docname)); | |||
}, | |||
_show: function(docname) { | |||
_show: function (docname) { | |||
const me = this; | |||
const items = this.pswp_items; | |||
const item_index = items.findIndex(item => item.name === docname); | |||
var options = { | |||
index: item_index, | |||
getThumbBoundsFn: function(index) { | |||
const query = 'img[data-name="' + items[index].name + '"]'; | |||
getThumbBoundsFn: function (index) { | |||
const query = 'img[data-name="' + items[index]._name + '"]'; | |||
let thumbnail = me.wrapper.find(query).get(0); | |||
if (!thumbnail) { | |||
@@ -178,12 +227,16 @@ frappe.views.GalleryView = Class.extend({ | |||
var pageYScroll = window.pageYOffset || document.documentElement.scrollTop, | |||
rect = thumbnail.getBoundingClientRect(); | |||
return {x:rect.left, y:rect.top + pageYScroll, w:rect.width}; | |||
return { | |||
x: rect.left, | |||
y: rect.top + pageYScroll, | |||
w: rect.width | |||
}; | |||
}, | |||
history: false, | |||
shareEl: false, | |||
showHideOpacity: true | |||
} | |||
}; | |||
// init | |||
this.pswp = new PhotoSwipe( | |||
@@ -195,12 +248,12 @@ frappe.views.GalleryView = Class.extend({ | |||
this.browse_images(); | |||
this.pswp.init(); | |||
}, | |||
browse_images: function() { | |||
browse_images: function () { | |||
const $more_items = this.pswp_root.find('.pswp__more-items'); | |||
const images_map = this.images_map; | |||
let last_hide_timeout = null; | |||
this.pswp.listen('afterChange', function() { | |||
this.pswp.listen('afterChange', function () { | |||
const images = images_map[this.currItem.name]; | |||
if (!images || images.length === 1) { | |||
$more_items.html(''); | |||
@@ -214,7 +267,9 @@ frappe.views.GalleryView = Class.extend({ | |||
this.pswp.listen('beforeChange', hide_more_items); | |||
this.pswp.listen('initialZoomOut', hide_more_items); | |||
this.pswp.listen('destroy', $(document).off('mousemove', hide_more_items_after_2s)); | |||
this.pswp.listen('destroy', () => { | |||
$(document).off('mousemove', hide_more_items_after_2s); | |||
}); | |||
// Replace current image on click | |||
$more_items.on('click', '.pswp__more-item', (e) => { | |||
@@ -255,7 +310,7 @@ frappe.views.GalleryView = Class.extend({ | |||
</div>`; | |||
} | |||
}, | |||
load_lib: function() { | |||
load_lib: function () { | |||
return new Promise(resolve => { | |||
var asset_dir = 'assets/frappe/js/lib/photoswipe/'; | |||
frappe.require([ | |||
@@ -266,4 +321,4 @@ frappe.views.GalleryView = Class.extend({ | |||
], resolve); | |||
}); | |||
} | |||
}); | |||
}); |
@@ -32,13 +32,4 @@ | |||
</div> | |||
</a> | |||
</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> |
@@ -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.views.InboxView = frappe.views.ListRenderer.extend({ | |||
name: 'Inbox', | |||
render_view: function(values) { | |||
var me = this; | |||
frappe.views.InboxView = class InboxView extends frappe.views.ListView { | |||
static load_last_view() { | |||
const route = frappe.get_route(); | |||
if (!route[3] && frappe.boot.email_accounts.length) { | |||
let email_account; | |||
if (frappe.boot.email_accounts[0].email_id == "All Accounts") { | |||
email_account = "All Accounts"; | |||
} else { | |||
email_account = frappe.boot.email_accounts[0].email_account; | |||
} | |||
frappe.set_route("List", "Communication", "Inbox", email_account); | |||
return true; | |||
} else if (!route[3] || (route[3] !== "All Accounts" && !is_valid(route[3]))) { | |||
frappe.msgprint(__('Invalid Email Account')); | |||
window.history.back(); | |||
return true; | |||
} | |||
return false; | |||
this.emails = values; | |||
function is_valid(email_account) { | |||
return frappe.boot.email_accounts.find(d => d.email_account === email_account); | |||
} | |||
} | |||
show() { | |||
super.show(); | |||
// save email account in user_settings | |||
frappe.model.user_settings.save("Communication", 'Inbox', { | |||
this.save_view_user_settings({ | |||
last_email_account: this.current_email_account | |||
}); | |||
} | |||
this.render_inbox_view(); | |||
}, | |||
render_inbox_view: function() { | |||
var html = "" | |||
var email_account = this.get_current_email_account() | |||
if(email_account) | |||
html = this.emails.map(this.render_email_row.bind(this)).join(""); | |||
else | |||
html = this.make_no_result() | |||
this.container = $('<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(); | |||
}, | |||
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 = [ | |||
["Communication", "communication_type", "=", "Communication", true], | |||
["Communication", "communication_medium", "=", "Email", true], | |||
] | |||
var filters = [] | |||
]; | |||
var filters = []; | |||
if (email_account === "Sent") { | |||
filters = default_filters.concat([ | |||
["Communication", "sent_or_received", "=", "Sent", true], | |||
["Communication", "email_status", "not in", "Spam,Trash", true], | |||
]) | |||
} | |||
else if (in_list(["Spam", "Trash"], email_account)) { | |||
]); | |||
} else if (in_list(["Spam", "Trash"], email_account)) { | |||
filters = default_filters.concat([ | |||
["Communication", "email_status", "=", email_account, true], | |||
["Communication", "email_account", "in", frappe.boot.all_accounts, true] | |||
]) | |||
} | |||
else { | |||
var op = "=" | |||
]); | |||
} else { | |||
var op = "="; | |||
if (email_account == "All Accounts") { | |||
op = "in"; | |||
email_account = frappe.boot.all_accounts | |||
email_account = frappe.boot.all_accounts; | |||
} | |||
filters = default_filters.concat([ | |||
["Communication", "sent_or_received", "=", "Received", true], | |||
["Communication", "email_account", op, email_account, true], | |||
["Communication", "email_status", "not in", "Spam,Trash", true], | |||
]) | |||
]); | |||
} | |||
return filters | |||
}, | |||
get_header_html: function() { | |||
var header = "" | |||
if(this.current_email_account) { | |||
header = frappe.render_template('inbox_view_item_main_head', { | |||
_checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) | |||
&& !this.no_delete), | |||
is_sent_emails: this.is_sent_emails | |||
}); | |||
} | |||
return filters; | |||
} | |||
return header; | |||
}, | |||
get_current_email_account: function() { | |||
var route = frappe.get_route(); | |||
if(!route[3] && frappe.boot.email_accounts.length) { | |||
var email_account; | |||
if(frappe.boot.email_accounts[0].email_id == "All Accounts") { | |||
email_account = "All Accounts" | |||
} else { | |||
email_account = frappe.boot.email_accounts[0].email_account | |||
} | |||
frappe.set_route("List", "Communication", "Inbox", email_account); | |||
} else if(route[3] && route[3] != "All Accounts" && | |||
!frappe.boot.email_accounts.find(b => b.email_account === route[3])) { | |||
// frappe.throw(__(`Email Account <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; | |||
if (in_list(["Spam", "Trash"], email_account)) { | |||
return __("No {0} mail", [email_account]) | |||
} else if(!email_account && !frappe.boot.email_accounts.length) { | |||
return __("No {0} mail", [email_account]); | |||
} else if (!email_account && !frappe.boot.email_accounts.length) { | |||
// email account is not configured | |||
this.no_result_doctype = "Email Account" | |||
args = { | |||
doctype: "Email Account", | |||
msg: __("No Email Account"), | |||
label: __("New Email Account"), | |||
} | |||
}; | |||
} else { | |||
// no sent mail | |||
this.no_result_doctype = "Communication"; | |||
args = { | |||
doctype: "Communication", | |||
msg: __("No Emails"), | |||
label: __("Compose Email") | |||
} | |||
}; | |||
} | |||
var no_result_message = frappe.render_template("inbox_no_result", args) | |||
return no_result_message; | |||
}, | |||
make_new_doc: function() { | |||
if (this.no_result_doctype == "Communication") { | |||
const html = frappe.model.can_create(args.doctype) ? | |||
`<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({ | |||
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); | |||
// save kanban board name in user_settings | |||
frappe.model.user_settings.save(opts.doctype, 'Kanban', { | |||
last_kanban_board: opts.board_name | |||
}); | |||
updater.set({ | |||
doctype: opts.doctype, | |||
board: board, | |||
@@ -57,10 +52,8 @@ frappe.provide("frappe.views"); | |||
}, | |||
update_cards: function (updater, cards) { | |||
var state = this; | |||
var _cards = | |||
cards.map(card => { | |||
return prepare_card(card, state); | |||
}) | |||
var _cards = cards | |||
.map(card => prepare_card(card, state)) | |||
.concat(this.cards) | |||
.uniqBy(card => card.name); | |||
@@ -90,18 +83,18 @@ frappe.provide("frappe.views"); | |||
var board = this.board; | |||
fetch_customization(doctype) | |||
.then(function (doc) { | |||
return modify_column_field_in_c11n(doc, board, col.title, action) | |||
return modify_column_field_in_c11n(doc, board, col.title, action); | |||
}) | |||
.then(save_customization) | |||
.then(function (r) { | |||
return update_kanban_board(board.name, col.title, action) | |||
.then(function () { | |||
return update_kanban_board(board.name, col.title, action); | |||
}).then(function (r) { | |||
var cols = r.message; | |||
updater.set({ | |||
columns: prepare_columns(cols) | |||
}); | |||
}, function (err) { | |||
console.error(err); | |||
console.error(err); // eslint-disable-line | |||
}); | |||
}, | |||
set_filter_state: function (updater) { | |||
@@ -115,14 +108,14 @@ frappe.provide("frappe.views"); | |||
save_filters: function (updater) { | |||
if(saving_filters) return; | |||
saving_filters = true; | |||
var filters = JSON.stringify(this.cur_list.filter_list.get_filters()); | |||
var filters = JSON.stringify(this.cur_list.filter_area.get()); | |||
frappe.call({ | |||
method: method_prefix + 'save_filters', | |||
args: { | |||
board_name: this.board.name, | |||
filters: filters | |||
} | |||
}).then(function(r) { | |||
}).then(function() { | |||
saving_filters = false; | |||
updater.set({ filters_modified: false }); | |||
frappe.show_alert({ | |||
@@ -135,7 +128,6 @@ frappe.provide("frappe.views"); | |||
var doc = frappe.model.get_new_doc(this.doctype); | |||
var field = this.card_meta.title_field; | |||
var quick_entry = this.card_meta.quick_entry; | |||
var board = this.board; | |||
var state = this; | |||
var doc_fields = {}; | |||
@@ -198,7 +190,6 @@ frappe.provide("frappe.views"); | |||
order: order | |||
}, | |||
callback: (r) => { | |||
var state = this; | |||
var board = r.message[0]; | |||
var updated_cards = r.message[1]; | |||
var cards = update_cards_column(updated_cards); | |||
@@ -208,8 +199,7 @@ frappe.provide("frappe.views"); | |||
columns: columns | |||
}); | |||
} | |||
}) | |||
.fail(function(e) { | |||
}).fail(function() { | |||
// revert original order | |||
updater.set({ | |||
cards: _cards, | |||
@@ -246,7 +236,7 @@ frappe.provide("frappe.views"); | |||
updater.set({ | |||
columns: columns | |||
}); | |||
}) | |||
}); | |||
} | |||
} | |||
}); | |||
@@ -262,12 +252,12 @@ frappe.provide("frappe.views"); | |||
// update cards internally | |||
opts.cards = cards; | |||
if(self.wrapper.find('.kanban').length > 0) { | |||
if(self.wrapper.find('.kanban').length > 0 && self.cur_list.start !== 0) { | |||
fluxify.doAction('update_cards', cards); | |||
} else { | |||
init(); | |||
} | |||
} | |||
}; | |||
function init() { | |||
fluxify.doAction('init', opts); | |||
@@ -286,7 +276,7 @@ frappe.provide("frappe.views"); | |||
self.$kanban_board.appendTo(self.wrapper); | |||
} | |||
self.$filter_area = self.cur_list.$page.find('.set-filters'); | |||
self.$filter_area = self.cur_list.$page.find('.active-tag-filters'); | |||
bind_events(); | |||
setup_sortable(); | |||
} | |||
@@ -312,7 +302,7 @@ frappe.provide("frappe.views"); | |||
dataIdAttr: 'data-column-value', | |||
filter: '.add-new-column', | |||
handle: '.kanban-column-title', | |||
onEnd: function(evt) { | |||
onEnd: function() { | |||
var order = sortable.toArray(); | |||
order = order.slice(1); | |||
fluxify.doAction('update_column_order', order); | |||
@@ -322,7 +312,6 @@ frappe.provide("frappe.views"); | |||
function bind_add_column() { | |||
var wrapper = self.$kanban_board; | |||
var $add_new_column = self.$kanban_board.find(".add-new-column"), | |||
$compose_column = $add_new_column.find(".compose-column"), | |||
$compose_column_form = $add_new_column.find(".compose-column-form").hide(); | |||
@@ -342,7 +331,7 @@ frappe.provide("frappe.views"); | |||
var title = $compose_column_form.serializeArray()[0].value; | |||
var col = { | |||
title: title.trim() | |||
} | |||
}; | |||
fluxify.doAction('add_column', col); | |||
$compose_column_form.find('input').val(''); | |||
$compose_column.show(); | |||
@@ -352,7 +341,7 @@ frappe.provide("frappe.views"); | |||
}); | |||
// on form blur | |||
$compose_column_form.find('input').on("blur", function (e) { | |||
$compose_column_form.find('input').on("blur", function () { | |||
$(this).val(''); | |||
$compose_column.show(); | |||
$compose_column_form.hide(); | |||
@@ -362,7 +351,7 @@ frappe.provide("frappe.views"); | |||
function bind_save_filter() { | |||
var set_filter_state = function () { | |||
fluxify.doAction('set_filter_state'); | |||
} | |||
}; | |||
if(isBound(self.$kanban_board, 'after-refresh', set_filter_state)) return; | |||
@@ -375,8 +364,8 @@ frappe.provide("frappe.views"); | |||
function setup_restore_columns() { | |||
var cur_list = store.getState().cur_list; | |||
var columns = store.getState().columns; | |||
var list_row_right = | |||
cur_list.$page.find(`[data-list-renderer='Kanban'] .list-row-right`) | |||
var list_row_right = cur_list.$page | |||
.find(`[data-list-renderer='Kanban'] .list-row-right`) | |||
.css('margin-right', '15px'); | |||
list_row_right.empty(); | |||
@@ -398,16 +387,16 @@ frappe.provide("frappe.views"); | |||
"<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" + | |||
"<span class='dropdown-text'>" + __('Archived Columns') + "</span><i class='caret'></i></a>" + | |||
"<ul class='dropdown-menu'>" + options + "</ul>" + | |||
"</div>") | |||
"</div>"); | |||
list_row_right.html($dropdown); | |||
$dropdown.find(".dropdown-menu").on("click", "button.restore-column", function (e) { | |||
$dropdown.find(".dropdown-menu").on("click", "button.restore-column", function () { | |||
var column_title = $(this).data().column; | |||
var col = { | |||
title: column_title, | |||
status: 'Archived' | |||
} | |||
}; | |||
fluxify.doAction('restore_column', col); | |||
}); | |||
} | |||
@@ -427,7 +416,7 @@ frappe.provide("frappe.views"); | |||
init(); | |||
return self; | |||
} | |||
}; | |||
frappe.views.KanbanBoardColumn = function (column, wrapper) { | |||
var self = {}; | |||
@@ -455,7 +444,6 @@ frappe.provide("frappe.views"); | |||
function make_cards() { | |||
self.$kanban_cards.empty(); | |||
var cards = store.getState().cards; | |||
var board = store.getState().board; | |||
filtered_cards = get_cards_for_column(cards, column); | |||
var filtered_cards_names = filtered_cards.map(card => card.name); | |||
@@ -480,20 +468,20 @@ frappe.provide("frappe.views"); | |||
} | |||
function setup_sortable() { | |||
var sortable = Sortable.create(self.$kanban_cards.get(0), { | |||
Sortable.create(self.$kanban_cards.get(0), { | |||
group: "cards", | |||
animation: 150, | |||
dataIdAttr: 'data-name', | |||
onStart: function (evt) { | |||
onStart: function () { | |||
wrapper.find('.kanban-card.add-card').fadeOut(200, function () { | |||
wrapper.find('.kanban-cards').height('100vh'); | |||
}); | |||
}, | |||
onEnd: function (evt) { | |||
onEnd: function () { | |||
wrapper.find('.kanban-card.add-card').fadeIn(100); | |||
wrapper.find('.kanban-cards').height('auto'); | |||
// update order | |||
var order = {} | |||
var order = {}; | |||
wrapper.find('.kanban-column[data-column-value]') | |||
.each(function() { | |||
var col_name = $(this).data().columnValue; | |||
@@ -505,7 +493,7 @@ frappe.provide("frappe.views"); | |||
}); | |||
fluxify.doAction('update_order', order); | |||
}, | |||
onAdd: function (evt) { | |||
onAdd: function () { | |||
}, | |||
}); | |||
} | |||
@@ -543,7 +531,7 @@ frappe.provide("frappe.views"); | |||
}); | |||
// on textarea blur | |||
$textarea.on("blur", function (e) { | |||
$textarea.on("blur", function () { | |||
$(this).val(''); | |||
$btn_add.show(); | |||
$new_card_area.hide(); | |||
@@ -552,7 +540,7 @@ frappe.provide("frappe.views"); | |||
function bind_options() { | |||
self.$kanban_column.find(".column-options .dropdown-menu") | |||
.on("click", "[data-action]", function (e) { | |||
.on("click", "[data-action]", function () { | |||
var $btn = $(this); | |||
var action = $btn.data().action; | |||
@@ -564,11 +552,11 @@ frappe.provide("frappe.views"); | |||
} | |||
}); | |||
get_column_indicators(function(indicators) { | |||
var html = '<li class="button-group">' | |||
var html = '<li class="button-group">'; | |||
html += indicators.reduce(function(prev, curr) { | |||
return prev + '<div \ | |||
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>'; | |||
self.$kanban_column.find(".column-options .dropdown-menu") | |||
@@ -577,7 +565,7 @@ frappe.provide("frappe.views"); | |||
} | |||
init(); | |||
} | |||
}; | |||
frappe.views.KanbanBoardCard = function (card, wrapper) { | |||
var self = {}; | |||
@@ -630,56 +618,6 @@ frappe.provide("frappe.views"); | |||
}); | |||
} | |||
function setup_edit_card() { | |||
if (self.edit_dialog) { | |||
refresh_dialog(); | |||
self.edit_dialog.show(); | |||
return; | |||
} | |||
var card_meta = store.getState().card_meta; | |||
get_doc().then(function () { | |||
// prepare dialog fields | |||
var fields = []; | |||
if (card_meta.description_field) { | |||
fields.push({ | |||
fieldtype: "Small Text", label: __("Description"), | |||
fieldname: card_meta.description_field.fieldname | |||
}); | |||
} | |||
fields.push({ fieldtype: "Section Break" }); | |||
fields.push({ | |||
fieldtype: "Read Only", label: "Assigned to", | |||
fieldname: "assignees" | |||
}); | |||
fields.push({ fieldtype: "Column Break" }); | |||
if (card_meta.due_date_field) { | |||
fields.push(card_meta.due_date_field); | |||
} | |||
var d = make_edit_dialog(card.title, fields); | |||
refresh_dialog(); | |||
make_timeline(); | |||
edit_card_title(); | |||
d.set_primary_action(__('Save'), function () { | |||
if (d.working) return; | |||
var doc = d.get_values(true); | |||
$.extend(doc, { name: card.name, doctype: card.doctype }); | |||
d.working = true; | |||
fluxify.doAction('update_doc', doc, card) | |||
.then(function (r) { | |||
d.working = false; | |||
d.hide(); | |||
}); | |||
}); | |||
d.show(); | |||
}); | |||
} | |||
function refresh_dialog() { | |||
set_dialog_fields(); | |||
make_assignees(); | |||
@@ -694,36 +632,6 @@ frappe.provide("frappe.views"); | |||
}); | |||
} | |||
function get_doc() { | |||
return new Promise(function (resolve, reject) { | |||
frappe.model.with_doc(card.doctype, card.name, function () { | |||
frappe.call({ | |||
method: 'frappe.client.get', | |||
args: { | |||
doctype: card.doctype, | |||
name: card.name | |||
}, | |||
callback: function (r) { | |||
var doc = r.message; | |||
if (!doc) { | |||
reject(__("{0} {1} does not exist", [card.doctype, card.name])); | |||
} | |||
card.doc = doc; | |||
resolve(); | |||
} | |||
}); | |||
}); | |||
}); | |||
} | |||
function make_edit_dialog(title, fields) { | |||
self.edit_dialog = new frappe.ui.Dialog({ | |||
title: title, | |||
fields: fields | |||
}); | |||
return self.edit_dialog; | |||
} | |||
function make_assignees() { | |||
var d = self.edit_dialog; | |||
var html = get_assignees_html() + '<a class="add-assignment avatar avatar-small avatar-empty">\ | |||
@@ -751,7 +659,7 @@ frappe.provide("frappe.views"); | |||
method: 'frappe.desk.form.assign_to.add', | |||
doctype: card.doctype, | |||
docname: card.name, | |||
callback: function(r) { | |||
callback: function() { | |||
var user = self.assign_to_dialog.get_values().assign_to; | |||
card.assigned_list.push(user); | |||
fluxify.doAction('update_card', card); | |||
@@ -762,100 +670,8 @@ frappe.provide("frappe.views"); | |||
self.assign_to_dialog.show(); | |||
} | |||
function make_timeline() { | |||
var d = self.edit_dialog; | |||
// timeline wrapper | |||
d.$wrapper.find('.modal-body').append('<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(); | |||
} | |||
}; | |||
// Helpers | |||
function get_board(board_name) { | |||
@@ -874,7 +690,7 @@ frappe.provide("frappe.views"); | |||
} | |||
return prepare_board(board); | |||
}, function(e) { | |||
console.log(e) | |||
console.log(e); // eslint-disable-line | |||
}); | |||
} | |||
@@ -897,7 +713,11 @@ frappe.provide("frappe.views"); | |||
} | |||
meta.fields.forEach(function (df) { | |||
if (in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) && !title_field) { | |||
const is_valid_field = | |||
in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) | |||
&& !df.hidden; | |||
if (is_valid_field && !title_field) { | |||
// can be mapped to textarea | |||
title_field = df; | |||
} | |||
@@ -926,7 +746,7 @@ frappe.provide("frappe.views"); | |||
title_field: title_field, | |||
description_field: description_field, | |||
due_date_field: due_date_field, | |||
} | |||
}; | |||
} | |||
function get_date_field(fields) { | |||
@@ -993,7 +813,7 @@ frappe.provide("frappe.views"); | |||
} | |||
function fetch_customization(doctype) { | |||
return new Promise(function (resolve, reject) { | |||
return new Promise(function (resolve) { | |||
frappe.model.with_doc("Customize Form", "Customize Form", function () { | |||
var doc = frappe.get_doc("Customize Form"); | |||
doc.doc_type = doctype; | |||
@@ -1023,7 +843,7 @@ frappe.provide("frappe.views"); | |||
args: { | |||
doc: doc | |||
}, | |||
callback: function (r) { | |||
callback: function () { | |||
frappe.model.clear_doc(doc.doctype, doc.name); | |||
frappe.show_alert({ message: __("Saved"), indicator: 'green' }, 1); | |||
} | |||
@@ -1049,24 +869,27 @@ frappe.provide("frappe.views"); | |||
} | |||
function is_filters_modified(board, cur_list) { | |||
return new Promise(function(resolve, reject) { | |||
return new Promise(function(resolve) { | |||
setTimeout(function() { | |||
// sometimes the filter_list is not initiated, so early return | |||
if(!cur_list.filter_list) resolve(false); | |||
try { | |||
var list_filters = JSON.stringify(cur_list.filter_area.get()); | |||
resolve(list_filters !== board.filters); | |||
} catch(e) { | |||
// sometimes the filter_list is not initiated | |||
resolve(false); | |||
} | |||
var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); | |||
resolve(list_filters !== board.filters); | |||
}, 2000); | |||
}) | |||
}); | |||
} | |||
function is_active_column(col) { | |||
return col.status !== 'Archived' | |||
return col.status !== 'Archived'; | |||
} | |||
function get_cards_for_column(cards, column) { | |||
return cards.filter(function (card) { | |||
return card.column === column.title | |||
return card.column === column.title; | |||
}); | |||
} | |||
@@ -1099,7 +922,7 @@ frappe.provide("frappe.views"); | |||
}); | |||
if(!indicators) { | |||
// | |||
indicators = ['green', 'blue', 'orange', 'grey'] | |||
indicators = ['green', 'blue', 'orange', 'grey']; | |||
} | |||
callback(indicators); | |||
}); | |||
@@ -1118,7 +941,7 @@ frappe.provide("frappe.views"); | |||
} | |||
function remove_img_tags(html) { | |||
const $temp = $(`<div>${html}</div>`) | |||
const $temp = $(`<div>${html}</div>`); | |||
$temp.find('img').remove(); | |||
return $temp.html(); | |||
} | |||
@@ -1,77 +1,63 @@ | |||
frappe.provide('frappe.views'); | |||
frappe.views.KanbanView = frappe.views.ListRenderer.extend({ | |||
name: 'Kanban', | |||
render_view: function(values) { | |||
var board_name = this.get_board_name(); | |||
frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
static load_last_view() { | |||
const route = frappe.get_route(); | |||
if (route.length === 3) { | |||
const doctype = route[1]; | |||
const user_settings = frappe.get_user_settings(doctype)['Kanban'] || {}; | |||
if (!user_settings.last_kanban_board) { | |||
frappe.msgprint({ | |||
title: __('Error'), | |||
indicator: 'red', | |||
message: __('Missing parameter Kanban Board Name') | |||
}); | |||
frappe.set_route('List', doctype, 'List'); | |||
return true; | |||
} | |||
route.push(user_settings.last_kanban_board); | |||
frappe.set_route(route); | |||
return true; | |||
} | |||
return false; | |||
} | |||
setup_defaults() { | |||
super.setup_defaults(); | |||
this.board_name = frappe.get_route()[3]; | |||
this.page_title = this.board_name; | |||
} | |||
show() { | |||
super.show(); | |||
this.save_view_user_settings({ | |||
last_kanban_board: this.board_name | |||
}); | |||
} | |||
render() { | |||
const board_name = this.board_name; | |||
if(this.kanban && board_name === this.kanban.board_name) { | |||
this.kanban.update(values); | |||
this.kanban.update(this.data); | |||
this.kanban.$kanban_board.trigger('after-refresh'); | |||
return; | |||
} | |||
this.kanban = new frappe.views.KanbanBoard({ | |||
doctype: this.doctype, | |||
board_name: board_name, | |||
cards: values, | |||
wrapper: this.wrapper, | |||
cur_list: this.list_view, | |||
user_settings: this.user_settings | |||
cards: this.data, | |||
wrapper: this.$result, | |||
cur_list: this, | |||
user_settings: this.view_user_settings | |||
}); | |||
}, | |||
after_refresh: function() { | |||
this.wrapper.find('.kanban').trigger('after-refresh'); | |||
frappe.kanban_filters[this.get_board_name()] = | |||
this.list_view.filter_list.get_filters(); | |||
}, | |||
should_refresh: function() { | |||
var to_refresh = this._super(); | |||
if(!to_refresh) { | |||
this.last_kanban_board = this.current_kanban_board || ''; | |||
this.current_kanban_board = this.get_board_name(); | |||
this.page_title = __(this.get_board_name()); | |||
this.kanban.$kanban_board.trigger('after-refresh'); | |||
} | |||
to_refresh = this.current_kanban_board !== this.last_kanban_board; | |||
} | |||
return to_refresh; | |||
}, | |||
init_settings: function() { | |||
this._super(); | |||
this.filters = this.get_kanban_filters(); | |||
}, | |||
get_kanban_filters: function() { | |||
frappe.provide('frappe.kanban_filters'); | |||
var board_name = this.get_board_name(); | |||
if (!frappe.kanban_filters[board_name]) { | |||
var kb = this.meta.__kanban_boards.find( | |||
board => board.name === board_name | |||
); | |||
frappe.kanban_filters[board_name] = JSON.parse(kb && kb.filters || '[]'); | |||
} | |||
if(typeof frappe.kanban_filters[board_name] === 'string') { | |||
frappe.kanban_filters[board_name] = | |||
JSON.parse( | |||
frappe.kanban_filters[board_name] || '[]' | |||
) | |||
} | |||
var filters = frappe.kanban_filters[board_name]; | |||
return filters; | |||
}, | |||
set_defaults: function() { | |||
this._super(); | |||
this.no_realtime = true; | |||
this.show_no_result = false; | |||
this.page_title = __(this.get_board_name()); | |||
}, | |||
get_board_name: function() { | |||
var route = frappe.get_route(); | |||
return route[3]; | |||
}, | |||
get_header_html: function() { | |||
return frappe.render_template('list_item_row_head', { main: '', list: this }); | |||
}, | |||
required_libs: [ | |||
'assets/frappe/js/lib/fluxify.min.js', | |||
'assets/frappe/js/frappe/views/kanban/kanban_board.js' | |||
] | |||
}); | |||
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 %} | |||
{% if col.name && col._id !== "_check" %} | |||
<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" | |||
{% endif %}>{{ __(col.name) }}</th> | |||
{% endif %} | |||
@@ -24,11 +24,13 @@ | |||
{% for col in columns %} | |||
{% if col.name && col._id !== "_check" %} | |||
{% var value = col.fieldname ? row[col.fieldname] : row[col.field]; %} | |||
{% var value = col.fieldname ? row[col.fieldname] : row[col.field]; %} | |||
<td>{{ col.formatter | |||
<td> | |||
{{ col.formatter | |||
? col.formatter(row._index, col._index, value, col, row, true) | |||
: value }}</td> | |||
: (col.docfield ? frappe.format(value, col.docfield) : value) }} | |||
</td> | |||
{% endif %} | |||
{% endfor %} | |||
</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; | |||
frappe.breadcrumbs.add(module, this.doctype); | |||
this.parent.reportview = new frappe.views.ReportView({ | |||
this.parent.reportview = new frappe.views.ReportView2({ | |||
doctype: this.doctype, | |||
docname: this.docname, | |||
parent: this.parent | |||
}); | |||
} | |||
}); | |||
frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||
init: function(opts) { | |||
var me = this; | |||
$.extend(this, opts); | |||
this.can_delete = frappe.model.can_delete(me.doctype); | |||
this.tab_name = '`tab'+this.doctype+'`'; | |||
this.setup(); | |||
}, | |||
setup: function() { | |||
var me = this; | |||
this.add_totals_row = 0; | |||
this.page = this.parent.page; | |||
this.meta = frappe.get_meta(this.doctype); | |||
this._body = $('<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) { | |||
opts.data = opts.grid.getData().getItems(); | |||
} | |||
} else { | |||
opts.columns = []; | |||
} | |||
// show landscape view if columns more than 10 | |||
@@ -237,6 +237,17 @@ a.no-decoration& { | |||
margin: 15px; | |||
} | |||
.margin-(@position) { | |||
.margin-@{position} { | |||
margin-@{position}: 15px; | |||
} | |||
} | |||
.margin-(top); | |||
.margin-(bottom); | |||
.margin-(left); | |||
.margin-(right); | |||
@media (max-width: 767px) { | |||
.text-center-xs { | |||
text-align: center; | |||
@@ -3,6 +3,14 @@ | |||
margin-top: 5px; | |||
margin-bottom: 5px; | |||
label { | |||
position: relative; | |||
} | |||
input[type=checkbox] { | |||
margin-left: 0; | |||
} | |||
& + .checkbox { | |||
margin-top: 5px; | |||
margin-bottom: 5px; | |||
@@ -304,6 +304,10 @@ textarea.form-control { | |||
} | |||
} | |||
.input-area { | |||
position: relative; | |||
} | |||
.link-field.ui-front { | |||
z-index: inherit; | |||
} | |||
@@ -434,11 +438,6 @@ li.user-progress { | |||
padding: 15px 30px; | |||
} | |||
.footnote-area { | |||
padding: 0px 15px; | |||
border-top: 1px solid @border-color; | |||
} | |||
.file-upload { | |||
.input-group-addon { | |||
color: @text-muted; | |||
@@ -897,7 +896,7 @@ li.user-progress { | |||
// custom font awesome checkbox | |||
input[type="checkbox"] { | |||
position: relative; | |||
height: 16px; | |||
left: -999999px; | |||
&:before { | |||
position: absolute; | |||
@@ -913,9 +912,7 @@ input[type="checkbox"] { | |||
font-size: 14px; | |||
color: @text-extra-muted; | |||
.transition(150ms color); | |||
background-color: white; | |||
padding: 1px; | |||
margin: -1px; | |||
left: 999999px; | |||
} | |||
&: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"; | |||
.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 { | |||
@@ -15,7 +37,7 @@ | |||
} | |||
} | |||
.list-filters { | |||
.filter-list{ | |||
position: relative; | |||
.sort-selector { | |||
@@ -25,12 +47,12 @@ | |||
} | |||
} | |||
.show_filters { | |||
.tag-filters-area { | |||
padding: 15px 15px 0px; | |||
border-bottom: 1px solid @border-color; | |||
} | |||
.set-filters { | |||
.active-tag-filters { | |||
padding-bottom: 4px; | |||
padding-right: 120px; | |||
@@ -39,17 +61,17 @@ | |||
} | |||
} | |||
.set-filters .btn { | |||
.active-tag-filters .btn { | |||
margin-bottom: 10px; | |||
} | |||
.set-filters .btn-group { | |||
margin-right: 10px; | |||
.active-tag-filters .btn-group { | |||
margin-left: 10px; | |||
white-space: nowrap; | |||
font-size: 0; | |||
} | |||
.set-filters .btn-group .btn-default { | |||
.active-tag-filters .btn-group .btn-default { | |||
background-color: transparent; | |||
border: 1px solid @border-color; | |||
color: @text-muted; | |||
@@ -58,7 +80,6 @@ | |||
.filter-box { | |||
border-bottom: 1px solid @border-color; | |||
// margin: 0px -15px; | |||
padding: 10px 15px 3px; | |||
.remove-filter { | |||
@@ -66,7 +87,7 @@ | |||
margin-left: 15px; | |||
} | |||
.filter_field { | |||
.filter-field { | |||
padding-right: 15px; | |||
width: calc(100% - 36px); | |||
@@ -77,12 +98,12 @@ | |||
} | |||
// for sm and above | |||
@media (min-width: 768px) { | |||
@media (min-width: @screen-xs) { | |||
.filter-box .row > div[class*="col-sm-"] { | |||
padding-right: 0px; | |||
} | |||
.filter_field { | |||
.filter-field { | |||
width: 65% !important; | |||
.frappe-control { | |||
@@ -91,144 +112,108 @@ | |||
} | |||
} | |||
.list-row { | |||
padding: 9px 15px; | |||
.list-row-container { | |||
border-bottom: 1px solid @border-color; | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
.list-row { | |||
padding: 12px 15px; | |||
height: 40px; | |||
cursor: pointer; | |||
transition: color 0.2s; | |||
-webkit-transition: color 0.2s; | |||
} | |||
.list-row .h6 { | |||
margin-top: 0px; | |||
margin-bottom: 0px; | |||
&:hover { | |||
background-color: @panel-bg; | |||
} | |||
&:last-child { | |||
border-bottom: 0px; | |||
} | |||
.level-left { | |||
flex: 3; | |||
} | |||
.level-right { | |||
flex: 1; | |||
} | |||
} | |||
.list-row-head { | |||
background-color: @panel-bg; | |||
border-bottom: 1px solid @border-color !important; | |||
} | |||
.list-row:hover, .grid-row:hover { | |||
background-color: @panel-bg; | |||
} | |||
.no-hover:hover { | |||
background-color: transparent !important; | |||
} | |||
.list-subject { | |||
font-weight: normal; | |||
} | |||
.list-row:last-child { | |||
border-bottom: 0px; | |||
.checkbox-actions { | |||
display: none; | |||
} | |||
} | |||
.list-row-head { | |||
background-color: @panel-bg; | |||
border-bottom: 1px solid @border-color !important; | |||
.list-row-col { | |||
flex: 1; | |||
margin-right: 15px; | |||
} | |||
.list-row .h6 { | |||
margin-top: 0px; | |||
margin-bottom: 0px; | |||
} | |||
.list-subject { | |||
flex: 2; | |||
font-weight: bold; | |||
justify-content: start; | |||
.list-item-col { | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
height: 30px; | |||
padding-top: 3px; | |||
} | |||
.level-item { | |||
margin-right: 8px; | |||
} | |||
.list-paging-area { | |||
padding: 10px 15px; | |||
border-top: 1px solid @border-color; | |||
&.seen { | |||
font-weight: normal; | |||
} | |||
} | |||
.list-value { | |||
display: table; | |||
vertical-align: middle; | |||
.list-row-activity { | |||
justify-content: flex-end; | |||
min-width: 120px; | |||
.list-row-checkbox, .liked-by, .list-id, .list-select-all { | |||
display: table-cell; | |||
vertical-align: middle; | |||
} | |||
.list-row-checkbox, .list-select-all { | |||
.avatar:not(.avatar-empty) { | |||
margin: 0; | |||
margin-right: 7px; | |||
} | |||
.liked-by { | |||
padding-top: 2px; | |||
&> span { | |||
display: inline-block; | |||
margin-left: 10px; | |||
margin-right: 0; | |||
} | |||
} | |||
.list-col-title { | |||
vertical-align: middle; | |||
} | |||
.list-paging-area, .footnote-area { | |||
padding: 10px 15px; | |||
border-top: 1px solid @border-color; | |||
overflow: auto; | |||
} | |||
.progress { | |||
height: 10px; | |||
} | |||
.doclist-row { | |||
font-size: 12px; | |||
} | |||
.likes-count { | |||
display: inline-block; | |||
width: 15px; | |||
margin-left: -5px; | |||
color: @text-muted; | |||
font-size: 12px; | |||
display: none; | |||
} | |||
.doclist-row .docstatus .octicon { | |||
font-size: 12px; | |||
.list-liked-by-me { | |||
margin-bottom: 1px; | |||
} | |||
.doclist-row .progress { | |||
margin-top: 12px; | |||
input.list-check-all, input.list-row-checkbox { | |||
margin-top: 0px; | |||
} | |||
.filterable { | |||
cursor: pointer; | |||
} | |||
.doclist-row .label { | |||
margin-right: 8px; | |||
} | |||
.list-info-row { | |||
float: left; | |||
margin-top: 1px; | |||
} | |||
.list-row-right .modified { | |||
margin-top: 3px; | |||
} | |||
.list-row-right .list-row-modified { | |||
margin-right: 9px; | |||
margin-top: 3px; | |||
} | |||
.list-row-right { | |||
margin-top: -2px; | |||
margin-bottom: -4px; | |||
} | |||
.list-row-right .indicator { | |||
margin-left: 10px; | |||
margin-right: -5px; | |||
} | |||
.side-panel { | |||
border-bottom: 1px solid @border-color; | |||
margin: 0px -15px; | |||
padding: 5px 15px; | |||
} | |||
.listview-main-section { | |||
.octicon-heart { | |||
cursor: pointer; | |||
@@ -256,36 +241,6 @@ | |||
color: @heart-color; | |||
} | |||
.list-id { | |||
font-weight: bold; | |||
} | |||
.list-id.seen { | |||
font-weight: normal; | |||
} | |||
.list-col { | |||
height: 20px; | |||
} | |||
.list-value { | |||
vertical-align: middle; | |||
} | |||
@media(max-width: 767px) { | |||
.doclist-row { | |||
font-size: @text-regular; | |||
} | |||
.doclist-row [type='checkbox'] { | |||
display: none; | |||
} | |||
.doclist-row .list-row-right .list-row-modified { | |||
display: none; | |||
} | |||
} | |||
.list-comment-count { | |||
display: inline-block; | |||
width: 37px; | |||
@@ -294,10 +249,15 @@ | |||
// tags | |||
.result.tags-shown { | |||
.tag-row { | |||
display: block; | |||
} | |||
} | |||
.tag-row { | |||
padding-left: 55px; | |||
margin-bottom: 0px; | |||
margin-top: -5px; | |||
display: none; | |||
margin-left: 50px; | |||
} | |||
.taggle_placeholder { | |||
@@ -374,6 +334,7 @@ | |||
padding: 15px; | |||
border-bottom: 1px solid @light-border-color; | |||
border-right: 1px solid @light-border-color; | |||
max-width: 100%/4; | |||
} | |||
.image-view-item:nth-child(4n) { | |||
@@ -410,6 +371,10 @@ | |||
img { | |||
max-height: 100%; | |||
} | |||
&.no-image { | |||
background-color: @light-bg; | |||
} | |||
} | |||
.placeholder-text { | |||
@@ -457,6 +422,7 @@ | |||
.image-view-container.three-column { | |||
.image-view-item { | |||
flex: 0 0 100%/3; | |||
max-width: 100%/3; | |||
} | |||
.image-view-item:nth-child(3n) { | |||
@@ -510,6 +476,11 @@ | |||
} | |||
// gantt | |||
.list-paging-area .gantt-view-mode { | |||
margin-left: 15px; | |||
margin-right: 15px; | |||
} | |||
.gantt { | |||
.details-container { | |||
.heading { | |||
@@ -67,3 +67,16 @@ | |||
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'} | |||
]), | |||
() => frappe.timeout(1), | |||
() => frappe.tests.make("Event", [ | |||
{subject: random_text + ':Pub'}, | |||
{starts_on: today}, | |||
{event_type: 'Public'} | |||
]), | |||
() => frappe.timeout(1), | |||
// Goto Calendar view | |||
() => frappe.set_route(["List", "Event", "Calendar"]), | |||
() => { | |||
// clear filter | |||
$('[data-fieldname="event_type"]').val('').trigger('change'); | |||
}, | |||
// clear filter | |||
() => cur_list.filter_area.remove('event_type'), | |||
() => frappe.timeout(2), | |||
// Check if event is created | |||
() => { | |||
// Check if the event exists and if its title matches with the one created | |||
assert.ok(event_title_text().includes(random_text + ':Pri'), | |||
"Event title verified"); | |||
// Check if time of event created is correct | |||
// assert.ok(visible_time().includes("4:20"), | |||
// "Event start time verified"); | |||
}, | |||
// check filter | |||
() => { | |||
$('[data-fieldname="event_type"]').val('Public').trigger('change'); | |||
}, | |||
() => cur_list.filter_area.add('Event', 'event_type', '=', 'Public'), | |||
() => frappe.timeout(1), | |||
() => { | |||
// private event should be hidden | |||
@@ -4,17 +4,21 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { | |||
assert.expect(2); | |||
let done = assert.async(); | |||
const board_name = 'Kanban test'; | |||
frappe.run_serially([ | |||
() => frappe.set_route("List", "ToDo", "List"), | |||
// wait for cur_list to initialize | |||
() => cur_list.init(), | |||
// click kanban in side bar | |||
() => frappe.click_link('Kanban'), | |||
() => frappe.click_link('New Kanban Board'), | |||
() => frappe.tests.click_link('Kanban'), | |||
() => frappe.tests.click_link('New Kanban Board'), | |||
() => frappe.timeout(0.5), | |||
// create new kanban | |||
() => { | |||
assert.equal(cur_dialog.title, 'New Kanban Board', | |||
"Dialog for new kanban opened."); | |||
cur_dialog.set_value('board_name', 'Kanban test'); | |||
cur_dialog.set_value('board_name', board_name); | |||
cur_dialog.set_value('field_name', 'Priority'); | |||
}, | |||
() => frappe.timeout(0.5), | |||
@@ -23,7 +27,7 @@ QUnit.test("Test: Creation [Kanban view]", function(assert) { | |||
() => frappe.set_route("List", "Kanban Board", "List"), | |||
() => frappe.timeout(0.5), | |||
// check in kanban list if new kanban is created | |||
() => assert.equal(cur_list.data[0].name, 'Kanban test', | |||
() => assert.equal(cur_list.data[0].name, board_name, | |||
"Added kanban is visible in kanban list."), | |||
() => done() | |||
]); |
@@ -10,9 +10,9 @@ QUnit.test("Test: Filters [Kanban view]", function(assert) { | |||
() => { | |||
assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), | |||
"Kanban view opened successfully."); | |||
// set filter values | |||
return frappe.set_control('priority', 'Low'); | |||
}, | |||
// set filter values | |||
() => cur_list.filter_area.add('ToDo', 'priority', '=', 'Low'), | |||
() => frappe.timeout(1), | |||
() => cur_list.page.btn_secondary.click(), | |||
() => frappe.timeout(1), | |||
@@ -1,25 +1,28 @@ | |||
QUnit.module('views'); | |||
QUnit.test("Test: Kanban view", function(assert) { | |||
assert.expect(3); | |||
assert.expect(4); | |||
let done = assert.async(); | |||
let total_elements; | |||
frappe.run_serially([ | |||
() => frappe.set_route("List", "ToDo", "List"), | |||
// calculate number of element in list | |||
() => frappe.timeout(1), | |||
() => total_elements = cur_list.data.length, | |||
() => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), | |||
() => frappe.timeout(1), | |||
() => frappe.timeout(2), | |||
() => { | |||
assert.equal('Kanban', cur_list.current_view, | |||
assert.equal('Kanban', cur_list.view_name, | |||
"Current view is kanban."); | |||
assert.equal("Kanban test", cur_list.list_renderer.page_title, | |||
assert.equal("Kanban test", cur_list.page_title, | |||
"Kanban view opened successfully."); | |||
// check if all elements are visible in kanban view | |||
assert.equal(total_elements, cur_list.data.length, | |||
"All elements are visible in kanban view."); | |||
const $high_priority_cards = | |||
$('.kanban-column[data-column-value="High"] .kanban-card-wrapper'); | |||
const $low_priority_cards = | |||
$('.kanban-column[data-column-value="Low"] .kanban-card-wrapper'); | |||
assert.equal($high_priority_cards.length, 1); | |||
assert.equal($low_priority_cards.length, 1); | |||
}, | |||
() => done() | |||
]); |
@@ -14,10 +14,10 @@ QUnit.test("Test paging in list view", function(assert) { | |||
() => frappe.click_button('More'), | |||
() => frappe.timeout(2), | |||
() => assert.equal(cur_list.data.length, 40, 'show more items'), | |||
() => frappe.click_button('100', '.btn-group-paging'), | |||
() => frappe.tests.click_button('100'), | |||
() => frappe.timeout(2), | |||
() => assert.ok(cur_list.data.length > 40, 'show 100 items'), | |||
() => frappe.click_button('20', '.btn-group-paging'), | |||
() => frappe.tests.click_button('20'), | |||
() => frappe.timeout(2), | |||
() => assert.equal(cur_list.data.length, 20, 'show 20 items again'), | |||
() => done() | |||
@@ -8,24 +8,23 @@ QUnit.test("Test List Count", function(assert) { | |||
() => frappe.set_route('List', 'DocType'), | |||
() => frappe.timeout(0.5), | |||
() => { | |||
let count = $('.list-row-right').text().split(' ')[0]; | |||
let count = $('.list-count').text().split(' ')[0]; | |||
assert.equal(cur_list.data.length, count, "Correct Count"); | |||
}, | |||
() => frappe.timeout(1), | |||
() => cur_list.filter_list.add_filter('Doctype', 'module', '=', 'Desk'), | |||
() => cur_list.filter_area.add('Doctype', 'module', '=', 'Desk'), | |||
() => frappe.click_button('Refresh'), | |||
() => { | |||
let count = $('.list-row-right').text().split(' ')[0]; | |||
let count = $('.list-count').text().split(' ')[0]; | |||
assert.equal(cur_list.data.length, count, "Correct Count"); | |||
}, | |||
() => cur_list.filter_list.clear_filters(), | |||
() => cur_list.filter_area.clear(), | |||
() => frappe.timeout(1), | |||
() => { | |||
cur_list.filter_list.push_new_filter('DocField', 'fieldname', 'like', 'owner'); | |||
frappe.click_button('Apply'); | |||
let count = $('.list-row-right').text().split(' ')[0]; | |||
cur_list.filter_area.add('DocField', 'fieldname', 'like', 'owner'); | |||
let count = $('.list-count').text().split(' ')[0]; | |||
assert.equal(cur_list.data.length, count, "Correct Count"); | |||
}, | |||