From 95a150b9656dac2f13035784880138f52f6cf7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 7 Sep 2019 23:56:25 +0200 Subject: [PATCH] Add drag and drop and manage sequence issue8240 review265181002 --- CHANGELOG | 2 + bower.json | 3 +- images/tryton-drag.svg | 1 + index.html | 1 + src/model.js | 66 +++++++++ src/sao.less | 8 +- src/view/form.js | 31 ++++- src/view/tree.js | 310 ++++++++++++++++++++++++++++++++++++----- 8 files changed, 384 insertions(+), 38 deletions(-) create mode 100644 images/tryton-drag.svg diff --git a/CHANGELOG b/CHANGELOG index fe36379cd0..0c92a4fd3b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Manage sequence +* Add drag and drop on tree views * Support expand on column * Set minimal width for columns * Order Dict keys by sequence diff --git a/bower.json b/bower.json index fdac090525..0153b30d99 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,8 @@ "papaparse": "^5.0", "fullcalendar": "^3.0", "mousetrap": "^1.6", - "bootstrap-rtl-ondemand": "^3.3.4-ondemand" + "bootstrap-rtl-ondemand": "^3.3.4-ondemand", + "Sortable": "sortablejs#^1.8.4" }, "devDependencies": { "qunit": "^1.18" diff --git a/images/tryton-drag.svg b/images/tryton-drag.svg new file mode 100644 index 0000000000..b8d36b16f0 --- /dev/null +++ b/images/tryton-drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index bfa49c56d4..8382b74916 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,7 @@ this repository contains the full copyright notices and license terms. --> + diff --git a/src/model.js b/src/model.js index ee8e38dc1d..abaeb52ca3 100644 --- a/src/model.js +++ b/src/model.js @@ -480,6 +480,45 @@ }; return jQuery.when().then(browse_child); }; + array.set_sequence = function(field) { + var changed = false; + var prev = null; + var record, index, update, value; + for (var i=0; i < this.length; i++) { + record = this[i]; + if (record.get_loaded([field]) || changed || record.id < 0) { + if (prev) { + index = prev.field_get(field); + } else { + index = null; + } + update = false; + value = record.field_get(field); + if (value === null) { + if (index) { + update = true; + } else if (prev && (record.id >= 0)) { + update = record.id < prev.id; + } + } else if (value === index) { + if (prev && (record.id >= 0)) { + update = record.id < prev.id; + } + } else if (value <= (index || 0)) { + update = true; + } + if (update) { + if (index === null) { + index = 0; + } + index += 1; + record.field_set_client(field, index); + changed = record; + } + } + prev = record; + } + }; return array; }; @@ -1256,6 +1295,33 @@ path.reverse(); return path; }, + children_group: function(field_name) { + var group_prm = jQuery.Deferred(); + if (!field_name) { + group_prm.resolve([]); + return group_prm; + } + var load_prm = this._check_load([field_name]); + load_prm.done(function() { + var group = this._values[field_name]; + if (group === undefined) { + group_prm.resolve(null); + return; + } + + if (group.model.fields !== this.group.model.fields) { + jQuery.extend(this.group.model.fields, group.model.fields); + group.model.fields = this.group.model.fields; + } + group.on_write = this.group.on_write; + group.readonly = this.group.readonly; + jQuery.extend(group._context, this.group._context); + + group_prm.resolve(group); + return; + }.bind(this)); + return group_prm; + }, get deleted() { return Boolean(~this.group.record_deleted.indexOf(this)); }, diff --git a/src/sao.less b/src/sao.less index 7904f26027..e0ad3dfbf9 100644 --- a/src/sao.less +++ b/src/sao.less @@ -213,10 +213,13 @@ img.icon { width: 100%; table-layout: fixed; - col.selection-state, col.favorite { + col.selection-state, col.favorite, col.draggable-handle { width: 30px; } + td.draggable-handle { + cursor: grab; + } th.selection-state, td.selection-state { > input { .center-block(); @@ -245,6 +248,9 @@ img.icon { vertical-align: middle; } } + > tbody > tr.dragged-row { + background-color: @state-info-bg; + } td.prefix { padding-right: @table-cell-padding; diff --git a/src/view/form.js b/src/view/form.js index 1610193677..df1e8605c0 100644 --- a/src/view/form.js +++ b/src/view/form.js @@ -2959,6 +2959,17 @@ function eval_pyson(value){ !this.position || !access.write || !access.read); } }, + _sequence: function() { + for (var i=0, len = this.screen.views.length; i < len; i++) { + var view = this.screen.views[i]; + if (view.view_type == 'tree') { + var sequence = view.attributes.sequence; + if (sequence) { + return sequence; + } + } + } + }, display: function() { Sao.View.Form.One2Many._super.display.call(this); @@ -3027,7 +3038,7 @@ function eval_pyson(value){ domain = ['OR', domain, ['id', 'in', removed_ids]]; var text = this.wid_text.val(); - // TODO sequence + var sequence = self._sequence(); var callback = function(result) { var prm = jQuery.when(); @@ -3039,6 +3050,9 @@ function eval_pyson(value){ } this.screen.group.load(ids, true); prm = this.screen.display(); + if (sequence) { + this.screen.group.set_sequence(sequence); + } } prm.done(function() { this.screen.set_cursor(); @@ -3083,17 +3097,22 @@ function eval_pyson(value){ new_single: function() { var context = jQuery.extend({}, this.field.get_context(this.record)); - // TODO sequence + var sequence = this._sequence(); + var update_sequence = function() { + if (sequence) { + this.screen.group.set_sequence(sequence); + } + }.bind(this); if (this.screen.current_view.type == 'form' || this.screen.current_view.editable) { - this.screen.new_(); + this.screen.new_().then(update_sequence); this.screen.current_view.el.prop('disabled', false); } else { var record = this.record; var field_size = record.expr_eval( this.attributes.size) || -1; field_size -= this.field.get_eval(record); - var win = new Sao.Window.Form(this.screen, function() {}, { + var win = new Sao.Window.Form(this.screen, update_sequence, { new_: true, many: field_size, context: context, @@ -3162,6 +3181,10 @@ function eval_pyson(value){ record.set_default(default_value); }); }); + var sequence = this._sequence(); + if (sequence) { + screen.group.set_sequence(sequence); + } }; search_set(); diff --git a/src/view/tree.js b/src/view/tree.js index 713e9dfd96..e5c12774dd 100644 --- a/src/view/tree.js +++ b/src/view/tree.js @@ -76,6 +76,7 @@ Sao.View.Tree = Sao.class_(Sao.View, { view_type: 'tree', xml_parser: Sao.View.TreeXMLViewParser, + draggable: false, init: function(view_id, screen, xml, children_field) { this.children_field = children_field; this.sum_widgets = {}; @@ -99,7 +100,7 @@ this.treeview.scrollLeft(this.scrollbar.parent().scrollLeft()); }.bind(this)); - this.expanded = {}; + this.expanded = new Set(); Sao.View.Tree._super.init.call(this, view_id, screen, xml); @@ -113,10 +114,10 @@ this.table.addClass('table-bordered'); } this.treeview.append(this.table); - var colgroup = jQuery('').appendTo(this.table); + this.colgroup = jQuery('').appendTo(this.table); var col = jQuery('', { 'class': 'selection-state', - }).appendTo(colgroup); + }).appendTo(this.colgroup); if (this.selection_mode == Sao.common.SELECTION_NONE) { col.css('width', 0); } @@ -146,7 +147,7 @@ this.columns.forEach(function(column) { col = jQuery('', { 'class': column.attributes.widget, - }).appendTo(colgroup); + }).appendTo(this.colgroup); th = jQuery('', { 'class': column.attributes.widget, }); @@ -197,6 +198,8 @@ this.tbody = jQuery(''); this.table.append(this.tbody); + this.set_drag_and_drop(); + this.display_size = Sao.config.display_size; }, get editable() { @@ -292,6 +295,195 @@ } }); }, + _add_drag_n_drop: function() { + Sortable.create(this.tbody[0], { + handle: '.draggable-handle', + ghostClass: 'dragged-row' + }); + this.tbody.on('dragstart', this.drag_data_get.bind(this)); + this.tbody.on('drop', this.drag_data_received.bind(this)); + }, + set_drag_and_drop: function() { + var dnd = false; + var children, parent_name; + if (this.children_field) { + children = this.screen.model.fields[this.children_field]; + if (children) { + parent_name = children.description.relation_field; + dnd = Boolean(this.widgets[parent_name]); + } + } else if (this.attributes.sequence) { + dnd = true; + } + if (this.screen.readonly) { + dnd = false; + } + + this.draggable = dnd; + if (dnd) { + this.colgroup.prepend(jQuery('', { + 'class': 'draggable-handle', + })); + this.thead.children().prepend(jQuery('', { + 'class': 'draggable-handle', + })); + this._add_drag_n_drop(); + } + }, + drag_data_get: function(evt) { + var row_position = 0; + var row_leaves = []; + var set_dragged_row = function(row) { + if (row.el[0] === evt.target) { + evt.originalEvent.dataTransfer.setData('path', row.path); + evt.originalEvent.dataTransfer.setData( + 'position', row_position); + } + if (row.rows.length === 0) { + row_leaves.push(row); + } + row_position += 1; + row.rows.forEach(set_dragged_row.bind(this)); + }; + this.rows.forEach(set_dragged_row.bind(this)); + }, + drag_data_received: function(evt) { + var dataTransfer = evt.originalEvent.dataTransfer; + var origin_path = dataTransfer.getData('path').split('.'); + if (origin_path.length === 0) { + return ; + } + + var row = this; + while (origin_path.length > 0) { + row = row.rows[origin_path[0]]; + origin_path = origin_path.slice(1); + } + var record = row.record; + + var parent_row = null; + var dest_position; + if (evt.ctrlKey && this.children_field) { + parent_row = this._find_row(row.el.prev()); + dest_position = (parent_row || this).rows.length; + } else { + var sibling_row; + if (evt.shiftKey) { + sibling_row = this._find_row(row.el.prev()); + if (sibling_row) { + parent_row = sibling_row.parent_; + dest_position = ( + (parent_row || this).rows.indexOf(sibling_row) + 1); + } else { + parent_row = null; + dest_position = 0; + } + } else { + sibling_row = this._find_row(row.el.next()); + if (sibling_row) { + parent_row = sibling_row.parent_; + dest_position = ( + (parent_row || this).rows.indexOf(sibling_row)); + } else { + parent_row = null; + dest_position = this.rows.length; + } + } + } + + var current_row = parent_row; + while (current_row && (current_row != row)) { + current_row = current_row.parent_; + } + if (current_row) { + // There is a recursion cancel the drop + // by moving the row at its previous place + var original_position = dataTransfer.getData('position'); + var successor = jQuery( + this.tbody.children()[original_position]); + successor.before(row.el); + return; + } + + var previous_row = row; + var move_child = function(child_row) { + previous_row.el.after(child_row.el); + previous_row = child_row; + child_row.rows.forEach(move_child); + }; + row.rows.forEach(move_child); + + var dest_group_prm; + var origin_group, origin_position; + origin_group = record.group; + origin_position = row.group_position; + if (parent_row) { + dest_group_prm = parent_row.record.children_group( + this.children_field); + } else { + dest_group_prm = jQuery.Deferred().resolve(this.group); + } + + dest_group_prm.then(function(dest_group) { + var origin_rows, dest_rows; + if (row.parent_) { + origin_rows = row.parent_.rows; + } else { + origin_rows = this.rows; + } + if (parent_row) { + dest_rows = parent_row.rows; + } else { + dest_rows = this.rows; + } + + if (origin_group === dest_group) { + if (origin_position < dest_position) { + dest_position -= 1; + } + origin_group.splice(origin_position, 1); + origin_group.splice(dest_position, 0, record); + origin_group.changed(); + } else { + origin_group.remove(record, true, true, true); + // Don't remove record from previous group + // as the new parent will change the parent + // This prevents concurrency conflict + origin_group.record_removed.splice( + origin_group.record_removed.indexOf(record)); + dest_group.add(record, dest_position); + if (!record.parent_name) { + record._changed[origin_group.parent_name] = true; + record._values[origin_group.parent_name] = null; + } else { + record._changed[origin_group.parent_name] = true; + } + } + dest_rows.splice(dest_position, 0, row); + origin_rows.splice(origin_position, 1); + + row.parent_ = parent_row; + row.record.group = dest_group; + dest_rows.slice(dest_position).forEach(function(r) { + r.reset_path(); + }); + origin_rows.slice(origin_position).forEach(function(r) { + r.reset_path(); + }); + + var selected = this.get_selected_paths(); + row.redraw(selected); + var child_redraw = function(child_row) { + child_row.redraw(selected); + child_row.rows.forEach(child_redraw); + }; + row.rows.forEach(child_redraw); + + if (this.attributes.sequence) { + row.record.group.set_sequence(this.attributes.sequence); + } + }.bind(this)); + }, get_fields: function() { return Object.keys(this.widgets); }, @@ -344,7 +536,7 @@ }.bind(this); var min_display_size = Math.min( this.group.length, this.display_size); - // XXX find better check to keep focus + // XXX find better check to keep focus and expanded if (this.children_field) { this.construct(); } else if ((min_display_size > this.rows.length) && @@ -411,7 +603,8 @@ if (column.header.hasClass('invisible')) { column.col.css('width', 0); column.col.hide(); - } else if (!column.col.hasClass('selection-state') && + } else if (!column.col.hasClass('draggable-handle') && + !column.col.hasClass('selection-state') && !column.col.hasClass('favorite')) { var width = { 'integer': 6, @@ -458,6 +651,9 @@ if (!extend) { this.rows = []; this.tbody = jQuery(''); + if (this.draggable) { + this._add_drag_n_drop(); + } this.edited_row = null; } else { this.tbody.find('tr.more-row').remove(); @@ -818,6 +1014,18 @@ row.set_editable(); } this.edited_row = row; + }, + _find_row: function(tr) { + var row = null; + var find_row = function(r) { + if (r.el[0] == tr[0]) { + row = r; + return; + } + r.rows.forEach(find_row); + }; + this.rows.forEach(find_row); + return row; } }); @@ -853,18 +1061,40 @@ this.parent_ = parent; this.children_field = tree.children_field; this.expander = null; + this._group_position = null; + this._path = null; this._drawed_record = null; - var path = []; - if (parent) { - path = jQuery.extend([], parent.path.split('.')); - } - path.push(pos); - this.path = path.join('.'); this.el = jQuery(''); this.el.on('click', this.select_row.bind(this)); }, + get group_position() { + if (this._group_position === null) { + this._group_position = this.record.group.indexOf(this.record); + } + return this._group_position; + }, + get path() { + if (!this._path) { + var path, position; + if (this.parent_) { + path = jQuery.extend([], this.parent_.path.split('.')); + } else { + path = []; + } + path.push(this.group_position); + this._path = path.join('.'); + } + return this._path; + }, + reset_path: function() { + this._group_position = null; + this._path = null; + for (var i=0; i < this.rows.length; i++) { + this.rows[i].reset_path(); + } + }, is_expanded: function() { - return (this.path in this.tree.expanded); + return this.tree.expanded.has(this); }, get_last_child: function() { if (!this.children_field || !this.is_expanded() || @@ -892,8 +1122,15 @@ el_node.removeChild(el_node.firstChild); } - var td; + var td, drag_img; this.tree.el.uniqueId(); + if (this.tree.draggable) { + td = jQuery('', { + 'class': 'draggable-handle' + }); + td.append(Sao.common.ICONFACTORY.get_icon_img('tryton-drag')); + this.el.append(td); + } td = jQuery('', { 'class': 'selection-state', }).click(function(event_) { @@ -911,7 +1148,6 @@ this.selection.change(this.selection_changed.bind(this)); td.append(this.selection); - var depth = this.path.split('.').length; for (var i = 0; i < this.tree.columns.length; i++) { var column = this.tree.columns[i]; td = jQuery('', { @@ -945,11 +1181,6 @@ 'class': 'icon', }); this.expander.html(' '); - var margin = 'margin-left'; - if (Sao.i18n.rtl) { - margin = 'margin-right'; - } - this.expander.css(margin, (depth - 1) + 'em'); this.expander.on('click keypress', Sao.common.click_press(this.toggle_row.bind(this))); row.append(jQuery('', { @@ -988,7 +1219,11 @@ }, _get_column_td: function(column_index, row) { row = row || this.el; - return jQuery(row.children()[column_index + 1]); + var offset = 1; // take into account the selection column + if (this.tree.draggable) { + offset += 1; + } + return jQuery(row.children()[column_index + offset]); }, redraw: function(selected, expanded) { selected = selected || []; @@ -1061,19 +1296,24 @@ var row_id_path = this.get_id_path(); this.set_selection(Sao.common.contains(selected, row_id_path)); if (this.children_field) { + var depth = this.path.split('.').length; + var margin = 'margin-left'; + if (Sao.i18n.rtl) { + margin = 'margin-right'; + } + this.expander.css(margin, (depth - 1) + 'em'); + var update_expander = function() { - var children = this.record.field_get_client( - this.children_field); - if (this.is_expanded() || - Sao.common.contains(expanded, row_id_path) && - !jQuery.isEmptyObject(children)) { + var length = this.record.field_get_client( + this.children_field).length; + if (length && ( + this.is_expanded() || + Sao.common.contains(expanded, row_id_path))) { this.expander.css('visibility', 'visible'); - this.tree.expanded[this.path] = this; + this.tree.expanded.add(this); this.expand_children(selected, expanded); this.update_expander(true); } else { - var length = this.record.field_get_client( - this.children_field).length; this.expander.css('visibility', length ? 'visible' : 'hidden'); this.update_expander(false); @@ -1094,7 +1334,7 @@ toggle_row: function() { if (this.is_expanded()) { this.update_expander(false); - delete this.tree.expanded[this.path]; + this.tree.expanded.delete(this); this.collapse_children(); } else { if (this.tree.n_children(this) > Sao.config.limit) { @@ -1102,7 +1342,7 @@ this.tree.screen.switch_view('form'); } else { this.update_expander(true); - this.tree.expanded[this.path] = this; + this.tree.expanded.add(this); this.expand_children(); } } @@ -1512,7 +1752,13 @@ if (access.create && !limit) { prm.then(function() { return this.tree.screen.new_(); - }.bind(this)); + }.bind(this)) + .then(function(record) { + var sequence = this.tree.attributes.sequence; + if (sequence) { + record.group.set_sequence(sequence); + } + }.bind(this)); } } else { this._get_column_td(next_column, next_row)