Parcourir la source

New ListView 💥 + DataTable (#4577)

* 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 View
version-14
Faris Ansari il y a 7 ans
committed by GitHub
Parent
révision
7595fb75ba
Aucune clé connue n'a été trouvée dans la base pour cette signature ID de la clé GPG: 4AEE18F83AFDEB23
80 fichiers modifiés avec 4765 ajouts et 4418 suppressions
  1. +2
    -1
      .eslintrc
  2. +2
    -2
      frappe/core/doctype/file/file.json
  3. +0
    -239
      frappe/core/doctype/file/file_list.js
  4. +23
    -0
      frappe/core/doctype/file/test_file.js
  5. +1
    -1
      frappe/core/doctype/report/report.js
  6. +1
    -0
      frappe/core/page/desktop/desktop.js
  7. +1
    -1
      frappe/desk/doctype/event/test_event.js
  8. +1
    -1
      frappe/desk/doctype/kanban_board/kanban_board.js
  9. +1
    -0
      frappe/desk/page/activity/activity.css
  10. +49
    -36
      frappe/desk/page/activity/activity.js
  11. +5
    -1
      frappe/desk/reportview.py
  12. +6
    -1
      frappe/model/document.py
  13. +12
    -8
      frappe/public/build.json
  14. +12
    -0
      frappe/public/css/common.css
  15. +6
    -0
      frappe/public/css/controls.css
  16. +17
    -8
      frappe/public/css/desk.css
  17. +41
    -0
      frappe/public/css/flex.css
  18. +58
    -0
      frappe/public/css/frappe-datatable.css
  19. +104
    -126
      frappe/public/css/list.css
  20. +33
    -0
      frappe/public/css/regrid.css
  21. +6
    -0
      frappe/public/css/report.css
  22. +12
    -0
      frappe/public/css/website.css
  23. +10
    -1
      frappe/public/js/frappe/db.js
  24. +8
    -4
      frappe/public/js/frappe/form/controls/autocomplete.js
  25. +0
    -1
      frappe/public/js/frappe/form/controls/color.js
  26. +2
    -2
      frappe/public/js/frappe/form/footer/assign_to.js
  27. +8
    -0
      frappe/public/js/frappe/form/formatters.js
  28. +28
    -39
      frappe/public/js/frappe/form/linked_with.js
  29. +638
    -0
      frappe/public/js/frappe/list/base_list.js
  30. +95
    -0
      frappe/public/js/frappe/list/list_factory.js
  31. +3
    -3
      frappe/public/js/frappe/list/list_permission_footer.html
  32. +1
    -1
      frappe/public/js/frappe/list/list_sidebar.html
  33. +28
    -28
      frappe/public/js/frappe/list/list_sidebar.js
  34. +867
    -775
      frappe/public/js/frappe/list/list_view.js
  35. +56
    -0
      frappe/public/js/frappe/misc/file_manager.js
  36. +42
    -4
      frappe/public/js/frappe/misc/utils.js
  37. +22
    -2
      frappe/public/js/frappe/model/model.js
  38. +1
    -0
      frappe/public/js/frappe/model/user_settings.js
  39. +0
    -532
      frappe/public/js/frappe/ui/base_list.js
  40. +2
    -2
      frappe/public/js/frappe/ui/filters/edit_filter.html
  41. +155
    -0
      frappe/public/js/frappe/ui/filters/field_select.js
  42. +335
    -0
      frappe/public/js/frappe/ui/filters/filter.js
  43. +141
    -0
      frappe/public/js/frappe/ui/filters/filter_list.js
  44. +0
    -679
      frappe/public/js/frappe/ui/filters/filters.js
  45. +4
    -1
      frappe/public/js/frappe/ui/like.js
  46. +4
    -0
      frappe/public/js/frappe/ui/sort_selector.js
  47. +1
    -1
      frappe/public/js/frappe/upload.js
  48. +21
    -7
      frappe/public/js/frappe/views/breadcrumbs.js
  49. +62
    -80
      frappe/public/js/frappe/views/calendar/calendar.js
  50. +1
    -2
      frappe/public/js/frappe/views/communication.js
  51. +1
    -4
      frappe/public/js/frappe/views/factory.js
  52. +266
    -0
      frappe/public/js/frappe/views/file/file_view.js
  53. +131
    -167
      frappe/public/js/frappe/views/gantt/gantt_view.js
  54. +129
    -74
      frappe/public/js/frappe/views/image/image_view.js
  55. +0
    -9
      frappe/public/js/frappe/views/image/image_view_item_row.html
  56. +0
    -12
      frappe/public/js/frappe/views/inbox/inbox_no_result.html
  57. +153
    -117
      frappe/public/js/frappe/views/inbox/inbox_view.js
  58. +0
    -24
      frappe/public/js/frappe/views/inbox/inbox_view_item_main_head.html
  59. +0
    -49
      frappe/public/js/frappe/views/inbox/inbox_view_item_row.html
  60. +57
    -234
      frappe/public/js/frappe/views/kanban/kanban_board.js
  61. +52
    -66
      frappe/public/js/frappe/views/kanban/kanban_view.js
  62. +6
    -4
      frappe/public/js/frappe/views/reports/print_grid.html
  63. +716
    -0
      frappe/public/js/frappe/views/reports/report_view.js
  64. +1
    -880
      frappe/public/js/frappe/views/reports/reportview.js
  65. +16
    -0
      frappe/public/js/lib/clusterize.min.js
  66. +1
    -0
      frappe/public/js/lib/frappe-datatable.js
  67. +0
    -2
      frappe/public/js/lib/microtemplate.js
  68. +11
    -0
      frappe/public/less/common.less
  69. +8
    -0
      frappe/public/less/controls.less
  70. +6
    -9
      frappe/public/less/desk.less
  71. +47
    -0
      frappe/public/less/flex.less
  72. +71
    -0
      frappe/public/less/frappe-datatable.less
  73. +115
    -144
      frappe/public/less/list.less
  74. +13
    -0
      frappe/public/less/report.less
  75. +8
    -11
      frappe/tests/ui/test_calendar_view.js
  76. +8
    -4
      frappe/tests/ui/test_kanban/test_kanban_creation.js
  77. +2
    -2
      frappe/tests/ui/test_kanban/test_kanban_filters.js
  78. +11
    -8
      frappe/tests/ui/test_kanban/test_kanban_view.js
  79. +2
    -2
      frappe/tests/ui/test_list/test_list_paging.js
  80. +6
    -7
      frappe/tests/ui/test_list_count.js

+ 2
- 1
.eslintrc Voir le fichier

@@ -120,6 +120,7 @@
"QUnit": true,
"JsBarcode": true,
"L": true,
"Chart": true
"Chart": true,
"DataTable": true
}
}

+ 2
- 2
frappe/core/doctype/file/file.json Voir le fichier

@@ -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",


+ 0
- 239
frappe/core/doctype/file/file_list.js Voir le fichier

@@ -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);
}
});
}
};

+ 23
- 0
frappe/core/doctype/file/test_file.js Voir le fichier

@@ -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()
]);

});

+ 1
- 1
frappe/core/doctype/report/report.js Voir le fichier

@@ -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);


+ 1
- 0
frappe/core/page/desktop/desktop.js Voir le fichier

@@ -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) {


+ 1
- 1
frappe/desk/doctype/event/test_event.js Voir le fichier

@@ -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');
},


+ 1
- 1
frappe/desk/doctype/kanban_board/kanban_board.js Voir le fichier

@@ -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');


+ 1
- 0
frappe/desk/page/activity/activity.css Voir le fichier

@@ -6,6 +6,7 @@
#page-activity .list-row {
border: none;
padding: 0px;
height: auto;
cursor: pointer;
}



+ 49
- 36
frappe/desk/page/activity/activity.js Voir le fichier

@@ -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);
});
}
};

+ 5
- 1
frappe/desk/reportview.py Voir le fichier

@@ -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=[]):


+ 6
- 1
frappe/model/document.py Voir le fichier

@@ -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.


+ 12
- 8
frappe/public/build.json Voir le fichier

@@ -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",


+ 12
- 0
frappe/public/css/common.css Voir le fichier

@@ -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;


+ 6
- 0
frappe/public/css/controls.css Voir le fichier

@@ -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;


+ 17
- 8
frappe/public/css/desk.css Voir le fichier

@@ -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;


+ 41
- 0
frappe/public/css/flex.css Voir le fichier

@@ -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;
}

+ 58
- 0
frappe/public/css/frappe-datatable.css Voir le fichier

@@ -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;
}
}

+ 104
- 126
frappe/public/css/list.css Voir le fichier

@@ -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;


+ 33
- 0
frappe/public/css/regrid.css Voir le fichier

@@ -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;
}

+ 6
- 0
frappe/public/css/report.css Voir le fichier

@@ -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;
}

+ 12
- 0
frappe/public/css/website.css Voir le fichier

@@ -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;


+ 10
- 1
frappe/public/js/frappe/db.js Voir le fichier

@@ -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
- 4
frappe/public/js/frappe/form/controls/autocomplete.js Voir le fichier

@@ -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%');


+ 0
- 1
frappe/public/js/frappe/form/controls/color.js Voir le fichier

@@ -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;
}
});

+ 2
- 2
frappe/public/js/frappe/form/footer/assign_to.js Voir le fichier

@@ -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,


+ 8
- 0
frappe/public/js/frappe/form/formatters.js Voir le fichier

@@ -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;
}
}



+ 28
- 39
frappe/public/js/frappe/form/linked_with.js Voir le fichier

@@ -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>`;
}
}
};

+ 638
- 0
frappe/public/js/frappe/list/base_list.js Voir le fichier

@@ -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);

+ 95
- 0
frappe/public/js/frappe/list/list_factory.js Voir le fichier

@@ -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;
}
}
});

+ 3
- 3
frappe/public/js/frappe/list/list_permission_footer.html Voir le fichier

@@ -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;">


+ 1
- 1
frappe/public/js/frappe/list/list_sidebar.html Voir le fichier

@@ -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>


+ 28
- 28
frappe/public/js/frappe/list/list_sidebar.js Voir le fichier

@@ -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';


+ 867
- 775
frappe/public/js/frappe/list/list_view.js
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 56
- 0
frappe/public/js/frappe/misc/file_manager.js Voir le fichier

@@ -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;
}
};
}();

+ 42
- 4
frappe/public/js/frappe/misc/utils.js Voir le fichier

@@ -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


+ 22
- 2
frappe/public/js/frappe/model/model.js Voir le fichier

@@ -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


+ 1
- 0
frappe/public/js/frappe/model/user_settings.js Voir le fichier

@@ -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;


+ 0
- 532
frappe/public/js/frappe/ui/base_list.js Voir le fichier

@@ -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);
}
}
});
}
});

+ 2
- 2
frappe/public/js/frappe/ui/filters/edit_filter.html Voir le fichier

@@ -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>


+ 155
- 0
frappe/public/js/frappe/ui/filters/field_select.js Voir le fichier

@@ -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;
}
},
});

+ 335
- 0
frappe/public/js/frappe/ui/filters/filter.js Voir le fichier

@@ -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';
}
}
};

+ 141
- 0
frappe/public/js/frappe/ui/filters/filter_list.js Voir le fichier

@@ -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>`);
}
};

+ 0
- 679
frappe/public/js/frappe/ui/filters/filters.js Voir le fichier

@@ -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;
}
},
})

+ 4
- 1
frappe/public/js/frappe/ui/like.js Voir le fichier

@@ -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) {


+ 4
- 0
frappe/public/js/frappe/ui/sort_selector.js Voir le fichier

@@ -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;
}
})

+ 1
- 1
frappe/public/js/frappe/upload.js Voir le fichier

@@ -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++) {


+ 21
- 7
frappe/public/js/frappe/views/breadcrumbs.js Voir le fichier

@@ -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);



+ 62
- 80
frappe/public/js/frappe/views/calendar/calendar.js Voir le fichier

@@ -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;
}
}
})
});

+ 1
- 2
frappe/public/js/frappe/views/communication.js Voir le fichier

@@ -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");
});



+ 1
- 4
frappe/public/js/frappe/views/factory.js Voir le fichier

@@ -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();
}


+ 266
- 0
frappe/public/js/frappe/views/file/file_view.js Voir le fichier

@@ -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');
}
}
};

+ 131
- 167
frappe/public/js/frappe/views/gantt/gantt_view.js Voir le fichier

@@ -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"
];
}
};

+ 129
- 74
frappe/public/js/frappe/views/image/image_view.js Voir le fichier

@@ -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);
});
}
});
});

+ 0
- 9
frappe/public/js/frappe/views/image/image_view_item_row.html Voir le fichier

@@ -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>

+ 0
- 12
frappe/public/js/frappe/views/inbox/inbox_no_result.html Voir le fichier

@@ -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>

+ 153
- 117
frappe/public/js/frappe/views/inbox/inbox_view.js Voir le fichier

@@ -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)
});
}
}
});
};

+ 0
- 24
frappe/public/js/frappe/views/inbox/inbox_view_item_main_head.html Voir le fichier

@@ -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>

+ 0
- 49
frappe/public/js/frappe/views/inbox/inbox_view_item_row.html Voir le fichier

@@ -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>

+ 57
- 234
frappe/public/js/frappe/views/kanban/kanban_board.js Voir le fichier

@@ -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();
}


+ 52
- 66
frappe/public/js/frappe/views/kanban/kanban_view.js Voir le fichier

@@ -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'
];
}
};

+ 6
- 4
frappe/public/js/frappe/views/reports/print_grid.html Voir le fichier

@@ -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>


+ 716
- 0
frappe/public/js/frappe/views/reports/report_view.js Voir le fichier

@@ -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 }));
}

};

+ 1
- 880
frappe/public/js/frappe/views/reports/reportview.js Voir le fichier

@@ -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">&times;</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();
}
});

+ 16
- 0
frappe/public/js/lib/clusterize.min.js Voir le fichier

@@ -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});

+ 1
- 0
frappe/public/js/lib/frappe-datatable.js
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 0
- 2
frappe/public/js/lib/microtemplate.js Voir le fichier

@@ -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


+ 11
- 0
frappe/public/less/common.less Voir le fichier

@@ -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;


+ 8
- 0
frappe/public/less/controls.less Voir le fichier

@@ -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;


+ 6
- 9
frappe/public/less/desk.less Voir le fichier

@@ -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 {


+ 47
- 0
frappe/public/less/flex.less Voir le fichier

@@ -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;
}

+ 71
- 0
frappe/public/less/frappe-datatable.less Voir le fichier

@@ -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;
}
}

+ 115
- 144
frappe/public/less/list.less Voir le fichier

@@ -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 {


+ 13
- 0
frappe/public/less/report.less Voir le fichier

@@ -67,3 +67,16 @@
margin-bottom: 2px;
}
}

// datatable
.data-table {
.edit-popup {
.frappe-control {
padding: 0;

.form-group {
margin: 0;
}
}
}
}

+ 8
- 11
frappe/tests/ui/test_calendar_view.js Voir le fichier

@@ -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


+ 8
- 4
frappe/tests/ui/test_kanban/test_kanban_creation.js Voir le fichier

@@ -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()
]);

+ 2
- 2
frappe/tests/ui/test_kanban/test_kanban_filters.js Voir le fichier

@@ -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),


+ 11
- 8
frappe/tests/ui/test_kanban/test_kanban_view.js Voir le fichier

@@ -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()
]);

+ 2
- 2
frappe/tests/ui/test_list/test_list_paging.js Voir le fichier

@@ -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()


+ 6
- 7
frappe/tests/ui/test_list_count.js Voir le fichier

@@ -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");
},



Chargement…
Annuler
Enregistrer