/* * 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'); ui.controller('SearchViewCtrl', SearchViewCtrl); SearchViewCtrl.$inject = ['$scope', '$element', '$http', 'DataSource', 'ViewService', 'MenuService']; function SearchViewCtrl($scope, $element, $http, DataSource, ViewService, MenuService) { var view = $scope._views.search || {}; $scope._dataSource = DataSource.create('multi-search'); $scope.$applyAsync(function(){ if (view.deferred) view.deferred.resolve($scope); }); function fixFields(fields) { _.each(fields, function(field){ if (field.type == 'reference') { field.type = 'MANY_TO_ONE'; field.canNew = false; field.canEdit = false; } if (field.type) field.type = field.type.toUpperCase(); else field.type = 'STRING'; }); return fields; } $scope.show = function(viewPromise) { if (!viewPromise) { viewPromise = $scope.loadView('search', view.name); viewPromise.success(function(fields, schema){ $scope.initView(schema); }); } $scope.onShow(viewPromise); }; $scope.onShow = function() { }; $scope.initView = function(schema) { var params = $scope._viewParams; $scope._searchFields = fixFields(schema.searchFields); $scope._resultFields = fixFields(schema.resultFields); $scope._searchView = schema; $scope._showSingle = params.params && params.params.showSingle; $scope._forceEdit = params.params && params.params.forceEdit; $scope._hideActions = params.params && params.params.hideActions; $scope.updateRoute(); if (params.options && params.options.mode == "search") { $scope.setRouteOptions(params.options); } }; $scope.getRouteOptions = function() { var args = [], query = $scope._routeSearch; return { mode: 'search', args: args, query: query }; }; $scope._routeSearch = null; var onNewCalled = false; $scope.setRouteOptions = function(options) { var opts = options || {}, fields = $scope._searchFields || [], search = opts.search, record = {}; var changed = !angular.equals($scope._routeSearch, search); $scope._routeSearch = search; if (!onNewCalled && _.isEmpty(search)) { onNewCalled = true; scopes.form.$broadcast('on:new'); } if (!search || _.isEmpty(search) || !changed) { return $scope.updateRoute(); } _.each(fields, function(field) { var value = search[field.name]; if (value === undefined) { return; } if (field.target) { if (value) { record[field.name] = {id: +value}; } } else { record[field.name] = value; } }); if (search.objects) { scopes.toolbar.editRecord({ objectSelect: search.objects }); } scopes.form.editSearch(record, fields); function _doSearch() { var promise = $scope.doSearch(); if (promise && promise.then && $scope._showSingle) { promise.then(function () { var items = scopes.grid.getItems(); if (items && items.length === 1) { scopes.grid.selection = [0]; scopes.grid.onEdit(); } }); } } var promise = scopes.toolbar._viewPromise; if (promise && promise.then) { promise.then(function() { $scope.$timeout(_doSearch); }); } else { _doSearch(); } }; var scopes = {}; $scope._register = function(key, scope) { scopes[key] = scope; }; $scope.doSearch = function() { var params = _.extend({}, scopes.form.record), empty = _.chain(params).values().compact().isEmpty().value(); if (empty) return $scope.doClear(); var selected = (scopes.toolbar.record || {}).objectSelect; _.extend(params,{ __name: view.name, __selected: _.isEmpty(selected) ? null : selected.split(/,\s*/) }); var promise = $http.post('ws/search', { limit: view.limit || 80, data: params }); return promise.then(function(response){ var res = response.data, records = res.data || []; // slickgrid expects unique `id` so generate them and store original one _.each(records, function(rec, i){ rec._id = rec.id; rec.id = i + 1; }); scopes.grid.setItems(records); if (scopes.form.$events.onLoad) { scopes.form.record._count = records.length; scopes.form.record._countByModels = _.countBy(records, function(rec) { return rec._model; }); scopes.form.$events.onLoad(); } if (_.isEmpty(records)) { axelor.notify.info(_t("No records found.")); } }); }; $scope.doClear = function(all) { scopes.form.edit(null); scopes.form.$broadcast('on:new'); scopes.grid.setItems([]); if (all) { scopes.toolbar.edit(null); scopes.toolbar.doReset(); } }; $scope.doAction = function() { var action = scopes.toolbar.getMenuAction(); if (!action) { return; } var grid = scopes.grid, index = _.first(grid.selection), record = grid.getItem(index); action = action.action; record = _.extend({ _action: action }, record); record.id = record._id; MenuService.action(action).success(function(result){ if (!result.data) { return; } var view = result.data[0].view; var tab = view; tab.action = _.uniqueId('$act'); tab.viewType = 'form'; tab.context = _.extend({}, tab.context, { _ref : record }); $scope.openTab(tab); }); }; } ui.controller('SearchFormCtrl', SearchFormCtrl); SearchFormCtrl.$inject = ['$scope', '$element', 'ViewService']; function SearchFormCtrl($scope, $element, ViewService) { ui.FormViewCtrl.call(this, $scope, $element); $scope._register('form', $scope); $scope.setEditable(); // prevent requesting defaults $scope.defaultValues = {}; $scope.$watch('_searchView', function searchSchemaWatch(schema) { if (!schema) return; var form = { title: 'Search', type: 'form', cols: 1, items: [{ type: 'panel', title: schema.title, items: schema.searchFields }] }; var meta = { fields: schema.searchFields }; ViewService.process(meta, schema.searchForm); function process(item) { if (item.items || item.pages) { return _.each(item.items || item.pages, process); } switch (item.widget) { case 'ManyToOne': case 'OneToOne': case 'SuggestBox': item.canNew = false; item.canEdit = false; break; case 'OneToMany': case 'ManyToMany': case 'MasterDetail': item.hidden = true; } } if (schema.searchForm && schema.searchForm.items) { _.each(schema.searchForm.items, process); } $scope.fields = meta.fields; $scope.schema = schema.searchForm || form; $scope.schema.loaded = true; }); var model = null; var getContext = $scope.getContext; $scope.getContext = function() { var view = $scope._searchView || {}; if (model === null && view.selects) { model = (_.first(view.selects) || {}).model; } var ctx = getContext.apply(this, arguments) || {}; ctx._model = model; return ctx; }; $scope.editSearch = function (record, fields) { $scope.editRecord(record); setTimeout(function () { _.each(fields, function (field) { if (!field.target || !$scope.record) return; var item = $element.find('[x-field=' + field.name + ']'); var itemScope = item.data('$scope'); var value = itemScope.getValue(); if (value && itemScope && !itemScope.text && itemScope.select) { itemScope.select(value); } }); }); }; } ui.controller('SearchGridCtrl', SearchGridCtrl); SearchGridCtrl.$inject = ['$scope', '$element', 'ViewService', '$interpolate']; function SearchGridCtrl($scope, $element, ViewService, $interpolate) { ui.GridViewCtrl.call(this, $scope, $element); $scope._register('grid', $scope); var viewTitles = {}; $scope.$watch('_searchView', function searchSchemaWatch(schema) { if (!schema) return; var view = { title: 'Search', type: 'grid', editIcon: true, items: [] }; var objItem = _.findWhere(schema.resultFields, {name: 'object'}); if (!objItem) { view.items.push(objItem = {}); } objItem = _.extend(objItem, { name : '_modelTitle', title: _t('Object') }); view.items = view.items.concat(schema.resultFields); if (+(objItem.width) === 0) { objItem.hidden = true; } var meta = { fields: schema.resultFields }; ViewService.process(meta); _.each(schema.selects, function (select) { viewTitles[select.model] = select.viewTitle; }); _.each(view.items, function (item) { if (item.width) { objItem.width = objItem.width || 220; } }); $scope.fields = meta.fields; $scope.schema = view; $scope.schema.loaded = true; }); $scope.onEdit = function(force) { var index = _.first(this.selection), records = this.getItems(), record = this.getItem(index), ids, domain, views; ids = _.chain(records).filter(function(rec){ return rec._model == record._model; }).pluck('_id').value(); domain = "self.id IN (" + ids.join(',') + ")"; views = _.map(['form', 'grid'], function(type){ var view = { type : type }; var name = record["_" + type]; if (name) view.name = name; return view; }); if (force === undefined) { force = $scope._forceEdit; } var title = viewTitles[record._model]; if (title) { title = $interpolate(title)(record); } var tab = { action: _.uniqueId('$act'), model: record._model, title: title || record._modelTitle, forceTitle: true, domain: domain, recordId: record._id, forceEdit: force, viewType: 'form', views: views }; this.openTab(tab); }; $scope.onSort = function(event, args) { var grid = args.grid; var data = grid.getData(); var sortCols = args.sortCols; var types = {}; _.each($scope.fields, function (field) { types[field.name] = field.type; }); data.sort(function(dataRow1, dataRow2) { for (var i = 0, l = sortCols.length; i < l; i++) { var name = sortCols[i].sortCol.field; var sign = sortCols[i].sortAsc ? 1 : -1; var value1 = dataRow1[name], value2 = dataRow2[name]; switch (types[name]) { case "integer": case "long": value1 = value1 || 0; value2 = value2 || 0; break; default: value1 = value1 || ""; value2 = value2 || ""; } var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign; if (result) { return result; } } return 0; }); grid.invalidate(); grid.render(); }; } ui.controller('SearchToolbarCtrl', SearchToolbarCtrl); SearchToolbarCtrl.$inject = ['$scope', '$element', '$http']; function SearchToolbarCtrl($scope, $element, $http) { ui.FormViewCtrl.call(this, $scope, $element); $scope._register('toolbar', $scope); $scope.setEditable(); var menus = {}; function fetch(key, parent, request, response) { if (menus[key]) { return response(menus[key]); } var promise = $http.get('ws/search/menu', { params: { parent: parent } }); promise.then(function(res){ var data = res.data.data; data = _.map(data, function(item){ return { value: item.name, action: item.action, label: item.title }; }); menus[key] = data; response(data); }); } $scope.fetchRootMenus = function(request, response) { fetch('menuRoot', null, request, response); }; $scope.fetchSubMenus = function(request, response) { fetch('menuSub', $scope.record.menuRoot, request, response); }; $scope.fetchItemMenus = function(request, response) { fetch('menuItem', $scope.record.menuSub, request, response); }; $scope.resetSelector = function(a, b) { _.each(arguments, function(name){ if (_.isString(name)) { $scope.record[name] = null; menus[name] = null; } }); }; $scope.getMenuAction = function() { return _.find(menus.menuItem, function(item){ return item.value === $scope.record.menuItem; }); }; $scope.$watch('_searchView', function searchSchemaWatch(schema) { if (!schema) { return; } var selected = []; $scope.fields = { 'objectSelect' : { type : 'string', placeholder: _t('Search Objects'), multiple : true, selectionList : _.map(schema.selects, function(x) { if (x.selected) { selected.push(x.model); } return { value : x.model, title : x.title }; }) }, 'menuRoot' : { type : 'string', placeholder: _t('Action Category'), widget: 'select-query', attrs: { query: 'fetchRootMenus', 'ng-change': 'resetSelector("menuSub", "menuItem")' } }, 'menuSub' : { placeholder: _t('Action Sub-Category'), widget: 'select-query', attrs: { query: 'fetchSubMenus', 'ng-change': 'resetSelector($event, "menuItem")' } }, 'menuItem' : { placeholder: _t('Action'), widget: 'select-query', attrs: { query: 'fetchItemMenus' } } }; var items1 = [{ name : 'objectSelect', showTitle : false, colSpan: 8 }, { type : 'button', title : _t('Search'), colSpan: 2, attrs: { 'ng-click': 'doSearch()' } }, { type : 'button', title : _t('Clear'), colSpan: 2, attrs: { 'ng-click': 'doClear(true)' } }]; var items2 = [{ name : 'menuRoot', showTitle : false, colSpan: 3 }, { name : 'menuSub', showTitle : false, colSpan: 3 }, { name : 'menuItem', showTitle : false, colSpan: 4 }, { type : 'button', title : _t('Go'), colSpan: 2, attrs: { 'ng-click': 'doAction()' } }]; var item1 = { type: "panel", colSpan: $scope._hideActions ? 12 : 6, items: items1 }; var item2 = { type: "panel", colSpan: 6, items: items2 }; schema = { type : 'form', items: [{ type: 'panel', items: $scope._hideActions ? [item1] : [item1, item2] }] }; $scope.schema = schema; $scope.schema.loaded = true; $scope.doReset = function () { var record = $scope.record || {}; if (selected.length > 0 && _.isEmpty(record.objectSelect)) { record.objectSelect = selected.join(', '); $scope.edit(record); } }; $scope.$timeout($scope.doReset); }); } angular.module('axelor.ui').directive('uiViewSearch', function(){ return { controller: SearchViewCtrl, link: function(scope, element, attrs, ctrl) { element.on('keypress', '.search-view-form form:first', function(event){ if (event.keyCode == 13 && $(event.target).is('input')){ scope.doSearch(); } }); var grid = element.children('.search-view-grid'); scope.$onAdjust(function(){ if (!element.is(':visible')) return; grid.height(element.height() - grid.position().top); }); } }; }); // ActionSelector (TODO: re-use search view toolbar) ActionSelectorCtrl.$inject = ['$scope', '$element', '$attrs', '$http', 'MenuService']; function ActionSelectorCtrl($scope, $element, $attrs, $http, MenuService) { ui.FormViewCtrl.call(this, $scope, $element); var menus = {}, category = $attrs.category; function fetch(key, request, response, params) { if (menus[key]) { return response(menus[key]); } var promise = $http.get('ws/search/menu', { params: params }); promise.then(function(res){ var data = res.data.data; data = _.map(data, function(item){ return { value: item.name, action: item.action, label: item.title }; }); menus[key] = data; response(data); }); } $scope.fetchRootMenus = function(request, response) { fetch('$menuRoot', request, response, { parent: '', category: category }); }; $scope.fetchSubMenus = function(request, response) { if (!$scope.record.$menuRoot) return; fetch('$menuSub', request, response, { parent: $scope.record.$menuRoot }); }; $scope.fetchItemMenus = function(request, response) { if (!$scope.record.$menuSub) return; fetch('$menuItem', request, response, { parent: $scope.record.$menuSub }); }; $scope.resetSelector = function(a, b) { _.each(arguments, function(name){ if (_.isString(name)) { $scope.record[name] = null; menus[name] = null; } }); }; $scope.getMenuAction = function() { return _.find(menus.$menuItem, function(item){ return item.value === $scope.record.$menuItem; }); }; $scope.doAction = function() { var action = $scope.getMenuAction(); if (!action) { return; } var context = $scope.$parent.getContext(), record; action = action.action; record = { id : context.id, _action: action, _model: context._model }; MenuService.action(action).success(function(result){ if (!result.data) { return; } var view = result.data[0].view; var tab = view; tab.action = _.uniqueId('$act'); tab.viewType = 'form'; tab.context = _.extend({}, tab.context, { _ref : record }); $scope.openTab(tab); }); }; $scope.fields = { '$menuRoot' : { type : 'string', placeholder: _t('Action Category'), widget: 'select-query', attrs: { query: 'fetchRootMenus', 'ng-change': 'resetSelector("$menuSub", "$menuItem")' } }, '$menuSub' : { placeholder: _t('Action Sub-Category'), type: 'select-query', attrs: { query: 'fetchSubMenus', 'ng-change': 'resetSelector($event, "$menuItem")' } }, '$menuItem' : { placeholder: _t('Action'), widget: 'select-query', attrs: { query: 'fetchItemMenus' } } }; $scope.schema = { cols : 4, colWidths : '30%,30%,30%,10%', type : 'form', items : [ { name : '$menuRoot', showTitle : false }, { name : '$menuSub', showTitle : false }, { name : '$menuItem', showTitle : false }, { type : 'button', title : _t('Go'), attrs: { 'ng-click': 'doAction()' } } ] }; } angular.module('axelor.ui').directive('uiActionSelector', function(){ return { scope: true, controller: ActionSelectorCtrl, template: '
' }; }); })();