/* * 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.ManyToOneCtrl = ManyToOneCtrl; ui.ManyToOneCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService']; function ManyToOneCtrl($scope, $element, DataSource, ViewService) { ui.RefFieldCtrl.apply(this, arguments); $scope.selectEnable = true; var ds = $scope._dataSource; $scope.createNestedEditor = function() { var embedded = $('
') .attr('ng-model', $element.attr('ng-model')) .attr('name', $element.attr('name')) .attr('x-title', $element.attr('x-title')) .attr('x-path', $element.attr('x-path')); embedded = ViewService.compile(embedded)($scope); embedded.hide(); var cell, row; // if panel form if ($element.parent().is("div.form-item")) { cell = $("
").addClass("span12 form-item").append(embedded); row = $("
").addClass("row-fluid").append(cell); row.insertAfter($element.parents(".row-fluid:first")); return embedded; } var colspan = $element.parents("form.dynamic-form:first").attr('x-cols') || 4; cell = $('').attr('colspan', colspan).append(embedded); row = $('').append(cell); row.insertAfter($element.parents("tr:first")); return embedded; }; $scope.selectMode = "single"; $scope.findRelativeFields = (function () { var relatives = null; return function () { if (relatives) { return relatives; } var path = $element.attr('x-path'); relatives = $element.parents().find('[x-field][x-path^="'+path+'."]:not(label,.panel)').map(function() { return $(this).attr('x-path').replace(path+'.',''); }).get(); relatives.push($scope.field.targetName); relatives = _.unique(_.compact(relatives)).filter(function (name) { var last = _.last(name.split('.')); if (last[0] === '$' || last[0] === '_') { return false; } return true; }); return relatives; }; })(); $scope.fetchMissingValues = function (value, fields) { var nameField = $scope.field.targetName || 'id'; var related = {}; var relatives = fields || $scope.findRelativeFields(); var missing = _.filter(relatives, function (name) { return !value || (ui.canSetNested(value, name) && ui.findNested(value, name) === undefined); }); _.each(relatives, function(name) { var prefix = name.split('.')[0]; related[prefix] = value[prefix]; }); if (missing.length > 0 && value && value.id) { return ds.read(value.id, { fields: missing }).success(function(rec){ var record = _.extend({}, related, { id: value.id, $version: value.version || value.$version }); if (nameField in rec) { record[nameField] = rec[nameField]; } _.each(missing, function(name) { var value = ui.findNested(rec, name); if (value === undefined) value = null; ui.setNested(record, name, value); }); $scope.setValue(record, false); }); } }; $scope.select = function(value) { if (_.isArray(value)) { value = _.first(value); } var nameField = $scope.field.targetName || 'id'; var trKey = '$t:' + nameField; var record = value; if (value && value.id) { record = _.extend({}, { id: value.id, $version: value.version === undefined ? value.$version : value.version }); var missing = $scope.findRelativeFields().filter(function (name) { return !value || ui.findNested(value, name) === undefined; }); if (value[trKey] !== undefined) { record[trKey] = value[trKey]; } if (value[nameField] !== undefined) { record[nameField] = value[nameField]; } else { missing.push(nameField); } if (missing.length) { return ds.read(value.id, {fields: missing}).success(function(rec) { record = _.extend(record, _.omit(rec, 'version')); record.$version = rec.version === undefined ? record.$version : rec.version; $scope.setValue(record, true); }); } if (value.code) { record.code = value.code; } } $scope.setValue(record, true); }; $scope.canEditTarget = function () { return $scope.canEdit() && $scope.attr('canEdit') !== false; }; $scope.canEdit = function () { var parent = $scope.$parent; if (parent.canEditTarget && !parent.canEditTarget()) { return false; } // disable edit icon if ($scope.attr('canEdit') === undefined) { return false; } return $scope.attr('canEdit') !== false && $scope.canView() && $scope.getValue(); }; $scope.canView = function () { return $scope.attr('canView') !== false && $scope.getValue(); }; $scope.onEdit = function() { var record = $scope.getValue(); $scope.showEditor(record); }; var canNew = $scope.canNew; $scope.canNew = function () { // disable new icon if ($scope.attr('canNew') === undefined) { return false; } return canNew.call($scope); }; $scope._isNestedOpen = false; $scope.onSummary = function() { $scope._isNestedOpen = !$scope._isNestedOpen; var record = $scope.getValue(); if (record && record.id) { return ds.read(record.id).success(function(record){ $scope.showNestedEditor($scope._isNestedOpen); }); } $scope.showNestedEditor($scope._isNestedOpen); }; var icons = null; var actions = { 'new': 'canNew', 'create': 'canNew', 'edit': 'canEdit', 'select': 'canSelect', 'remove': 'canRemove', 'clear': 'canRemove' }; $scope.canShowIcon = function (which) { var names; var field = $scope.field || {}; var prop = actions[which]; if (prop !== undefined && $scope.attr(prop) === false) { return false; } if (icons === null) { icons = {}; names = $scope.field.showIcons || $scope.$parent.field.showIcons; if (names === false || names === 'false') { icons.$all = false; } else if (names === true || names === 'true' || names === undefined) { icons.$all = true; } else if (names) { icons.$all = false; names = names.split(','); names.forEach(function (name) { icons[name.trim()] = true; }); } } return icons.$all || !!icons[which]; }; } ui.directive('uiCanSuggest', function () { return function (scope, element, attrs) { var field = scope.field || {}; if (field.canSuggest !== false) { return; } element.prop("readonly", true); element.addClass("not-readonly"); // jquery-ui doesn't allow keyboard navigation on autocomplete list element.on('keydown', function (e) { var inst = element.data('autocomplete'); switch (e.keyCode) { case 38: // up inst._keyEvent('previous', e); break; case 40: // down inst._keyEvent('next', e); break; case 9: // tab if (inst.menu.active) { inst.menu.select(event); } break; case 13: // enter if (inst.menu.active) { inst.menu.select(event); e.preventDefault(); } break; case 27: // escape if (inst.menu.element.is(':visible')) { inst.close(e); e.preventDefault(); } break; } }); }; }); var m2oTemplateReadonly = "" + "" + "{{text}}" + "{{text}}" + ""; var m2oTemplateEditable = '' + '
'+ ''+ ''+ '{{text}} '+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ '
'; ui.formInput('ManyToOne', 'Select', { css : 'many2one-item', widgets: ['OneToOne'], controller: ManyToOneCtrl, init: function(scope) { this._super(scope); scope.canShowTag = scope.field['tag-edit']; scope.formatItem = function(item) { var trKey = "$t:" + scope.field.targetName; if (item) { return item[trKey] || item[(scope.field.targetName || "id")]; } return ""; }; }, link: function(scope, element, attrs, model) { this._super.apply(this, arguments); scope.ngModel = model; scope.formPath = scope.formPath ? scope.formPath + "." + scope.field.name : scope.field.name; var field = scope.field; scope.canToggle = function() { return (field.widgetAttrs || {}).toggle; }; scope.$evalAsync(function() { var relatives = scope.findRelativeFields(); if (relatives.length > 0) { scope.$watch(attrs.ngModel, function m2oValueWatch(value, old) { if (value && value.id > 0) { scope.fetchMissingValues(scope.getValue(), relatives); } }, true); } }); scope.isHiddenSelf = function() { return scope.isHidden(); }; if (field.widget === 'NestedEditor') { scope.isHiddenSelf = function() { if (!scope.canSelect()) { return true; } if (scope.canToggle() === 'both' && scope._isNestedOpen) { return true; } return scope.isHidden(); }; var hiddenSet = false; var hasHidden = false; scope.$watch('attr("hidden")', function m2oHiddenWatch(hidden, old) { if (hidden && !hiddenSet) hasHidden = true; if (hiddenSet && hidden === old) return; hiddenSet = true; if (scope._isNestedOpen || hasHidden) { scope.showNestedEditor(!hidden); } }); scope.$on("on:check-nested-values", function (e, value) { if (scope.isHidden() && value) { var val = scope.getValue() || {}; if (val.$updatedValues === value && val.id === value.id) { _.extend(val, value); } } }); scope.$timeout(function() { if (!scope.attr("hidden")) { scope.onSummary(); } }); } }, link_editable: function(scope, element, attrs, model) { this._super.apply(this, arguments); var input = this.findInput(element); var field = scope.field; function create(term, popup) { scope.createOnTheFly(term, popup, function (record) { scope.select(record); }); } input.on("change", function () { input.attr('placeholder', input.is(':focus') ? _t('Search...') : null); }); input.on("focus", function () { // XXX: firefox prevents click event, bug in FF? if (!axelor.browser.mozilla) { input.attr('placeholder', _t('Search...')); } }); input.on("blur", function () { input.attr('placeholder', field.placeholder || ''); }); var validateSearch = _.debounce(function () { if (input.length === 0 || input.is(':hidden')) return; var my = String(input.val() || ''); var text = String(scope.text || '').replace(/\n/g, ''); var invalid = my !== text; scope.$applyAsync(function () { scope.setValidity('search', !invalid); }); }); if (input.length) { input.on("input", validateSearch); input.on("keydown", validateSearch); input.on("propertychange", validateSearch); scope.$on("on:edit", validateSearch); scope.$on("on:new", validateSearch); scope.$watch("text", validateSearch); } scope.loadSelection = function(request, response) { if (!scope.canSelect()) { return response([]); } this.fetchSelection(request, function(items, page) { var term = request.term; if (scope.canSelect() && (items.length < page.total || (request.term && items.length === 0))) { if (items.length === 0) { items.push({ label: _t("No results found"), click: function() {} }); } else { items.push({ label: _t("Search more..."), click: function() { scope.showSelector(); } }); } } if (field.create && term && scope.canNew()) { items.push({ label : _t('Create "{0}" and select...', term), click : function() { create(term); } }); items.push({ label : _t('Create "{0}"...', term), click : function() { create(term, true); } }); } if ((field.create === undefined || (field.create && !term)) && scope.canNew()) { items.push({ label: _t("Create..."), click: function() { scope.showPopupEditor(); } }); } response(items, page); }); }; scope.matchValues = function(a, b) { if (a === b) return true; if (!a) return false; if (!b) return false; return a.id === b.id; }; scope.setValidity = function(key, value) { model.$setValidity(key, value); }; if ((scope._viewParams || {}).summaryView) { element.removeClass('picker-icons-3').addClass('picker-icons-4'); } scope.isDisabled = function() { return this.isReadonly(); }; input.keydown(function(e){ var handled = false; if (e.keyCode == 113) { // F2 if (e.shiftKey || !scope.getValue()) { if (scope.canNew()) { scope.onNew(); } } else { if (scope.canEdit()) { scope.onEdit(); } } handled = true; } if (e.keyCode == 114) { // F3 if (scope.canSelect()) { scope.onSelect(); } handled = true; } if (!handled) { return; } e.preventDefault(); e.stopPropagation(); return false; }); scope.handleEnter = function (e) { var handled = false; var widget = input.autocomplete('widget'); if (widget) { var item = widget.find('li .ui-state-focus').parent(); if (item.length === 0) { item = widget.find('li:not(.tag-select-action)'); item = item.length === 1 ? item.first() : null; } var data = item ? item.data('ui-autocomplete-item') : null; if (data) { input.autocomplete('close'); if (data.click) { data.click.call(scope); } else { scope.setValidity('search', true); scope.select(data.value); handled = true; } } scope.$timeout(function () { adjustPadding(); if (scope.onChangeNotify && handled) { scope.onChangeNotify(scope, scope.record); } }, 100); } }; scope.handleSelect = function(e, ui) { var handled = false; if (ui.item.click) { ui.item.click.call(scope); } else { scope.setValidity('search', true); scope.select(ui.item.value); handled = true; } scope.$timeout(function () { adjustPadding(); if (scope.onChangeNotify && handled) { scope.onChangeNotify(scope, scope.record); } }, 100); }; scope.$render_editable = function() { if (scope.canShowTag) { return setTimeout(adjustPadding, 100); } var value = scope.getValue(); var name = scope.field.targetName; if (value && value.id > 0 && !value[name]) { return scope._dataSource.details(value.id, name).success(function(rec) { value[name] = rec[name]; input.val(scope.getText()); validateSearch(); }); } input.val(scope.getText()); validateSearch(); }; if (scope.field && scope.field['tag-edit']) { scope.attachTagEditor(scope, element, attrs); } scope.onTagSelect = function (e) { if (scope.canEdit()) { return scope.onTagEdit(e, scope.getValue()); } scope.onEdit(); }; scope.onTagRemove = function (e) { scope.setValue(null); }; function adjustPadding() { var tag = element.find('span.tag-link'); if (tag.length && tag.is(':visible')) { input.css('padding-left', tag.width() + 24); } else { input.css('padding-left', ''); } if (scope.canShowTag) { input.val(''); } } if (scope.canShowTag) { input.focus(adjustPadding); } }, template_editable: m2oTemplateEditable, template_readonly: m2oTemplateReadonly }); ui.InlineManyToOneCtrl = InlineManyToOneCtrl; ui.InlineManyToOneCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService']; function InlineManyToOneCtrl($scope, $element, DataSource, ViewService) { var field = $scope.field || $scope.getViewDef($element); var names = []; var params = { model: field.target }; if (field.editor) { params.views = [{ type: 'form', items: field.editor.items }]; names = _.pluck(field.editor.items, 'name'); names = _.compact(names); } else if (field.viewer && field.viewer.depends) { names = field.viewer.depends.split(/\s*,\s*/); } $scope._viewParams = params; ManyToOneCtrl.call(this, $scope, $element, DataSource, ViewService); if (field.targetName) { names.push(field.targetName); } names = _.unique(names); $scope.select = function (value) { if (_.isArray(value)) { value = _.first(value); } if (_.isEmpty(value)) { return $scope.setValue(null, true); } function set(val) { var record = _.pick(val, names); record.id = val.id; record.$version = val.version || val.$version; $scope.setValue(record, true); } var record = _.pick(value, names); // if some field is missing if (_.keys(record).length !== names.length && value.id) { return $scope._dataSource.read(value.id, { fields: names }).success(function (rec) { set(rec); }); } set(value); }; $scope.onClear = function() { $scope.setValue(null, true); }; } ui.formInput('InlineManyToOne', 'ManyToOne', { widgets: ['InlineOneToOne'], controller: InlineManyToOneCtrl, template_readonly: function (scope) { var field = scope.field || {}; if (field.viewer) { return field.viewer.template; } if (field.editor && (field.editor.viewer || !field.targetName)) { return null; } return m2oTemplateReadonly; }, template_editable: function (scope) { var editor = scope.field.editor || {}; var template = "" + "
" + "
" + "" + "" + "" + "" + "
" + "
" + "
"; if (editor.showOnNew !== false) { return template; } scope.canShowEditor = function () { return (scope.record || {}).id > 0 || !!scope.getValue(); }; template = "
" + "
"+ m2oTemplateEditable +"
" + "
"+ template +"
" + "
"; return template; }, template: null }); ui.formInput('SuggestBox', 'ManyToOne', { metaWidget: true, link_editable: function(scope, element, attrs, model) { this._super.apply(this, arguments); var field = scope.field; if (field.canNew === undefined && field.create) { field.canNew = true; } if (field.create === undefined && field.canNew) { field.create = true; } }, template_editable: ''+ ''+ ''+ ''+ ''+ ''+ '' }); ui.formInput('RefSelect', { css: 'multi-object-select', metaWidget: true, controller: ['$scope', 'ViewService', function($scope, ViewService) { $scope.createElement = function(id, name, selectionList, related) { var elemGroup = $('
'); var elemSelect = $('') .attr("name", name) .attr("x-for-widget", id) .attr("ng-model", "record." + name); var elemSelects = $('
').attr('ng-switch', "record." + name); var elemItems = _.map(selectionList, function(s) { return $('') .attr('ng-model', 'record.$_' + related) .attr('x-target', s.value) .attr('x-watch-name', name) .attr('x-ref', related); }); elemGroup .append($('
').append(elemSelect)) .append(elemSelects.append(elemItems)); return ViewService.compile(elemGroup)($scope); }; }], link: function(scope, element, attrs, model) { this._super.apply(this, arguments); var name = scope.field.name, selectionList = scope.field.selectionList, related = scope.field.related || scope.field.name + "Id"; scope.isLink = this.isLink; scope.fieldsCache = {}; scope.selectionList = selectionList; scope.refFireEvent = function (name) { var handler = scope.$events[name]; if (handler) { return handler(); } }; var elem = scope.createElement(element.attr('id'), name, selectionList, related); setTimeout(function() { element.append(elem); }); setTimeout(function () { var selectScope = element.find('[name="'+name+'"]').scope(); var setValue = selectScope.setValue; selectScope.setValue = function (value) { var old = scope.getValue(); if (old !== value && scope.record) { scope.record[related] = null; } setValue.apply(selectScope, arguments); }; }); }, template_editable: null, template_readonly: null }); ui.formInput('RefLink', 'RefSelect', { css: 'multi-object-select multi-object-link', isLink: true }); ui.formInput('RefItem', 'ManyToOne', { showTitle: false, controller: ['$scope', '$element', 'DataSource', 'ViewService', function ($scope, $element, DataSource, ViewService) { var target = $element.attr('x-target'); var data = (_.findWhere($scope.$parent.selectionList || $scope.$parent.field.selectionList, { value: target})||{}).data || {}; var getViewDef = $scope.getViewDef; $scope.getViewDef = function (elem) { if (elem === $element) { return _.extend({}, { domain: data.domain, formView: data.form, gridView: data.grid }); } return getViewDef.call($scope, elem); }; ManyToOneCtrl.apply(this, arguments); }], link: function(scope, element, attrs, model) { this._super.apply(this, arguments); if (scope.field.targetName) { return this._link.apply(this, arguments); } var self = this; var target = element.attr('x-target'); var data = (_.findWhere(scope.$parent.selectionList, {value: target})||{}).data || {}; function doLink(fields) { var name = false, search = []; _.each(fields, function(f) { if (f.nameColumn) name = f.name; if (f.name === "name") search.push("name"); if (f.name === "code") search.push("code"); }); if (!name && _.contains(search, "name")) { name = "name"; } _.extend(scope.field, { target: scope._model, targetName: name, targetSearch: search, domain: data.domain, canEdit: false }); self._link(scope, element, attrs, model); scope.setDomain(data.domain, data.context); if (scope.$parent.isLink) { scope.onEdit = function () { var value = scope.getValue() || {}; scope.openTab({ action: _.uniqueId('$act'), model: scope._model, viewType: "form", views: [{ type: "form" }] }, { mode: "edit", state: value.id }); }; } } if (scope.fieldsCache[scope._model]) { doLink(scope.fieldsCache[scope._model]); } else { scope.loadFields().success(function (fields) { scope.fieldsCache[scope._model] = fields; doLink(fields); }); } }, _link: function(scope, element, attrs, model) { var ref = element.attr('x-ref'); var watch = element.attr('x-watch-name'); var target = element.attr('x-target'); var refField = scope.fields[ref] || {}; function setRef(value) { if (!scope.record) { return; } if (value && refField.type === 'string') { value = '' + value; } var old = scope.record[ref]; scope.record[ref] = value; if (old != value) { scope.refFireEvent('onChange'); } } scope.targetValue = target; var __setValue = scope.setValue; scope.setValue = function(value) { __setValue.call(scope, value); setRef(value ? value.id : 0); }; var doSelect = _.debounce(function () { scope.$timeout(function() { var ds = scope._dataSource; var value = (scope.record || {})[ref]; if (!value) return; var v = scope.getValue(); if (v && v.id === value) return; var nameField = scope.field.targetName; var fields = nameField ? [nameField] : undefined; var promise = ds._request('fetch', value).post({ fields: fields }, { silent: true }); promise.success(function (response) { var record = _.first(response.data) || (value ? { id: value } : null); scope.select(record); }).error(function () { var record = value ? { id: value } : null; if (record) { record[nameField] = value; } scope.setValue(record, false); }); }); }, 100); var watchExpr = "record.id + record." + watch + " + record." + ref; scope.$watch(watchExpr, doSelect); scope.$watch("record", doSelect); } }); ui.formInput('RefText', 'ManyToOne', { metaWidget: true, link_editable: function (scope, element, attrs) { this._super.apply(this, arguments); if (!scope.field.create) { return; } function freeSelect(text) { return function () { scope.$timeout(function () { scope.select(text); }); }; } scope.loadSelection = function(request, response) { this.fetchSelection(request, function(items, page) { var term = request.term; if (term) { items.push({ label : _t('Select "{0}"...', term), click : freeSelect(term) }); } response(items); }); }; }, link: function (scope, element, attrs, model) { scope.canNew = function () { return false; }; scope.canView = function () { return false; }; scope.canEdit = function () { return false; }; var field = scope.field; var targetName = field.targetName || "name"; scope._viewParams.views = [{ type: "grid", items: [{ name: targetName, type: "field" }] }]; scope.formatItem = function (record) { return record || ""; }; scope.select = function (value) { var record = _.isArray(value) ? _.first(value) : value; var val = _.isString(record) ? record : (record || {})[targetName]; if (val === undefined) { val = null; } scope.setValue(val, true); }; } }); ui.formInput('EvalRefSelect', 'Select', { controller: ['$scope', '$element', 'DataSource', 'ViewService', function ($scope, $element, DataSource, ViewService) { var fetchDS; $scope.$fetchDS = function () { var target = this.$target; if (!fetchDS || fetchDS._model !== target) { fetchDS = DataSource.create(target); } return fetchDS; }; $scope.canNew = function () { return false; }; $scope.showSelector = function () { var child = $scope.$new(); child._viewParams = { model: $scope.$target, views: [{ type: 'grid', items: [{ type: 'field', name: $scope.$targetName }] }] }; var selector = ViewService.compile('
')(child); var popup = selector.isolateScope(); selector.on('dialogclose', function () { selector.remove(); child.$destroy(); }); popup.show(); }; }], init: function(scope) { this._super.apply(this, arguments); var field = scope.field; function toValue(value) { var val = value; if (val === undefined || val === null) { val = null; } else { val = '"' + val + '"'; } return val; } Object.defineProperties(scope, { $target: { get: function () { return scope.$eval(field.evalTarget); } }, $targetName: { get: function () { return scope.$eval(field.evalTargetName); } }, $recordValue: { get: function () { return scope.$eval(field.evalValue); }, set: function (value) { scope.$eval(field.evalValue + " = " + toValue(value)); } }, $recordTitle: { get: function () { return scope.$eval(field.evalTitle); }, set: function (value) { scope.$eval(field.evalTitle + " = " + toValue(value)); } } }); scope.formatItem = function (value) { return scope.$recordTitle; }; scope.select = function (value) { var item = _.isArray(value) ? _.first(value) : value; scope.setValue(item); }; scope.setValue = function (value) { var nameField = scope.$targetName; var val = value || {}; scope.$recordValue = val.id; scope.$recordTitle = val[nameField]; }; }, link_editable: function(scope, element, attrs, model) { this._super.apply(this, arguments); scope.handleSelect = function(e, ui) { if (ui.item.click) { ui.item.click.call(scope); } else { scope.select(ui.item.value); } scope.$applyAsync(); }; scope.loadSelection = function(request, response) { var targetName = scope.$targetName; if (!targetName) { return response([]); } var ds = scope.$fetchDS(); var filter = {}; if (request.term) { filter[targetName] = request.term; } ds.search({ fields: ['id', targetName], filter: filter, limit: axelor.device.small ? 6 : 10 }).success(function (records, page) { var items = _.map(records, function(record) { return { label: record[targetName], value: record }; }); if (items.length < page.total || (request.term && items.length === 0)) { items.push({ label: _t("Search more..."), click: function() { scope.showSelector(); } }); } response(items, page); }); }; } }); })();