Files
ERP/sophal/js/widget/widget.search.js

1574 lines
47 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');
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:
"<div class='flex-layout'>" +
"<div class='flex-row'>" +
"<div class='flex-item filter-remove'>" +
"<a href='' ng-click='remove(filter)'><i class='fa fa-times'></i></a>" +
"</div>" +
"<div class='flex-item filter-inputs'>" +
"<span>" +
"<select ng-model='filter.field' ng-options='v.name as v.title for v in getOptions()' ng-change='onFieldChange()' class='input-medium'></select> " +
"</span>" +
"<span>" +
"<select ng-model='filter.operator' ng-options='o.name as o.title for o in operators' ng-change='onOperatorChange()' class='input-medium'></select> "+
"</span>" +
"<span ng-show='canShowSelect()'>" +
"<select ng-model='filter.value' class='input=medium' ng-options='o.value as o.title for o in getSelection()'></select>" +
"</span>" +
"<span ng-if='canShowTags()'>" +
"<div ui-filter-tags x-filter='filter' x-model='model' x-fields='fields'></div>" +
"</span>" +
"<span ng-show='canShowInput()'>" +
"<input type='text' ui-filter-input ng-model='filter.value' class='input-medium'> " +
"</span>" +
"<span ng-show='canShowRange()'>" +
"<input type='text' ui-filter-input ng-model='filter.value2' class='input-medium'> " +
"</span>" +
"</div>" +
"</div>" +
"</div>"
};
});
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:
"<div ui-view-form x-handler='true'></div>"
};
});
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:
"<div class='flex-layout filter-context' ng-show='contextFields.length'>" +
"<div class='flex-row'>" +
"<div class='flex-item filter-remove'>" +
"<a href='' ng-click='remove()'><i class='fa fa-times'></i></a>" +
"</div>" +
"<div class='flex-item filter-inputs'>" +
"<span>" +
"<select ng-model='context.field' ng-options='v as v.title for v in contextFields'></select>" +
"</span>" +
"<span>" +
"<span ui-eval-ref-select ng-model='context.value' x-field='contextValue'></span>" +
"</span>" +
"</div>" +
"</div>" +
"</div>"
};
});
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:
"<div class='filter-form'>" +
"<div ui-filter-context fields='fields' context='contextData'></div>" +
"<form class='filter-operator form-inline'>" +
"<label class='radio inline'>" +
"<input type='radio' name='operator' ng-model='operator' value='and' x-translate><span x-translate>and</span>" +
"</label>" +
"<label class='radio inline'>" +
"<input type='radio' name='operator' ng-model='operator' value='or' x-translate><span x-translate>or</span>" +
"</label>" +
"<label class='checkbox inline show-archived'>" +
"<input type='checkbox' ng-model='showArchived'><span x-translate>Show archived</span>" +
"</label>" +
"</form>" +
"<div ng-repeat='filter in filters' ui-filter-item x-model='model' x-fields='fields' x-filter='filter'></div>" +
"<div class='links'>"+
"<a href='' ng-click='addFilter()' x-translate>Add filter</a>"+
"<span class='divider'>|</span>"+
"<a href='' ng-click='clearFilter(true)' x-translate>Clear</a></li>"+
"<span class='divider' ng-if='canExport()'>|</span>"+
"<a href='' ng-if='canExport()' ng-click='onExport()' x-translate>Export</a></li>"+
"<span class='divider' ng-if='canExport()'>|</span>"+
"<a href='' ng-if='canExport(true)' ng-click='onExport(true)' x-translate>Export full</a></li>"+
"<span class='divider' ng-if='canExport(true)'>|</span>"+
"<a href='' ng-click='applyFilter(true)' x-translate>Apply</a></li>"+
"<div>"+
"</div>"
};
});
ui.directive('uiFilterBox', function() {
return {
scope: {
handler: '='
},
controller: ['$scope', 'ViewService', 'DataSource', function($scope, ViewService, DataSource) {
var handler = $scope.handler,
params = (handler._viewParams || {}).params;
var filterView = params ? params['search-filters'] : null;
var filterDS = DataSource.create('com.axelor.meta.db.MetaFilter');
this.$scope = $scope;
$scope.model = handler._model;
$scope.view = {};
sharedProperty($scope, $scope.handler, 'viewFilters', []);
sharedProperty($scope, $scope.handler, 'custFilters', []);
sharedProperty($scope, $scope.handler, 'tagItems', []);
$scope.canShare = axelor.config["view.adv-search.share"] !== false;
if (filterView) {
ViewService.getMetaDef($scope.model, {name: filterView, type: 'search-filters'})
.success(function(fields, view) {
var viewItems = _.map(view.items, function (item) {
var field = fields[item.name] || {};
return _.extend({}, field, item, { type: field.type });
});
$scope.view = view;
$scope.viewItems = viewItems;
$scope.viewFilters = angular.copy(view.filters);
});
} else {
$scope.viewItems = [];
filterView = 'act:' + (handler._viewParams || {}).action;
}
if (filterView) {
filterDS.rpc('com.axelor.meta.web.MetaFilterController:findFilters', {
model: 'com.axelor.meta.db.MetaFilter',
context: {
filterView: filterView
}
}).success(function(res) {
_.each(res.data, function(item) {
acceptCustom(item);
});
});
}
var current = {
criteria: {},
domains: [],
customs: []
};
function acceptCustom(filter) {
var custom = {
title: filter.title,
name: filter.name,
shared: filter.shared,
criteria: angular.fromJson(filter.filterCustom)
};
custom.selected = filter.filters ? filter.filters.split(/\s*,\s*/) : [];
custom.selected = _.map(custom.selected, function(x) {
return parseInt(x);
});
var found = _.findWhere($scope.custFilters, {name: custom.name});
if (found) {
_.extend(found, custom);
} else {
$scope.custFilters.push(custom);
}
return found ? found : custom;
}
$scope.selectFilter = function(filter, isCustom, live) {
var selected = live ? !filter.$selected : filter.$selected;
var selection = isCustom ? current.customs : current.domains;
var applyAll = (handler.schema||{}).customSearch === false;
if (live) {
$scope.onClear();
}
filter.$selected = selected;
var index = selection.indexOf(filter);
if (selected) {
selection.push(filter);
}
if (!selected && index > -1) {
selection.splice(index, 1);
}
if (isCustom && (live || applyAll)) {
$scope.hasCustSelected = filter.$selected && !applyAll;
$scope.custName = filter.$selected ? filter.name : null;
$scope.oldCustTitle = filter.$selected ? filter.title : '';
$scope.custTitle = filter.$selected ? filter.title : '';
$scope.custShared = filter.$selected ? filter.shared : false;
return $scope.$broadcast('on:select-custom', filter, selection);
}
if (live || applyAll) {
$scope.$broadcast('on:select-domain', filter);
}
};
$scope.isSelected = function(filter) {
return filter.$selected;
};
$scope.onRefresh = function() {
if (this.custTerm) {
return this.onFreeSearch();
}
handler.onRefresh();
};
$scope.hasFilters = function(which) {
if (which === 1) {
return this.viewFilters && this.viewFilters.length;
}
if (which === 2) {
return this.custFilters && this.custFilters.length;
}
return (this.viewFilters && this.viewFilters.length) ||
(this.custFilters && this.custFilters.length);
};
$scope.canSaveNew = function() {
if ($scope.hasCustSelected && $scope.oldCustTitle && $scope.custTitle) {
return !angular.equals(_.underscored($scope.oldCustTitle), _.underscored($scope.custTitle));
}
return false;
};
$scope.onSave = function(saveAs) {
var data = { criteria: null };
$scope.$broadcast('on:before-save', data);
var title = _.trim($scope.custTitle),
name = $scope.custName || _.underscored(title);
if (saveAs) {
name = _.underscored(title);
}
var selected = [];
_.each($scope.viewFilters, function(item, i) {
if (item.$selected) selected.push(i);
});
var custom = data.criteria || {};
custom = _.extend({
operator: custom.operator,
criteria: custom.criteria
});
var value = {
name: name,
title: title,
shared: $scope.custShared,
filters: selected.join(', '),
filterView: filterView,
filterCustom: angular.toJson(custom)
};
filterDS.rpc('com.axelor.meta.web.MetaFilterController:saveFilter', {
model: 'com.axelor.meta.db.MetaFilter',
context: value
}).success(function(res) {
var custom = acceptCustom(res.data);
custom.$selected = false;
$scope.selectFilter(custom, true, true);
});
};
$scope.onDelete = function() {
var name = $scope.custName;
if (!$scope.hasCustSelected || !name) {
return;
}
function doDelete() {
filterDS.rpc('com.axelor.meta.web.MetaFilterController:removeFilter', {
model: 'com.axelor.meta.db.MetaFilter',
context: {
name: name,
filterView: filterView
}
}).success(function(res) {
var found = _.findWhere($scope.custFilters, {name: name});
if (found) {
$scope.custFilters.splice($scope.custFilters.indexOf(found), 1);
$scope.hasCustSelected = false;
$scope.custName = null;
$scope.custTitle = null;
$scope.custShared = false;
}
$scope.onFilter();
});
}
axelor.dialogs.confirm(_t("Would you like to remove the filter?"), function(confirmed){
if (confirmed) {
doDelete();
}
});
};
$scope.onClear = function() {
_.each($scope.viewFilters, function(d) { d.$selected = false; });
_.each($scope.custFilters, function(d) { d.$selected = false; });
current.domains.length = 0;
current.customs.length = 0;
$scope.hasCustSelected = false;
$scope.custName = null;
$scope.oldCustTitle = null;
$scope.custTitle = null;
$scope.custShared = false;
$scope.custTerm = null;
$scope.tagItems = [];
if ($scope.handler && $scope.handler.clearFilters) {
$scope.handler.clearFilters();
}
};
$scope.onFilter = function(criteria) {
if (criteria) {
current.criteria = criteria;
} else {
criteria = current.criteria;
}
var search = _.extend({}, criteria);
if (!search.criteria) {
search.operator = 'and';
search.criteria = [];
} else {
search.criteria = _.clone(search.criteria);
}
if (arguments.length > 1 && _.isString(arguments[1])) {
search._searchText = arguments[1];
}
var domains = [],
customs = [];
_.each(current.domains, function(domain) {
domains.push(domain);
});
_.each(current.customs, function(custom) {
if($scope.hasCustSelected) { return; }
if (custom.criteria && custom.criteria.criteria) {
customs.push({
operator: custom.criteria.operator || 'and',
criteria: custom.criteria.criteria
});
}
_.each(custom.selected, function(i) {
var domain = $scope.viewFilters[i];
if (domains.indexOf(domain) == -1) {
domains.push(domain);
}
});
});
if (customs.length > 0) {
search.criteria.push({
operator: criteria.operator || 'and',
criteria: customs
});
}
search._domains = domains;
search.criteria = process(search.criteria);
// process criteria for datetime fields, always use between operator
function process(filter) {
if (_.isArray(filter)) return _.map(filter, process);
if (_.isArray(filter.criteria)) {
filter.criteria = process(filter.criteria);
return filter;
}
var name = filter.fieldName;
var type = (($scope.fields||$scope.$parent.fields||{})[filter.fieldName]||{}).type;
var v1 = filter.value;
var v2 = filter.value2;
// if json date/datetime field
if (name.indexOf('::') > -1 && (type === 'date' || type === 'datetime')) {
filter = _.extend({}, filter);
switch (filter.operator) {
case '>':
filter.value = moment(v1).endOf('day').toDate();
filter.value2 = undefined;
break;
case '<':
filter.value = moment(v1).startOf('day').toDate();
filter.value2 = undefined;
break;
case '=':
filter.operator = 'between';
filter.value = moment(v1).startOf('day').toDate();
filter.value2 = moment(v1).endOf('day').toDate();
break;
case '!=':
filter.operator = 'notBetween';
filter.value = moment(v1).startOf('day').toDate();
filter.value2 = moment(v1).endOf('day').toDate();
break;
case 'between':
case 'notBetween':
filter.value = moment(v1).startOf('day').toDate();
filter.value2 = moment(v2).endOf('day').toDate();
break;
}
return filter;
}
if (filter.operator !== '=') return filter;
if (type != 'datetime') return filter;
if (!(/\d+-\d+\d+T/.test(filter.value) || _.isDate(filter.value))) {
return filter;
}
v1 = moment(v1).startOf('day').toDate();
v2 = moment(v1).endOf('day').toDate();
return _.extend({}, filter, {
operator: 'between',
value: v1,
value2: v2
});
}
function countCustom(criteria) {
var n = _.filter(criteria, function (item) { return item.$new; }).length;
if (criteria.length === 2 && criteria[1].criteria) {
n += countCustom(criteria[1].criteria);
}
return n;
}
var tag = {};
var all = _.chain([$scope.viewFilters, $scope.custFilters])
.flatten()
.filter(function (item) {
return item && item.$selected;
})
.pluck('title')
.value();
var nCustom = countCustom((criteria||{}).criteria);
if (nCustom > 0) {
all.push(_t('Custom ({0})', nCustom));
}
if (all.length === 1) {
tag.title = all[0];
}
if (all.length > 1) {
tag.title = _t('Filters ({0})', all.length);
tag.help = all.join(', ');
}
if (all.length === 0) {
$scope.tagItems = [];
} else {
$scope.tagItems = [tag];
}
return handler._simpleFilters
? handler.filter(handler._simpleFilters, search)
: handler.filter(search);
};
$scope.onFreeSearch = function() {
var filters = [],
fields = {},
text = this.custTerm,
number = +(text);
var freeSearch = handler.schema && handler.schema.freeSearch;
var freeCols = freeSearch && freeSearch !== 'all' ? freeSearch.split(/\s*,\s*/) : [];
var cols = freeCols.length === 0 ? $scope.findCols() : freeCols;
text = text ? text.trim() : null;
fields = _.pick(this.$parent.fields, cols);
if (text) {
for(var name in fields) {
var fieldName = null,
operator = "like",
value = text;
var field = fields[name];
if (field.transient) continue;
switch (field.type) {
case 'integer':
case 'decimal':
if (_.isNaN(number) || !_.isNumber(number)) continue;
if (field.type === 'integer' && (number > 2147483647 || number < -2147483648)) continue;
fieldName = name;
operator = '=';
value = number;
break;
case 'text':
case 'string':
fieldName = name;
break;
case 'one-to-one':
case 'many-to-one':
if (field.jsonField) {
fieldName = name;
} else if (field.targetName) {
fieldName = name + '.' + field.targetName;
}
break;
case 'boolean':
if (/^(t|f|y|n|true|false|yes|no)$/.test(text)) {
fieldName = name;
operator = '=';
value = /^(t|y|true|yes)$/.test(text);
}
break;
}
if (!fieldName) continue;
filters.push({
fieldName: fieldName,
operator: operator,
value: value
});
}
}
var criteria = {
operator: 'or',
criteria: filters
};
this.onFilter(criteria, text);
};
}],
link: function(scope, element, attrs) {
var menu = element.children('.filter-menu'),
toggleButton = null;
scope.onSearch = function(e) {
if (menu && menu.is(':visible')) {
hideMenu();
return;
}
toggleButton = $(e.currentTarget);
// more than top navbar's zIndex 1030 to hide
menu.zIndex(element.parent().zIndex() + 1031);
menu.show();
scope.doAdjust();
$(document).on('mousedown.search-menu', onMouseDown);
scope.$applyAsync(function () {
scope.visible = true;
scope.$broadcast('on:show-menu');
});
};
scope.doAdjust = (function() {
var opts = {
my: "left top",
at: "left bottom",
of: element,
collision: "fit"
};
if (element.hasClass('pull-right')) {
opts.my = "right top";
opts.at = "right bottom";
}
return function() {
menu.position(opts);
};
}());
scope.onClearFilter = function () {
hideMenu();
scope.visible = true;
scope.$broadcast('on:clear-filter');
scope.$timeout(function () {
scope.visible = false;
});
};
// append menu to body to fix overlaping issue
scope.$timeout(function() {
menu.zIndex(element.zIndex() + 1);
menu.appendTo("body");
});
element.on('keydown.search-query', '.search-query', function(e) {
if (e.keyCode === 13) { // enter
scope.onFreeSearch();
}
});
scope.$on('on:hide-menu', function () {
hideMenu();
});
scope.$on('on:clear-filter-silent', function () {
var visible = scope.visible;
scope.visible = true;
scope.$broadcast('on:clear-filter', { silent: true });
scope.$timeout(function () {
scope.visible = visible;
});
});
function hideMenu() {
$(document).off('mousedown.search-menu', onMouseDown);
scope.$timeout(function () {
scope.visible = false;
});
menu.hide();
}
function onMouseDown(e) {
var all = $(menu).add(toggleButton);
if (all.is(e.target) || all.has(e.target).length > 0) {
return;
}
if ($(e.target).zIndex() > $(menu).zIndex() || $(e.target).parents('.ui-dialog').length) {
return;
}
if(menu) {
hideMenu();
}
}
scope.hideMenu = hideMenu;
scope.findCols = function () {
var grid = element.parents('.grid-view:first').children('[ui-slick-grid]:first').data('grid');
return grid
? _.pluck(grid.getColumns(), 'field').filter(function (n) { return n in scope.$parent.fields; })
: _.pluck(scope.$parent.fields, 'name');
};
scope.handler.$watch('schema.freeSearch', function searchFreeSearchWatch(value, old) {
if (value === 'none') {
var input = element.find('input:first')
.addClass('not-readonly')
.prop('readonly', true)
.click(scope.onSearch.bind(scope));
}
});
element.on('$destroy', function() {
$(document).off('mousedown.search-menu', onMouseDown);
if (menu) {
menu.remove();
menu = null;
}
});
},
replace: true,
template:
"<div class='filter-box'>" +
"<div class='tag-select picker-input search-query'>" +
"<ul>" +
"<li class='tag-item label label-primary' ng-repeat='item in tagItems'>" +
"<span class='tag-text' title='{{item.help}}'>{{item.title}}</span> " +
"<i class='fa fa-times fa-small' ng-click='onClearFilter()'></i>" +
"</li>" +
"<li class='tag-selector' ng-show='!tagItems.length'>" +
"<input type='text' autocomplete='off' ng-model='custTerm'>" +
"</li>" +
"</ul>" +
"<span class='picker-icons'>" +
"<i ng-click='onSearch($event)' class='fa fa-caret-down'></i>"+
"<i ng-click='onRefresh()' class='fa fa-search'></i>" +
"</span>" +
"</div>" +
"<div class='filter-menu'>" +
"<span>" +
"<strong x-translate>Advanced Search</strong>" +
"<a href='' class='pull-right' ng-click='hideMenu()'><i class='fa fa-times'></i></a>" +
"</span>" +
"<hr>"+
"<div class='filter-list'>" +
"<dl ng-show='!hasFilters() && handler.schema.customSearch == false' style='display: hidden;'>" +
"<dd><span x-translate>No filters available</span></dd>" +
"</dl>" +
"<dl ng-show='hasFilters(1)'>" +
"<dt><i class='fa fa-floppy-o'></i><span x-translate> Filters</span></dt>" +
"<dd ng-repeat='filter in viewFilters' class='checkbox'>" +
"<input type='checkbox' " +
"ng-model='filter.$selected' " +
"ng-click='selectFilter(filter, false, false)' ng-disabled='hasCustSelected'> " +
"<a href='' ng-click='selectFilter(filter, false, true)' ng-disabled='hasCustSelected'>{{filter.title}}</a>" +
"</dd>" +
"</dl>" +
"<dl ng-show='hasFilters(2)'>" +
"<dt><i class='fa fa-filter'></i><span x-translate> My Filters</span></dt>" +
"<dd ng-repeat='filter in custFilters' class='checkbox'>" +
"<input type='checkbox' " +
"ng-model='filter.$selected' " +
"ng-click='selectFilter(filter, true, false)' ng-disabled='hasCustSelected'> " +
"<a href='' ng-click='selectFilter(filter, true, true)' ng-disabled='!filter.$selected && hasCustSelected'>{{filter.title}}</a>" +
"</dd>" +
"</dl>" +
"</div>" +
"<div ng-hide='handler.schema.customSearch == false'>" +
"<hr ng-show='hasFilters()'>" +
"<div ui-filter-form x-model='model'></div>" +
"<hr>" +
"<div class='form-inline'>" +
"<div class='control-group'>" +
"<input type='text' placeholder='{{\"Save filter as\" | t}}' ng-model='custTitle'> " +
"<label class='checkbox' ng-show='canShare'>" +
"<input type='checkbox' ng-model='custShared'><span x-translate>Share</span>" +
"</label>" +
"</div>" +
"<button class='btn btn-small' ng-click='onSave()' ng-show='custTitle && !hasCustSelected'><span x-translate>Save</span></button> " +
"<button class='btn btn-small' ng-click='onSave()' ng-show='custTitle && hasCustSelected'><span x-translate>Update</span></button> " +
"<button class='btn btn-small' ng-click='onSave(true)' ng-show='canSaveNew()'><span x-translate>Save as</span></button> " +
"<button class='btn btn-small' ng-click='onDelete()' ng-show='hasCustSelected'><span x-translate>Delete</span></button>" +
"</div>" +
"</div>" +
"</div>" +
"</div>"
};
});
})();