/* * 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() { 'use strict'; var ui = angular.module('axelor.ui'); var OPERATORS = { "=" : _t("equals"), "!=" : _t("not equal"), ">=" : _t("greater or equal"), "<=" : _t("less or equal"), ">" : _t("greater than"), "<" : _t("less than"), "like" : _t("contains"), "notLike" : _t("doesn't contain"), "in" : _t("in"), "notIn" : _t("not in"), "between" : _t("in range"), "notBetween" : _t("not in range"), "isNull" : _t("is null"), "notNull" : _t("is not null"), "true" : _t("is true"), "false" : _t("is false") }; var OPERATORS_BY_TYPE = { "enum" : ["=", "!=", "isNull", "notNull"], "text" : ["like", "notLike", "isNull", "notNull"], "string" : ["=", "!=", "like", "notLike", "isNull", "notNull"], "integer" : ["=", "!=", ">=", "<=", ">", "<", "between", "notBetween", "isNull", "notNull"], "boolean" : ["true", "false"] }; _.each(["long", "decimal", "date", "time", "datetime"], function(type) { OPERATORS_BY_TYPE[type] = OPERATORS_BY_TYPE.integer; }); _.each(["one-to-many"], function(type) { OPERATORS_BY_TYPE[type] = ["isNull", "notNull"]; }); _.each(["one-to-one", "many-to-one", "many-to-many"], function(type) { OPERATORS_BY_TYPE[type] = ["like", "notLike", "in", "notIn", "isNull", "notNull"]; }); function sharedProperty(scope, handler, property, initialValue) { var ds = handler._dataSource; if (ds) { var adv = ds._advSearch || (ds._advSearch = {}); adv[property] = adv[property] || initialValue; Object.defineProperty(scope, property, { get: function () { return adv[property]; }, set: function (value) { adv[property] = value; } }); } } ui.directive('uiFilterItem', function() { return { replace: true, require: '^uiFilterForm', scope: { fields: "=", filter: "=", model: "=" }, link: function(scope, element, attrs, form) { function getOperators() { if (element.is(':hidden')) { return; } var filter = scope.filter || {}; if (filter.type === undefined || !filter.field) { return []; } var field = scope.fields[filter.field] || {}; var operators = filter.selectionList ? OPERATORS_BY_TYPE["enum"] || [] : OPERATORS_BY_TYPE[filter.type] || []; if (field.target && !field.targetName) { operators = ["isNull", "notNull"]; } return _.map(operators, function(name) { return { name: name, title: OPERATORS[name] }; }); } scope.remove = function(filter) { form.removeFilter(filter); }; scope.canShowSelect = function () { return scope.filter && scope.filter.selectionList && scope.filter.operator && !( scope.filter.operator == 'isNull' || scope.filter.operator == 'notNull'); }; scope.canShowTags = function() { return scope.filter && ['many-to-one', 'one-to-one', 'many-to-many'].indexOf(scope.filter.type) > -1 && ( scope.filter.operator == 'in' || scope.filter.operator == 'notNot'); }; scope.canShowInput = function() { return scope.filter && !scope.canShowSelect() && !scope.canShowTags() && scope.filter.operator && !( scope.filter.type == 'boolean' || scope.filter.operator == 'isNull' || scope.filter.operator == 'notNull'); }; scope.canShowRange = function() { return scope.filter && ( scope.filter.operator === 'between' || scope.filter.operator === 'notBetween'); }; scope.getSelection = function () { if (!scope.canShowSelect()) return []; var field = (scope.fields||{})[scope.filter.field] || {}; return field.selectionList || []; }; scope.onFieldChange = function() { var filter = scope.filter, field = scope.fields[filter.field] || {}; filter.type = field.type || 'string'; filter.selectionList = field.selectionList; filter.value = undefined; filter.value2 = undefined; if (field.type === 'many-to-one' || field.type === 'one-to-one') { filter.targetName = field.targetName; } else { filter.targetName = null; } }; scope.onOperatorChange = function() { setTimeout(function() { scope.$parent.$parent.$parent.doAdjust(); }); }; scope.$watch('filter.field', function searchFilterFieldWatch(value, old) { scope.operators = getOperators(); }); scope.$on('on:show-menu', function () { scope.operators = getOperators(); }); scope.getOptions = function () { var all = []; var data = scope.$parent.contextData || {}; var field = data.field || {}; _.each(scope.options, function (item) { var name = field.name; if (name && item.name === name && data.value) { return; } if (item.contextField && !(item.contextField === name && item.contextFieldValue === data.value)) { return; } all.push(item); }); return all; }; var unwatch = scope.$watch('fields', function searchFiledsWatch(fields, old) { if (_.isEmpty(fields)) return; unwatch(); var options = _.values(fields); scope.options = _.sortBy(options, function (x) { return (x.title||'').toLowerCase(); }); }, true); }, template: "
" + "
" + "
" + "" + "
" + "
" + "" + " " + "" + "" + " "+ "" + "" + "" + "" + "" + "
" + "
" + "" + " " + "" + "" + " " + "" + "
" + "
" + "
" }; }); ui.directive('uiFilterTags', function() { return { scope: { filter: '=', fields: '=', model: '=' }, controller: ['$scope', '$element', 'DataSource', 'ViewService', function ($scope, $element, DataSource, ViewService) { var filter = $scope.filter || {}; var fields = $scope.fields || {}; var field = _.extend({}, fields[filter.field], { required: false, readonly: false, widget: 'tag-select', showTitle: false, canNew: false, canEdit: false, colSpan: 12 }); var schema = { cols: 1, type: 'form', items: [{ type: 'panel', items: [field] }] }; $scope._viewParams = { model: $scope.model, views: [schema], fields: fields }; ui.ViewCtrl($scope, DataSource, ViewService); ui.FormViewCtrl.call(this, $scope, $element); $scope.schema = schema; $scope.schema.loaded = true; $scope.$watch('record.' + filter.field, function searchFilterFieldWatch(value) { filter.value = _.pluck(value, 'id'); }); function fetchValues(value) { $scope._dataSource._new(field.target).search({ fields: _.compact([field.targetName]), domain: 'self.id in (:ids)', context: { ids: value } }).success(function (records) { var record = {}; var names = filter.field.split('.'); var rec = record; while (names.length > 1) { rec = rec[names.shift()] = {}; } rec[names[0]] = records; $scope.edit(record); }); } $scope.defaultValues = {}; $scope.editRecord = function (record) { if (record && record !== $scope.defaultValues) { $scope.record = record; } }; $scope.setEditable(); $scope.show(); if (_.isArray(filter.value) && filter.value.length) { fetchValues(filter.value); } }], template: "
" }; }); ui.directive('uiFilterInput', function() { return { require: '^ngModel', link: function(scope, element, attrs, model) { var picker = null; var pattern = /^(\d{2}\/\d{2}\/\d{4})$/; var isopattern = /^(\d{4}-\d{2}-\d{2}T.*)$/; var options = { dateFormat: 'dd/mm/yy', showButtonsPanel: false, showTime: false, showOn: null, onSelect: function(dateText, inst) { var value = picker.datepicker('getDate'); var isValue2 = _.str.endsWith(attrs.ngModel, 'value2'); value = isValue2 ? moment(value).endOf('day').toDate() : moment(value).startOf('day').toDate(); model.$setViewValue(value.toISOString()); }, onClose: function (dateText, inst) { picker.datepicker('destroy'); picker = null; } }; model.$formatters.push(function(value) { if (_.isDate(value)) { value = moment(value).format('DD/MM/YYYY'); } return value; }); model.$parsers.push(function(value) { if (/^date/.test(scope.filter.type)) { if (isopattern.test(value)) { return value; } else if (pattern.test(value)) { var isValue2 = _.str.endsWith(attrs.ngModel, 'value2'); return isValue2 ? moment(value, 'DD/MM/YYYY').endOf('day').toDate() : moment(value, 'DD/MM/YYYY').startOf('day').toDate(); } return null; } return value; }); model.$parsers.push(function(value) { var type = scope.filter.type; if (!(type == 'date' || type == 'datetime') || isDate(value)) { return value; } return toMoment(value).toDate(); }); function isDate(value) { if (value === null || value === undefined) return true; if (_.isDate(value)) return true; if (/\d+-\d+-\d+T/.test(value)) return true; } function toMoment(value) { var format = null; if (/\d+\/\d+\/\d+/.test(value)) format = 'DD/MM/YYYY'; if (/\d+\/\d+\/\d+\s+\d+:\d+/.test(value)) format = 'DD/MM/YYYY HH:mm'; if (format === null) { return moment(); } return moment(value, format); } element.focus(function(e) { var type = scope.filter.type; if (!(type == 'date' || type == 'datetime')) { return; } picker = picker || element.datepicker(options); picker.datepicker('show'); }); element.on('$destroy', function() { if (picker) { picker.datepicker('destroy'); picker = null; } }); } }; }); ui.directive('uiFilterContext', function () { return { scope: { fields: '=', context: '=' }, controller: ['$scope', function ($scope) { $scope.field = { name: 'contextValue', evalTarget: 'context.field.target', evalTargetName: 'context.field.targetName', evalValue: 'context.value', evalTitle: 'context.title' }; $scope.getViewDef = function () { return $scope.field; }; $scope.remove = function () { var context = {}; var fields = $scope.contextFields || []; if (fields.length === 1) { context.field = fields[0]; } $scope.context = context; }; $scope.$watch('context.field.name', function searchContextFieldWatch(name) { if (!name) { $scope.remove(); } }); $scope.onFields = function (fields) { var contextFields = {}; for (var item in fields) { var field = fields[item]; var name = field.contextField; if (name && fields[name] && !contextFields[name]) { contextFields[name] = fields[name]; } } $scope.contextFields = _.sortBy(_.values(contextFields), function (x) { return (x.title||'').toLowerCase(); }); $scope.remove(); }; }], link: function (scope, element, attrs) { var unwatch = scope.$watch('fields', function searchContextFieldsWatch(fields) { if (_.isEmpty(fields)) return; unwatch(); scope.onFields(fields); }, true); }, template: "
" + "
" + "
" + "" + "
" + "
" + "" + "" + "" + "" + "" + "" + "
" + "
" + "
" }; }); FilterFormCtrl.$inject = ['$scope', '$element', 'ViewService']; function FilterFormCtrl($scope, $element, ViewService) { this.doInit = function(model, viewItems) { var context = $scope.$parent.$parent._context || {}; return ViewService .getFields(model, context.jsonModel) .success(function(fields, jsonFields) { var items = {}; var nameField = null; _.each(fields, function(field, name) { if (field.name === 'id' || field.name === 'version' || field.name === 'archived' || field.name === 'selected') return; if (field.type === 'binary' || field.large || field.encrypted) return; if (field.nameColumn) { nameField = name; } items[name] = field; }); // include json fields _.each(jsonFields, function (fields, prefix) { _.each(fields, function (field, name) { if (['button', 'panel', 'separator', 'many-to-many'].indexOf(field.type) > -1) return; var key = prefix + '.' + name; if (field.type !== 'many-to-one') { key += '::' + (field.jsonType || 'text'); } items[key] = _.extend({}, field, { name: key, title: (field.title || field.autoTitle) + " (" + items[prefix].title + ")" }); }); // don't search parent delete items[prefix]; }); if (!nameField) { nameField = (fields.name || {}).name; } _.each(viewItems, function (item) { if (item.hidden) { delete items[item.name]; } else { items[item.name] = item; } }); var contextFieldNames = []; for (var item in items) { var field = items[item]; var name = field.contextField; if (name && items[name] && contextFieldNames.indexOf(name) === -1) { contextFieldNames.push(name); } } $scope.fields = items; $scope.contextFieldNames = contextFieldNames; $scope.$parent.fields = $scope.fields; $scope.$parent.contextFieldNames = $scope.contextFieldNames; $scope.$parent.nameField = nameField || ($scope.fields.name ? 'name' : null); }); }; $scope.fields = {}; sharedProperty($scope, $scope.$parent.handler, 'filters', [{ $new: true }]); sharedProperty($scope, $scope.$parent.handler, 'operator', 'and'); sharedProperty($scope, $scope.$parent.handler, 'showArchived', false); var handler = $scope.$parent.handler; if (handler && handler._dataSource) { $scope.showArchived = handler._dataSource._showArchived; } $scope.addFilter = function(filter) { var last = _.last($scope.filters); if (last && !(last.field && last.operator)) return; $scope.filters.push(filter || { $new: true }); }; this.removeFilter = function(filter) { var index = $scope.filters.indexOf(filter); if (index > -1) { $scope.filters.splice(index, 1); } if ($scope.filters.length === 0) { $scope.addFilter(); } }; $scope.$on('on:select-custom', function(e, custom) { $scope.filters.length = 0; $scope.contextData = {}; if (custom.$selected) { select(custom); } else { $scope.addFilter(); } return $scope.applyFilter(); }); $scope.$on('on:select-domain', function(e, filter) { $scope.filters.length = 0; $scope.addFilter(); return $scope.applyFilter(); }); $scope.$on('on:before-save', function(e, data) { var criteria = $scope.prepareFilter(); if (data) { data.criteria = criteria; } }); $scope.$on('on:clear-filter', function (e, options) { $scope.clearFilter(options); }); function select(custom) { var criteria = custom.criteria; var filters = criteria.criteria; var contextFieldNames = $scope.contextFieldNames || []; if (filters && filters.length && filters.length < 3) { var first = _.first(filters); var last = _.last(filters); var name = first.fieldName.replace('.id', ''); if (contextFieldNames.indexOf(name) > -1) { $scope.contextData = { field: $scope.fields[name], value: first.value, title: first.title, saved: true }; filters = (last||{}).criteria || [{}]; } } $scope.operator = criteria.operator || 'and'; _.each(filters, function(item) { var fieldName = item.fieldName || ''; if (fieldName && $scope.fields[fieldName] === undefined && fieldName.indexOf('.') > -1 && fieldName.indexOf('::') === -1) { fieldName = fieldName.substring(0, fieldName.lastIndexOf('.')); } var field = $scope.fields[fieldName] || {}; var filter = { field: fieldName, value: item.value, value2: item.value2 }; filter.type = field.type || 'string'; filter.operator = item.operator; if (field.selectionList) { filter.selectionList = field.selectionList; } if (item.operator === '=' && filter.value === true) { filter.operator = 'true'; } if (filter.operator === '=' && filter.value === false) { filter.operator = 'false'; } if (field.type === 'date' || field.type === 'datetime') { if (filter.value) { filter.value = moment(filter.value).toDate(); } if (filter.value2) { filter.value2 = moment(filter.value2).toDate(); } } if (filter.type == 'many-to-one' || field.type === 'one-to-one') { filter.targetName = field.targetName; } $scope.addFilter(filter); }); } $scope.clearFilter = function(options) { $scope.filters.length = 0; $scope.showArchived = false; $scope.addFilter(); $scope.contextData = {}; if ($scope.$parent.onClear) { $scope.$parent.onClear(); } var hide = options === true; var silent = !hide && options && options.silent; if (!silent) { $scope.applyFilter(); } if ($scope.$parent && hide) { $scope.$parent.$broadcast('on:hide-menu'); } }; $scope.prepareFilter = function() { var criteria = { archived: $scope.showArchived, operator: $scope.operator, criteria: [] }; _.each($scope.filters, function(filter) { if (!filter.field || !filter.operator) { return; } var criterion = { fieldName: filter.field, operator: filter.operator, value: filter.value }; if (filter.operator == 'like' || filter.operator == 'notLike') { criterion.value = criterion.value || ''; } if (filter.operator == 'in' || filter.operator == 'notIn') { if (_.isEmpty(filter.value)) { return; } criterion.fieldName += '.id'; } else if (filter.targetName && criterion.fieldName.indexOf(':') == -1 && ( filter.operator !== 'isNull' || filter.operator !== 'notNull')) { criterion.fieldName += '.' + filter.targetName; } else if (/-many/.test(filter.type) && ( filter.operator !== 'isNull' || filter.operator !== 'notNull')) { criterion.fieldName += '.id'; } if (criterion.operator == "true") { criterion.operator = "="; criterion.value = true; } if (criterion.operator == "false") { criterion = { operator: "or", criteria: [ { fieldName: filter.field, operator: "=", value: false }, { fieldName: filter.field, operator: "isNull" } ] }; } if (criterion.operator == "between" || criterion.operator == "notBetween") { criterion.value2 = filter.value2; } if (filter.$new) { criterion.$new = true; } criteria.criteria.push(criterion); }); var contextData = $scope.contextData || {}; if (contextData.value && contextData.field && contextData.field.name) { var previous = criteria.criteria; var operator = criteria.operator; criteria.operator = "and"; criteria.criteria = [{ fieldName: contextData.field.name + ".id", operator: "=", value: contextData.value, title: contextData.title, $new: !contextData.saved }]; if (previous && previous.length) { criteria.criteria.push({ operator: operator, criteria: previous }); } } return criteria; }; var appliedFilters = false; var appliedContext = false; $scope.$watch('filters', function (fitlers, old) { appliedFilters = fitlers === old; }, true); $scope.$watch('contextData', function (data, old) { appliedContext = data === old; }, true); $scope.applyFilter = function(hide) { var criteria = $scope.prepareFilter(); var promise; if ($scope.$parent.onFilter) { promise = $scope.$parent.onFilter(criteria); } if ($scope.$parent && hide) { $scope.$parent.$broadcast('on:hide-menu'); } handler.$broadcast('on:advance-filter', criteria); handler.$broadcast('on:context-field-change', $scope.contextData); appliedFilters = true; appliedContext = true; return promise; }; $scope.canExport = function(full) { var allowFull = axelor.config["view.adv-search.export.full"] !== false; var handler = $scope.$parent.handler; if (handler && handler.hasPermission) { return full ? allowFull && handler.hasPermission('export') : handler.hasPermission('export'); } return true; }; $scope.onExport = function(full) { var handler = $scope.$parent.handler; if (handler && handler.onExport) { var promise = appliedFilters && appliedContext ? null : $scope.applyFilter(true); if (promise && promise.then) { promise.then(function () { handler.onExport(full); }); } else { handler.onExport(full); } } }; } ui.directive('uiFilterForm', function() { return { replace: true, scope: { model: '=', onSearch: '&' }, controller: FilterFormCtrl, link: function(scope, element, attrs, ctrl) { var unwatch = scope.$watch("$parent.viewItems", function searchViewItemsWatch(items) { if (items === undefined) return; unwatch(); ctrl.doInit(scope.model, items); }); }, template: "
" + "
" + "
" + "" + "" + "" + "
" + "
" + "