Files
ERP/sophal/js/view/view.tree.js

796 lines
19 KiB
JavaScript

/*
* Axelor Business Solutions
*
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
"use strict";
var ui = angular.module('axelor.ui');
ui.controller('TreeViewCtrl', TreeViewCtrl);
TreeViewCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
function TreeViewCtrl($scope, $element, DataSource, ActionService) {
var view = $scope._views.tree;
var viewPromise = $scope.loadView('tree', view.name);
$scope.$applyAsync(function() {
if (view.deferred) {
view.deferred.resolve($scope);
}
});
viewPromise.success(function(fields, schema){
$scope.parse(schema);
});
$scope.show = function() {
$scope.updateRoute();
};
$scope.onShow = function(promise) {
};
$scope.getRouteOptions = function() {
return {
mode: "tree"
};
};
$scope.setRouteOptions = function(options) {
$scope.updateRoute();
};
$scope.onRefresh = function() {
};
$scope.onSort = function(column) {
if (column) {
column.sort = true;
column.desc = column.desc !== undefined && !column.desc;
column.sortCss = column.desc ? "slick-sort-indicator-desc" : "slick-sort-indicator-asc";
}
$scope.onRefresh();
};
var first = null;
$scope.parse = function(schema) {
var columns = _.map(schema.columns, function(col) {
return new Column($scope, col);
});
var last = null;
var draggable = false;
var loaders = _.map(schema.nodes, function(node) {
var loader = new Loader($scope, node, DataSource);
if (last) {
last.child = loader;
}
if (loader.draggable) {
draggable = true;
}
return last = loader;
});
$scope.viewTitle = schema.title;
$scope.columns = columns;
$scope.loaders = loaders;
$scope.draggable = draggable;
first = _.first(loaders);
first.domain = $scope._domain;
first.context = $scope._context;
// recursive tree (parent -> child on same object)
if (loaders.length === 2 && first.model === last.model) {
last.child = last;
$scope._countOn = _.last(schema.nodes).parent;
}
};
$scope.onNext = function() {
return first && first.onNext();
};
$scope.onPrev = function() {
return first && first.onPrev();
};
$scope.canNext = function() {
return first && first.canNext();
};
$scope.canPrev = function() {
return first && first.canPrev();
};
$scope.pagerText = function() {
return first ? first.pagerText() : "";
};
$scope.resetPager = function() {
if (first) {
first.resetPager();
}
};
$scope.onClick = function(e, options) {
var loader = options.loader,
record = options.record;
var target = $(e.target);
if (target.is('img,i')) {
target = target.parent();
}
if (e.type === 'click' && !target.is('.tree-button')) {
return;
}
var action = target.attr('x-action') || loader.action;
if (!action) {
return;
}
var $handler = ActionService.handler($scope.$new(), $(e.currentTarget), {
action: action
});
var model = loader.model;
var context = record.$record;
$handler.scope.record = context;
$handler.scope.getContext = function() {
return _.extend({
_model: model
}, context);
};
$handler.onClick().then(function(res){
});
};
$scope.$on('on:tab-reload', function(e, tab) {
if ($scope === e.targetScope && $scope.onRefresh) {
$scope.onRefresh();
}
});
}
/**
* Column controller.
*
*/
function Column(scope, col) {
this.css = col.type || 'string';
this.name = col.name;
this.title = col.title || col.autoTitle;
if (this.title === null || this.title === undefined) {
this.title = _.humanize(col.name);
}
if (col.type == 'button') {
this.title = null;
}
this.cellCss = function(record) {
return this.css;
};
this.cellText = function(record) {
if (col.type === 'button') {
var template = "---";
var item = _.findWhere(record.$node.items, { type: 'button', name: col.name });
if (item) {
template = "<a href='javascript:' class='tree-button' x-action='"+ item.onClick +"'>";
if (item.icon) {
if (item.icon.indexOf('fa') === 0) {
template += "<i class='fa " + item.icon + "'></i>";
} else {
template += "<img width='16px' src='"+ item.icon +"'>";
}
}
if (item.title) {
template += item.title;
}
template += "</a>";
}
return template;
}
var value = record[this.name];
if (value === undefined || value === null) {
return '---';
}
var selection = (record.$selection || {})[this.name];
if (selection) {
var cmp = col.type === "integer" ? function(a, b) { return a == b ; } : _.isEqual;
var res = _.find(selection, function(item){
return cmp(item.value, value);
}) || {};
if (col.widget === 'ImageSelect' && res.icon) {
var image = "<img style='max-height: 24px;' src='" + (res.icon || res.value) + "'>";
if (col.labels === false) {
return image;
}
return image + " " + res.title;
}
return res.title;
}
var type = col.type;
if (type === 'reference') {
type = 'many-to-one';
}
var item = _.findWhere(record.$node.items, { type: 'field', as: col.name });
var attrs = _.extend({}, item, col);
var fn = ui.formatters[type];
if (fn) {
value = fn(attrs, value, record);
}
return value === undefined || value === null ? '---' : value;
};
}
/**
* Node loader.
*
*/
function Loader(scope, node, DataSource) {
var ds = DataSource.create(node.model);
var names = _.pluck(node.items, 'name');
var domain = null;
if (node.parent) {
domain = "self." + node.parent + ".id = :parentId";
ds._page.limit = -1;
}
if (node.domain) {
if (domain) {
domain = '(' + domain + ') AND (' + node.domain + ')';
} else {
domain = node.domain;
}
}
this.node = node;
this.child = null;
this.model = node.model;
this.action = node.onClick;
this.draggable = node.draggable;
this.getDomain = function(context) {
var _domain = domain,
_context = context;
if (_domain && this.domain) {
_domain = "(" + this.domain + ") AND (" + domain + ")";
}
_domain = _domain || this.domain;
_context = _.extend({}, this.context, context);
return {
domain: _domain,
context: _context
};
};
this.resetPager = function () {
ds._page.from = 0;
};
this.load = function(item, callback) {
var context = _.extend({}, scope._context),
current = item && item.$record;
var sortOn = _.filter(scope.columns, function (col) { return col.sort; });
var sortBy = _.map(sortOn, function (col) {
var field = _.findWhere(node.items, { as: col.name });
if (field) {
return col.desc ? '-' + field.name : field.name;
}
});
sortBy = _.compact(sortBy).join(',') || node.orderBy;
if (scope.getContext) {
context = _.extend(context, scope.getContext());
}
if (current) {
context.parentId = current.id;
}
if (scope._countOn) {
context._countOn = scope._countOn;
} else if (this.child) {
var child = this.child.node;
context._childOn = {
model: child.model,
parent: child.parent
};
}
var opts = _.extend(this.getDomain(context), {
fields: names,
action: scope._viewAction
});
if (sortBy) {
opts.sortBy = sortBy.split(',');
}
var promise = ds.search(opts);
promise.success(function(records) {
if (callback) {
callback(accept(item, records));
}
});
return promise;
};
this.move = function(item, callback) {
var record = item.$record,
parent = { id: item.$parentId };
record[node.parent || scope._countOn] = parent;
return ds.save(record).success(function(rec) {
record.version = rec.version;
if (callback) {
callback(rec);
}
});
};
var that = this;
function accept(current, records) {
var fields = node.items,
parent = current && current.$record,
child = that.child;
return _.map(records, function(record) {
var $id = _.uniqueId('row');
var $parent = current ? current.$id : null;
var item = {
'$id': $id,
'$model': node.model,
'$node': node,
'$record': record,
'$selection': {},
'$parent': $parent,
'$parentId': parent && parent.id,
'$parentModel': current && current.$model,
'$draggable': node.draggable,
'$folder': child && (record._children === undefined || record._children > 0)
};
item.$expand = function(callback) {
if (child) {
return child.load(this, callback);
}
};
item.$move = function(callback) {
return that.move(this, callback);
};
item.$click = function(e) {
if (node.onClick) {
scope.onClick(e, {
loader: that,
record: item,
parent: parent
});
}
};
_.each(fields, function(field) {
var name = field.as || field.name;
item[name] = record[field.name];
item.$selection[name] = field.selectionList;
});
return item;
});
}
var page = {};
ds.on('change', function(e, _records, _page) {
page = _page;
});
this.canNext = function() {
return ds.canNext();
};
this.canPrev = function() {
return ds.canPrev();
};
this.onNext = function() {
ds.next(names).success(function(records){
scope.setRootNodes(accept(null, records));
});
};
this.onPrev = function() {
ds.prev(names).success(function(records){
scope.setRootNodes(accept(null, records));
});
};
this.pagerText = function() {
if (page && page.from !== undefined) {
if (page.total === 0) return null;
return _t("{0} to {1} of {2}", page.from + 1, page.to, page.total);
}
};
}
ui.directive('uiViewTree', function(){
return {
replace: true,
link: function(scope, element, attrs) {
var table = element.find('.tree-table > table');
table.treetable({
indent: 16,
expandable: true,
clickableNodeNames: true,
nodeIdAttr: "id",
parentIdAttr: "parent",
branchAttr: "folder",
onNodeCollapse: function onNodeCollapse() {
var node = this,
row = node.row;
if (node._state === "collapsed") {
return;
}
node._state = "collapsed";
table.treetable("collapseNode", row.data("id"));
adjustCols();
},
onNodeExpand: function onNodeExpand() {
var node = this,
row = this.row,
record = row.data('$record');
if (node._loading || node._state === "expanded") {
return;
}
node._state = "expanded";
if (node._loaded) {
table.treetable("expandNode", row.data("id"));
return adjustCols();
}
node._loading = true;
if (record.$expand) {
record.$expand(function(records) {
acceptNodes(records, node);
node._loading = false;
node._loaded = true;
adjustCols();
});
}
}
});
function acceptNodes(records, after) {
var rows = _.map(records, makeRow);
table.treetable("loadBranch", after, rows);
}
function makeRow(record) {
var tr = $('<tr>')
.attr('data-id', record.$id)
.attr('data-parent', record.$parent)
.attr('data-folder', record.$folder);
tr.data('$record', record);
_.each(scope.columns, function(col) {
$('<td>').html(col.cellText(record)).appendTo(tr);
});
if (scope.draggable && (record.$folder || scope._countOn || !record.$parent)) {
makeDroppable(tr);
}
if (record.$draggable || (scope.draggable && scope._countOn)) {
makeDraggable(tr);
}
tr.on('click dblclick taphold', function(e) {
record.$click(e);
});
return tr[0];
}
function onDrop(e, ui) {
/* jshint validthis: true */
var row = ui.draggable,
record = row.data('$record'),
current = $(this).data('$record'),
node = table.treetable("node", row.data("id")),
nodeParent = node.parentNode();
table.treetable("move", node.id, $(this).data("id"));
// make sure to remove expander icon if no children left
if (nodeParent && nodeParent.children.length === 0) {
nodeParent.row.removeClass('expanded');
nodeParent.row.removeClass('branch');
nodeParent.row.addClass('leaf');
nodeParent.treeCell.off('click.treetable');
nodeParent.treeCell.off('keydown.treetable');
nodeParent.indenter.empty();
}
record.$parentId = current.$record.id;
record.$move(function(result) {
});
}
function isParent(source, target) {
var parent = target.parent().find('[data-id=' + target.data('parent') + ']');
if (parent.data('id') === source.data('id')) {
return true;
}
if (parent.length) {
return isParent(source, parent);
}
return false;
}
function makeDroppable(row) {
row.droppable({
accept: function(draggable, x) {
var source = draggable.data('$record'),
target = row.data('$record');
// don't allow moving parent to child
if (scope._countOn) {
return !isParent(draggable, $(this));
}
return source && target && target.$model === source.$parentModel;
},
hoverClass: "accept",
drop: onDrop,
over: function(e, ui) {
var row = ui.draggable;
if(this != row[0] && !$(this).is(".expanded")) {
table.treetable("expandNode", $(this).data("id"));
}
}
});
}
function makeDraggable(row) {
var record = row.data('$record');
if (!record.$draggable && !scope._countOn) {
return;
}
row.draggable({
helper: function() {
return $('<span></span>').append(row.children('td:first').clone());
},
opacity: 0.75,
containment: 'document',
refreshPositions: true,
revert: "invalid",
revertDuration: 300,
delay: 300,
scroll: true
});
}
function clear() {
var tree = table.data('treetable');
if (tree === undefined) {
return;
}
_.each(tree.roots, function(node) {
tree.unloadBranch(node);
node.row.remove();
delete tree.tree[node.id];
});
tree.nodes.length = 0;
tree.roots.length = 0;
}
scope.onRefresh = function() {
var root = _.first(scope.loaders);
if (root) {
root.load(null, function(nodes) {
scope.setRootNodes(nodes);
});
}
};
scope.setRootNodes = function(nodes) {
clear();
acceptNodes(nodes);
};
scope.onHeaderClick = function (event, column) {
if (!event.shiftKey) {
_.each(scope.columns, function (col) {
if (col !== column) {
col.sort = false;
col.desc = undefined;
col.sortCss = null;
}
});
}
scope.onSort(column);
};
var watcher = scope.$watch('loaders', function treeLoadersWatch(loaders) {
if (loaders === undefined) {
return;
}
watcher();
var root = _.first(loaders);
if (root) {
root.load(null, acceptNodes).then(adjustCols);
}
});
var adjustCounter = 0;
function adjustCols() {
if (element.is(':hidden')) {
if (adjustCounter++ < 10) {
_.delay(adjustCols, 100);
}
return;
}
adjustCounter = 0;
var tds = table.find('tr:first').find('td');
var ths = element.find('.tree-header').find('th');
var widths = [];
if (tds.length !== ths.length) {
return;
}
tds.each(function() {
widths.push($(this).outerWidth());
});
ths.each(function(i) {
$(this).width(widths[i] - 12);
});
}
scope.$onAdjust(adjustCols, 100);
table.on('mousedown.treeview', 'tbody tr', function(e) {
table.find('tr.selected').removeClass('selected');
$(this).addClass("selected");
});
},
template:
'<div class="tree-view-container" ui-attach-scroll="> .tree-table">'+
'<table class="tree-header">'+
'<thead>'+
'<tr>'+
'<th ng-repeat="column in columns" ng-class="column.css" ng-click="onHeaderClick($event, column)">' +
'<span>{{column.title}}</span>'+
'<span ng-if="column.sort" class="slick-sort-indicator" ng-class="column.sortCss"></span>'+
'</th>'+
'</tr>'+
'</thead>'+
'</table>'+
'<div class="tree-table">'+
'<table>'+
'<tbody></tbody>'+
'</table>'+
'</div>'+
'</div>'
};
});
TreePortletCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
function TreePortletCtrl($scope, $element, DataSource, ActionService) {
TreeViewCtrl.call(this, $scope, $element, DataSource, ActionService);
$scope.showPager = true;
$scope.$on("on:new", function(e) {
$scope.resetPager();
$scope.onRefresh();
});
$scope.$on("on:edit", function(e) {
$scope.resetPager();
$scope.onRefresh();
});
}
ui.directive('uiPortletTree', function(){
return {
controller: TreePortletCtrl,
template: '<div ui-view-tree></div>'
};
});
})();