/* * Axelor Business Solutions * * Copyright (C) 2005-2019 Axelor (). * * 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 . */ (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: '
' }; }); 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 = $("
"); 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: '
'+ '
'+ '
' }; }); 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: "" }; }); })();