Files
ERP/sophal/js/view/view.grid.js

1066 lines
28 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() {
/* global Slick: true */
"use strict";
var ui = angular.module('axelor.ui');
ui.controller('GridViewCtrl', GridViewCtrl);
ui.GridViewCtrl = GridViewCtrl;
ui.GridViewCtrl.$inject = ['$scope', '$element'];
function GridViewCtrl($scope, $element) {
ui.DSViewCtrl('grid', $scope, $element);
var ds = $scope._dataSource;
var page = {};
$scope.dataView = new Slick.Data.DataView();
$scope.selection = [];
ds.on('change', function(e, records, page){
$scope.setItems(records, page);
});
var initialized = false;
var reloadDotted = false;
$scope.onShow = function(viewPromise) {
if (!initialized) {
viewPromise.then(function(){
var view = $scope.schema,
params = $scope._viewParams,
sortBy = view.orderBy,
pageNum = null;
if (sortBy) {
sortBy = sortBy.split(',');
}
if (params.options && params.options.mode === 'list') {
pageNum = params.options.state;
$scope._routeSearch = params.options.search;
}
reloadDotted = params.params && params.params['reload-dotted'];
$scope.view = view;
if (view.noFetch) return;
var opts = ds._filter ? ds._filter : {
_sortBy: sortBy,
_pageNum: pageNum
};
$scope.filter(opts).then(function(){
$scope.$broadcast('on:grid-selection-change', $scope.getContext());
$scope.updateRoute();
});
});
initialized = true;
} else {
if (reloadDotted || ($scope._viewTypeLast && $scope._viewTypeLast !== 'grid')) {
return $scope.reload().then(function() {
$scope.updateRoute();
});
}
var current = $scope.dataView.getItem(_.first($scope.selection));
if (current && current.id) {
$scope.dataView.updateItem(current.id, current);
}
}
};
$scope.getRouteOptions = function() {
var pos = 1,
args = [],
query = {},
params = $scope._viewParams;
if (page && page.limit) {
pos = (page.from / page.limit) + 1;
} else if (params.options && params.options.mode === 'list') {
pos = +(params.options.state);
}
pos = pos || 1;
args = [pos];
return {
mode: 'list',
args: args,
query: query
};
};
$scope._routeSearch = {};
$scope.setRouteOptions = function(options) {
var opts = options || {},
pos = +(opts.state),
current = (page.from / page.limit) + 1;
pos = pos || 1;
current = current || 1;
$scope._routeSearch = opts.search;
if (pos === current) {
return $scope.updateRoute();
}
var params = $scope._viewParams;
if (params.viewType !== "grid") {
return $scope.show();
}
$scope.filter({
_pageNum: pos
});
};
$scope.getItem = function(index) {
return $scope.dataView.getItem(index);
};
$scope.getItems = function() {
return $scope.dataView.getItems();
};
$scope.setItems = function(items, pageInfo) {
var dataView = $scope.dataView;
var selection = $scope.selection || [];
var selectionIds = dataView.mapRowsToIds(selection);
var hasSelected = _.some(items, function (item) { return item.selected; });
var syncSelection = function () {
if (dataView.$syncSelection) {
setTimeout(function(){
if (hasSelected) {
dataView.$syncSelection();
} else {
dataView.$syncSelection(selection, selectionIds);
}
});
}
};
//XXX: clear existing items (bug?)
if (dataView.getLength()) {
dataView.beginUpdate();
dataView.setItems([]);
dataView.endUpdate();
}
dataView.beginUpdate();
dataView.setItems(items);
dataView.endUpdate();
if (pageInfo) {
page = pageInfo;
}
var details = $scope.$details;
if (details) {
details.$timeout(function () {
var record = details.record || {};
var found = _.findWhere(items, { id: record.id });
if (found) {
found.selected = true;
return;
}
details.edit(null);
syncSelection();
});
} else {
syncSelection();
}
$scope.$broadcast('grid:adjust-columns');
};
$scope.attr = function (name) {
if (!$scope.schema || $scope.schema[name] === undefined) {
return true;
}
return $scope.schema[name];
};
$scope.canNew = function() {
return $scope.hasButton('new');
};
$scope.canEdit = function() {
return $scope.hasButton('edit') && $scope.selection.length > 0;
};
$scope.canShowDetailsView = function () {
var params = ($scope._viewParams || {}).params || {};
return params['details-view'] && !axelor.device.mobile;
};
$scope.canShowSave = function () {
if ($scope.$details) {
return true;
}
return $scope.hasButton('save') && $scope.canEditInline();
};
$scope.canSave = function() {
if ($scope.$details && $scope.$details.canSave()) {
return true;
}
return $scope.hasButton('save') && this.dataView.canSave && this.dataView.canSave();
};
$scope.canDelete = function() {
return $scope.hasButton('delete') && !$scope.canSave() && $scope.selection.length > 0;
};
$scope.canArchive = function() {
return $scope.hasPermission('write')
&& $scope.hasButton('archive')
&& !$scope.canSave()
&& $scope.selection.length > 0;
};
$scope.canUnarchive = function() {
return $scope.canArchive();
};
$scope.canEditInline = function() {
return _.isFunction(this.dataView.canSave);
};
$scope.canMassUpdate = function () {
// this permission is actually calculated from fields marked for mass update
return $scope.hasPermission('massUpdate', false) || ($scope.schema && $scope.schema.canMassUpdate);
};
$scope.canExport = function() {
return $scope.hasPermission('export');
};
$scope.selectFields = function() {
return _.map($scope.fields, function (field) {
if (field.jsonField) {
return field.name + '::' + (field.jsonType || 'text');
}
return field.name;
});
};
$scope.filter = function(searchFilter) {
var fields = $scope.selectFields(),
options = {};
function fixPage() {
var promise = ds.fixPage();
if (promise) {
$scope.updateRoute();
return promise;
}
}
// if criteria is given search using it
if (searchFilter.criteria || searchFilter._domains) {
options = {
filter: searchFilter,
fields: fields
};
if (searchFilter.archived !== undefined) {
options.archived = searchFilter.archived;
}
return ds.search(options).then(fixPage);
}
var filter = {},
sortBy, pageNum,
domain = null,
context = null,
action = null,
criteria = {
operator: 'and'
};
for(var name in searchFilter) {
var value = searchFilter[name];
if (value !== '') filter[name] = value;
}
pageNum = +(filter._pageNum || 0);
sortBy = filter._sortBy;
domain = filter._domain;
context = filter._context;
action = filter._action;
delete filter._pageNum;
delete filter._sortBy;
delete filter._domain;
delete filter._context;
delete filter._action;
criteria.criteria = _.map(filter, function(value, key) {
var field = $scope.fields[key] || _.findWhere(($scope.schema||{}).items, { name: key }) || {};
var type = field.type || 'string';
var operator = 'like';
var origValue = value;
var value2;
//TODO: implement expression parser
if (type === 'many-to-one' && !field.jsonField) {
if (field.targetName) {
key = key + '.' + field.targetName;
} else {
console.warn("Can't search on field: ", key);
}
}
if (field.selection) {
type = 'selection';
}
function stripOperator(val) {
var match = /(<)(.*)(<)(.*)/.exec(val);
if (match) {
operator = 'between';
value2 = match[2].trim();
return match[4].trim();
}
match = /(<=?|>=?|=)(.*)/.exec(val);
if (match) {
operator = match[1];
return match[2].trim();
}
return val;
}
function toMoment(val) {
var format = 'MM/YYYY';
if (/\d+\/\d+\/\d+/.test(val)) format = 'DD/MM/YYYY';
if (/\d+\/\d+\/\d+\s+\d+:\d+/.test(val)) format = 'DD/MM/YYYY HH:mm';
return val ? moment(val, format) : moment();
}
function toDate(val) {
return val ? toMoment(val).format('YYYY-MM-DD') : val;
}
function toDateString(val) {
return moment.utc(val, 'DD/MM/YYYY').toDate().toISOString().split("T")[0];
}
switch(type) {
case 'integer':
case 'long':
case 'decimal':
operator = '=';
value = stripOperator(value);
value = +(value) || 0;
if (value2) value2 = +(value2) || 0;
break;
case 'boolean':
operator = '=';
value = !/f|n|false|no|0/.test(value);
break;
case 'date':
operator = '=';
value = stripOperator(value);
if (value) value = toDateString(value);
if (value2) value2 = toDateString(value2);
break;
case 'time':
operator = '=';
break;
case 'datetime':
operator = 'between';
value = stripOperator(value);
var val = toMoment(value);
value = (operator == 'between' ? val.startOf('day') : val).toDate().toISOString();
value2 = (operator == 'between' ? val.endOf('day') : val).toDate().toISOString();
break;
case 'enum':
case 'selection':
operator = '=';
break;
}
// tag json fields
if (field.jsonField) {
key += '::' + (field.jsonType || 'text');
}
return {
fieldName: key,
operator: operator,
value: value,
value2: value2
};
});
domain = domain || $scope._domain;
context = _.extend({}, $scope._context, context);
if (domain && $scope.getContext) {
context = _.extend(context, $scope.getContext());
}
context._model = context._model || $scope._model;
options = {
filter: criteria,
fields: fields,
sortBy: sortBy,
domain: domain,
context: context,
action: action
};
if (pageNum) {
options.offset = (pageNum - 1 ) * ds._page.limit;
}
var advance = arguments.length > 1 ? arguments[1] : null;
if (advance && advance.criteria && advance.criteria.length) {
if (_.isEmpty(criteria.criteria)) {
options.filter = advance;
} else {
criteria.criteria = [{
operator: criteria.operator,
criteria: criteria.criteria
}, {
operator: advance.operator,
criteria: advance.criteria
}];
criteria.operator = "and";
}
}
if (advance && advance.archived !== undefined) {
options.archived = advance.archived;
}
return ds.search(options).then(fixPage);
};
$scope.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);
}
};
$scope.pagerIndex = function(fromSelection) {
var index = page.index,
record = null;
if (fromSelection) {
record = $scope.dataView.getItem(_.first($scope.selection));
index = ds._data.indexOf(record);
}
return index;
};
$scope.onNext = function() {
var fields = $scope.selectFields();
ds.next(fields).then(function(){
$scope.updateRoute();
});
};
$scope.onPrev = function() {
var fields = $scope.selectFields();
ds.prev(fields).then(function(){
$scope.updateRoute();
});
};
$scope.onNew = function() {
page.index = -1;
if ($scope.$details) {
$scope.$details.onNew();
return;
}
$scope.switchTo('form', function(viewScope){
$scope.ajaxStop(function(){
$scope.$timeout(function(){
viewScope.$broadcast('on:new');
});
});
});
};
$scope.onEdit = function(force) {
page.index = $scope.pagerIndex(true);
$scope.switchTo('form', function (formScope) {
formScope.__canForceEdit = force;
});
};
$scope.$confirmMessage = _t("Do you really want to delete the selected record(s)?");
$scope.$confirmArchiveMessage = _t("Do you really want to archive the selected record(s)?");
$scope.$confirmUnarchiveMessage = _t("Do you really want to unarchive the selected record(s)?");
$scope.onDelete = function() {
var message = $scope.$confirmMessage;
var message = _.isFunction(message) ? message() : message;
axelor.dialogs.confirm(message, function(confirmed){
if (!confirmed)
return;
var selected = _.map($scope.selection, function(index) {
return $scope.dataView.getItem(index);
});
ds.removeAll(selected).success(function(records, page){
if (records.length === 0 && page.total > 0) {
$scope.onRefresh();
}
});
});
};
$scope._doArchive = function (message, archive) {
axelor.dialogs.confirm(message, function(confirmed) {
if (!confirmed) {
return;
}
var selected = _.map($scope.selection, function(index) {
var item = $scope.dataView.getItem(index);
return _.extend({}, _.pick(item, 'id', 'version'), { archived: archive });
});
ds.saveAll(selected).success(function() {
$scope.onRefresh();
});
});
};
$scope.onArchive = function() {
$scope._doArchive($scope.$confirmArchiveMessage, true);
};
$scope.onUnarchive = function() {
$scope._doArchive($scope.$confirmUnarchiveMessage, false);
};
$scope.onRefresh = function() {
if ($scope.$details) {
$scope.$details.onNew().then(function () {
$scope.reload();
});
} else {
$scope.reload();
}
};
$scope.isDirty = function () {
if ($scope.$details && $scope.$details.isDirty) {
return $scope.$details.isDirty();
}
return false;
};
$scope.confirmDirty = function(callback, cancelCallback) {
if ($scope.$details && $scope.$details.confirmDirty) {
return $scope.$details.confirmDirty(callback, cancelCallback);
}
return callback();
};
$scope.reload = function() {
var fields = $scope.selectFields();
return ds.search({
fields: fields
});
};
$scope.onSort = function(event, args) {
var fields = $scope.selectFields();
var sortBy = _.map(args.sortCols, function(column) {
var field = column.sortCol.descriptor;
var name = column.sortCol.field;
if (field.jsonField) {
if (field.type === 'many-to-one' && field.targetName) {
name = name + "." + field.targetName;
}
name += '::' + ('integer,boolean,decimal'.indexOf(field.type) > -1 ? field.type : 'text');
}
var spec = column.sortAsc ? name : '-' + name;
return spec;
});
ds.search({
sortBy: sortBy,
fields: fields
});
};
$scope.onSelectionChanged = function(event, args) {
var items = $scope.getItems();
var selection = [];
_.each(items, function (item) {
item.selected = false;
});
_.each(args.rows, function(index) {
var item = args.grid.getDataItem(index);
if (item && item.id && item.id !== 0) {
item.selected = true;
selection.push(index);
}
});
$scope.selection = selection;
$scope.$timeout(function () {
$scope.$broadcast('on:grid-selection-change', $scope.getContext());
});
if ($scope.$details) {
$scope.$details.selectionChanged(selection);
}
};
$scope.onItemClick = function(event, args) {
if (axelor.device.mobile) {
$scope.$timeout(function () {
$scope.onEdit();
});
}
};
$scope.onItemDblClick = function(event, args) {
$scope.onEdit();
$scope.$applyAsync();
};
var _getContext = $scope.getContext;
$scope.getContext = function() {
// if nested grid then return parent's context
if (_getContext) {
return _getContext();
}
var dataView = $scope.dataView;
var selected = _.map($scope.selection || [], function(index) {
return dataView.getItem(index).id;
});
return selected.length ? { _ids: selected } : {};
};
$scope.getActionData = function() {
// ignore if nested grid or has selected rows
if (_getContext || !_.isEmpty($scope.selection)) {
return false;
}
return _.extend({
_domain: ds._lastDomain,
_domainContext: ds._lastContext,
_archived: ds._showArchived
}, ds._filter);
};
$scope.onSave = function() {
if ($scope.$details) {
$scope.$details.onSave();
}
if ($scope.dataView.saveChanges) {
$scope.dataView.saveChanges();
}
};
$scope.onArchived = function(e) {
var button = $(e.currentTarget);
setTimeout(function(){
var active = button.is('.active');
var fields = $scope.selectFields();
ds.search({
fields: fields,
archived: active
});
});
};
$scope.onExport = function (full) {
var view = $scope.view || $scope.schema || {};
if (!view.items) return;
var names = _.pluck(view.items, 'name');
var fields = full
? []
: _.chain($scope.getVisibleCols())
.map(function (col) { return (col.descriptor||{}).name; })
.compact()
.filter(function (name) { return names.indexOf(name) > -1; })
.value();
return ds.export_(fields).success(function(res) {
var fileName = res.fileName;
var filePath = 'ws/rest/' + $scope._model + '/export/' + fileName;
if (ds._page.total > res.exportSize) {
axelor.notify.alert(_t("{0} records exported.", res.exportSize), { title: _t('Warning!') });
}
ui.download(filePath, fileName);
});
};
function focusFirst() {
var index = _.first($scope.selection) || 0;
var first = $scope.dataView.getItem(index);
if (first) {
$scope.dataView.$syncSelection([], [first.id], true);
}
}
$scope.onHotKey = function (e, action) {
if (action === "save" && $scope.canSave()) {
$scope.onSave();
}
if (action === "refresh") {
$scope.onRefresh();
}
if (action === "new") {
$scope.onNew();
}
if (action === "edit") {
if ($scope.canEdit()) {
$scope.onEdit(true);
} else {
focusFirst();
}
}
if (action === "delete" && $scope.canDelete()) {
$scope.onDelete();
}
if (action === "select") {
focusFirst();
}
if (action === "prev" && $scope.canPrev()) {
$scope.onPrev();
}
if (action === "next" && $scope.canNext()) {
$scope.onNext();
}
$scope.$applyAsync();
return false;
};
}
ui.directive('uiViewGrid', function(){
return {
replace: true,
template: '<div ui-slick-grid ui-widget-states></div>'
};
});
ui.directive('uiViewDetails', ['DataSource', 'ViewService', function(DataSource, ViewService) {
return {
scope: {},
controller: ['$scope', '$element', function ($scope, $element) {
var parent = $scope.$parent;
var params = _.pick(parent._viewParams, ['views', 'model', 'domain', 'context', 'params']);
var view = _.findWhere(params.views, { type: 'form' }) || { type: 'form' };
if (params.params && _.isString(params.params['details-view'])) {
view = { type: 'form', name: params.params['details-view']};
}
params.views = [view];
$scope._viewParams = params;
$scope._isDetailsForm = true;
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
// use same ds as grid
$scope._dataSource = parent._dataSource;
ui.FormViewCtrl.call(this, $scope, $element);
parent.$parent.$details = $scope;
var ds = $scope._dataSource;
var noop = angular.noop;
$scope.getRouteOptions = noop;
$scope.setRouteOptions = noop;
$scope.updateRoute = noop;
$scope.$locationChangeCheck = noop;
$scope.switchBack = noop;
$scope.switchTo = noop;
$scope.onHotKey = noop;
$scope._dataSource = parent._dataSource;
$scope.setEditable(true);
$scope.show();
function doEdit(index) {
var found = ds.at(index);
var record = $scope.record;
if (record && found.id === record.id) return;
$scope.doRead(found.id).success(function(record) {
$scope.edit(record);
});
}
$scope.selectionChanged = _.debounce(function (selection) {
var current = $scope.record || {};
var first = _.first(selection);
if (first !== undefined) {
doEdit(first);
} else if (current.id > 0) {
$scope.edit(null);
}
}, 300);
$scope.$on("on:new", function(e) {
var dataView = parent.dataView;
if (dataView && dataView.$syncSelection) {
dataView.$syncSelection([], [], true);
}
});
$scope.$on("on:edit", function(e) {
var record = $scope.record || {};
var dataView = parent.dataView;
if (dataView && record.id > 0) {
var found = _.findWhere(dataView.getItems(), { id: record.id });
if (found) {
found.selected = true;
}
dataView.$syncSelection([], [], false);
}
});
}],
link: function (scope, element, attrs) {
var overlay = $("<div class='slickgrid-overlay'>");
scope.waitForActions(function () {
element.parent().children('.slickgrid').append(overlay);
});
scope.$watch('$$dirty', function gridDirtyWatch(dirty) {
overlay.toggle(dirty);
});
},
replace: true,
templateUrl: "partials/views/details-form.html"
};
}]);
ui.directive('uiPortletGrid', function(){
return {
controller: ['$scope', '$element', 'ViewService', 'NavService', 'MenuService',
function($scope, $element, ViewService, NavService, MenuService) {
GridViewCtrl.call(this, $scope, $element);
var ds = $scope._dataSource;
function doEdit(force) {
var promise = MenuService.action($scope._viewAction, {
context: $scope.getContext()
});
promise.success(function (result) {
if (!result.data) return;
var view = result.data[0].view;
return doOpen(force, view);
});
}
function doOpen(force, tab) {
var index = $scope.pagerIndex(true);
var record = ds.at(index);
if ($scope._viewAction === "dms.file.children") {
NavService.openTabByName("dms.file", {
mode: "edit",
state: record.id
});
return;
}
tab.viewType = "form";
tab.recordId = record.id;
tab.action = _.uniqueId('$act');
tab.forceReadonly = $scope.isReadonly && $scope.isReadonly();
if (tab.forceReadonly || $scope._isPopup || (($scope._viewParams || {}).params || {}).popup) {
tab.$popupParent = $scope;
tab.params = tab.params || {};
_.defaults(tab.params, {
'show-toolbar': false
});
}
setTimeout(function(){
NavService.openView(tab);
$scope.$applyAsync();
if (force) {
$scope.waitForActions(function() {
var scope = tab.$viewScope || ($scope.selectedTab || {}).$viewScope;
if (scope && scope.onEdit) {
scope.onEdit();
}
});
}
});
}
$scope.showPager = true;
$scope.onEdit = doEdit;
$scope.onItemDblClick = function(event, args) {
doEdit(false);
};
$scope.$on("on:new", function(e) {
ds._page.from = 0;
$scope.onRefresh();
});
$scope.$on("on:edit", function(e) {
ds._page.from = 0;
$scope.onRefresh();
});
var unwatch = false;
var loading = false;
$scope.onRefresh = function () {
var tab = NavService.getSelected();
var type = $scope.$parent._viewType
if (['dashboard', 'form'].indexOf(type) === -1) {
if (unwatch) {
unwatch();
unwatch = null;
}
return;
}
if (unwatch || loading) {
return;
}
unwatch = $scope.$watch(function gridVisibleWatch() {
if ($element.is(":hidden")) {
return;
}
unwatch();
unwatch = null;
loading = true;
$scope.waitForActions(function () {
$scope.ajaxStop(function () {
loading = false;
$scope.filter({});
});
});
});
};
var _onShow = $scope.onShow;
var _filter = $scope.filter;
var _action = $scope._viewAction;
var _field = $scope.field || {};
$scope.onGridInit = function (grid, inst) {
var editCol = _.findWhere(inst.cols, {id: '_edit_column'}) || {};
editCol.descriptor = { hidden : true };
$scope.$parent.$watch("isReadonly()", function (readonly) {
if (inst.editable) {
grid.setOptions({ editable: readonly });
}
inst.showColumn('_edit_column', !readonly);
editCol.descriptor = { hidden : readonly };
});
};
$scope.filter = function (searchFilter) {
var opts = _.extend({}, searchFilter, {
_action: _action
});
var ds = $scope._dataSource;
var view = $scope.schema || {};
if (!opts._sortBy && !ds._sortBy && view.orderBy) {
opts._sortBy = view.orderBy.split(',');
}
if ($scope._context && $scope.formPath && $scope.getContext) {
opts._context = _.extend({id: null}, _.pick($scope.getContext(), _.keys($scope._context)));
if ($scope._context._id) {
opts._context._id = opts._context.id;
}
}
return _filter.call($scope, opts);
};
$scope.onShow = function () {
var scope = ($scope.selectedTab || {}).$viewScope;
if (scope && scope.editRecord) {
return;
}
return _onShow.apply($scope, arguments);
};
}],
replace: true,
template:
'<div class="portlet-grid">'+
'<div ui-view-grid x-view="schema" x-on-init="onGridInit" x-data-view="dataView" x-editable="false" x-no-filter="{{noFilter}}" x-handler="this"></div>'+
'</div>'
};
});
ui.directive('uiTopHelp', function () {
return {
link: function (scope, element) {
var unwatch = scope.$watch('schema', function gridSchemaWatch(view) {
if (view) {
unwatch();
}
if (view && view.help) {
element.popover({
html: true,
title: view.title,
content: view.help,
placement: 'bottom',
trigger: 'hover',
delay: { show: 500, hide: 100 },
container: 'body'
});
}
});
},
replace: true,
template:
"<button ng-show='schema.help'>" +
"<i class='fa fa-info'></i>" +
"</button>"
};
});
})();