1324 lines
38 KiB
JavaScript
1324 lines
38 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");
|
|
|
|
ui.OneToManyCtrl = OneToManyCtrl;
|
|
ui.OneToManyCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
|
|
|
function OneToManyCtrl($scope, $element, DataSource, ViewService, initCallback) {
|
|
|
|
ui.RefFieldCtrl.call(this, $scope, $element, DataSource, ViewService, function(){
|
|
ui.GridViewCtrl.call(this, $scope, $element);
|
|
$scope.editorCanSave = false;
|
|
$scope.selectEnable = false;
|
|
if (initCallback) {
|
|
initCallback();
|
|
}
|
|
});
|
|
|
|
var embedded = null,
|
|
detailView = null;
|
|
|
|
$scope.createNestedEditor = function() {
|
|
|
|
embedded = $('<div ui-embedded-editor class="inline-form"></div>');
|
|
embedded.attr('x-title', $element.attr('x-title'));
|
|
embedded = ViewService.compile(embedded)($scope);
|
|
embedded.hide();
|
|
|
|
$element.append(embedded);
|
|
embedded.data('$rel', $element.children('.slickgrid:first').children('.slick-viewport'));
|
|
|
|
return embedded;
|
|
};
|
|
|
|
var _showNestedEditor = $scope.showNestedEditor;
|
|
$scope.showNestedEditor = function(show, record) {
|
|
_showNestedEditor(show, record);
|
|
if (embedded) {
|
|
embedded.data('$rel').hide();
|
|
var formScope = embedded.scope();
|
|
formScope._viewPromise.then(function () {
|
|
formScope.edit(record);
|
|
});
|
|
}
|
|
return embedded;
|
|
};
|
|
|
|
$scope.showDetailView = function() {
|
|
if (detailView == null) {
|
|
detailView = $('<div ui-embedded-editor class="detail-form"></div>').attr('x-title', $element.attr('x-title'));
|
|
detailView = ViewService.compile(detailView)($scope);
|
|
detailView.data('$rel', $());
|
|
detailView.data('$scope').isDetailView = true;
|
|
$element.after(detailView);
|
|
}
|
|
var es = detailView.data('$scope');
|
|
detailView.toggle(es.visible = !es.visible);
|
|
};
|
|
|
|
$scope.select = function(value) {
|
|
|
|
// if items are same, no need to set values
|
|
if ($scope._dataSource.equals(value, $scope.getValue())) {
|
|
return;
|
|
}
|
|
|
|
var items = _.chain([value]).flatten(true).compact().value();
|
|
var records = _.map($scope.getItems(), _.clone);
|
|
|
|
_.each(records, function(item) {
|
|
item.selected = false;
|
|
});
|
|
|
|
_.each($scope.itemsPending, function (item) {
|
|
var find = _.find(records, function(rec) {
|
|
if (rec.id && rec.id == item.id) {
|
|
return true;
|
|
}
|
|
var a = _.omit(item, 'id', 'version');
|
|
var b = _.omit(rec, 'id', 'version');
|
|
return $scope._dataSource.equals(a, b);
|
|
});
|
|
if (!find) {
|
|
records.push(item);
|
|
}
|
|
});
|
|
|
|
_.each(items, function(item){
|
|
item = _.clone(item);
|
|
var find = _.find(records, function(rec){
|
|
var id1 = rec.id || rec.$id;
|
|
var id2 = item.id || item.$id;
|
|
return id1 && id1 === id2;
|
|
});
|
|
|
|
if (find) {
|
|
_.extend(find, item);
|
|
} else {
|
|
records.push(item);
|
|
}
|
|
});
|
|
|
|
records = $scope.$$ensureIds(records);
|
|
|
|
_.each(records, function(rec){
|
|
if (rec.id <= 0) rec.id = null;
|
|
});
|
|
|
|
if ($scope.dataView.$resequence) {
|
|
$scope.dataView.$resequence(records);
|
|
}
|
|
|
|
var callOnChange = $scope.dataView.$isResequencing !== true;
|
|
|
|
$scope.itemsPending = records;
|
|
$scope.setValue(records, callOnChange);
|
|
$scope.$timeout(function() {
|
|
$scope.$broadcast('grid:changed');
|
|
});
|
|
};
|
|
|
|
var _setItems = $scope.setItems;
|
|
$scope.setItems = function(items) {
|
|
_setItems(items);
|
|
$scope.itemsPending = [];
|
|
if (embedded !== null) {
|
|
embedded.data('$scope').onClose();
|
|
}
|
|
if (detailView !== null)
|
|
if (items === null || _.isEmpty(items))
|
|
detailView.hide();
|
|
else
|
|
detailView.show();
|
|
};
|
|
|
|
$scope.removeItems = function(items, fireOnChange) {
|
|
var all, ids, records;
|
|
|
|
if (_.isEmpty(items)) return;
|
|
|
|
all = _.isArray(items) ? items : [items];
|
|
|
|
ids = _.map(all, function(item) {
|
|
return _.isNumber(item) ? item : item.id;
|
|
});
|
|
|
|
records = _.filter($scope.getItems(), function(item) {
|
|
return ids.indexOf(item.id) === -1;
|
|
});
|
|
|
|
$scope.setValue(records, fireOnChange);
|
|
$scope.$applyAsync();
|
|
};
|
|
|
|
$scope.removeSelected = function(selection) {
|
|
var selected, items;
|
|
if (_.isEmpty(selection)) return;
|
|
selected = _.map(selection, function (i) {
|
|
return $scope.dataView.getItem(i).id;
|
|
});
|
|
items = _.filter($scope.getItems(), function (item) {
|
|
return selected.indexOf(item.id) === -1;
|
|
});
|
|
// remove selected from data view
|
|
_.each(selected, function (id) {
|
|
if (id && id > -1) $scope.dataView.deleteItem(id);
|
|
});
|
|
$scope.dataView.$setSelection([]);
|
|
$scope.setValue(items, true);
|
|
$scope.$applyAsync();
|
|
};
|
|
|
|
$scope.canEditTarget = function () {
|
|
return $scope.canEdit() && $scope.attr('canEdit') !== false;
|
|
};
|
|
|
|
$scope.canShowEdit = function () {
|
|
var selected = $scope.selection.length ? $scope.selection[0] : null;
|
|
return selected !== null && $scope.canEdit();
|
|
};
|
|
|
|
$scope.canShowView = function () {
|
|
if ($scope.canShowEdit()) return false;
|
|
var selected = $scope.selection.length ? $scope.selection[0] : null;
|
|
return selected !== null && $scope.canView();
|
|
};
|
|
|
|
$scope.canEdit = function () {
|
|
return $scope.attr('canEdit') !== false && $scope.canView();
|
|
};
|
|
|
|
var _canRemove = $scope.canRemove;
|
|
$scope.canRemove = function () {
|
|
var selected = $scope.selection.length ? $scope.selection[0] : null;
|
|
return _canRemove() && selected !== null;
|
|
};
|
|
|
|
$scope.canCopy = function () {
|
|
if (!$scope.field || !($scope.field.widgetAttrs||{}).canCopy) return false;
|
|
if (!$scope.canNew()) return false;
|
|
if (!$scope.selection || $scope.selection.length !== 1) return false;
|
|
var record = $scope.dataView.getItem(_.first($scope.selection));
|
|
return !!record;
|
|
};
|
|
|
|
$scope.onCopy = function() {
|
|
var ds = $scope._dataSource;
|
|
var index = _.first($scope.selection);
|
|
var item = $scope.dataView.getItem(index);
|
|
var doSelect = function (record) {
|
|
if (!record.id) $scope.$$ensureIds([record]);
|
|
$scope.select([record]);
|
|
$scope.$timeout(function () {
|
|
$scope.dataView.$setSelection([$scope.dataView.getLength() - 1], true);
|
|
}, 100);
|
|
};
|
|
if (item && item.id > 0) {
|
|
ds.copy(item.id).success(doSelect);
|
|
} else if (item) {
|
|
item = angular.copy(item);
|
|
item.id = undefined;
|
|
item.$id = undefined;
|
|
doSelect(item);
|
|
}
|
|
};
|
|
|
|
$scope.onEdit = function() {
|
|
var selected = $scope.selection.length ? $scope.selection[0] : null;
|
|
if (selected !== null) {
|
|
var record = $scope.dataView.getItem(selected);
|
|
$scope.showEditor(record);
|
|
}
|
|
};
|
|
|
|
$scope.onRemove = function() {
|
|
if (this.isReadonly()) {
|
|
return;
|
|
}
|
|
axelor.dialogs.confirm(_t("Do you really want to delete the selected record(s)?"), function(confirmed){
|
|
if (confirmed && $scope.selection && $scope.selection.length)
|
|
$scope.removeSelected($scope.selection);
|
|
});
|
|
};
|
|
|
|
$scope.onSummary = function() {
|
|
var selected = $scope.getSelectedRecord();
|
|
if (selected) {
|
|
$scope.showNestedEditor(true, selected);
|
|
}
|
|
};
|
|
|
|
$scope.viewCanCopy = function () {
|
|
return this.hasPermission("create") && !this.isDisabled() && !this.isReadonly() && this.canCopy();
|
|
};
|
|
|
|
$scope.viewCanExport = function () {
|
|
if (!$scope.field || !($scope.field.widgetAttrs||{}).canExport) return false;
|
|
return this.hasPermission("export") && this.canExport();
|
|
};
|
|
|
|
$scope.getSelectedRecord = function() {
|
|
var selected = _.first($scope.selection || []);
|
|
if (_.isUndefined(selected))
|
|
return null;
|
|
return $scope.dataView.getItem(selected);
|
|
};
|
|
|
|
var _onSelectionChanged = $scope.onSelectionChanged;
|
|
$scope.onSelectionChanged = function(e, args) {
|
|
_onSelectionChanged(e, args);
|
|
|
|
var field = $scope.field,
|
|
record = $scope.record || {},
|
|
selection = $scope.selection || [];
|
|
|
|
record.$many = record.$many || (record.$many = {});
|
|
record.$many[field.name] = selection.length ? $scope.getItems : null;
|
|
|
|
if (detailView === null) {
|
|
return;
|
|
}
|
|
|
|
$scope.$timeout(function() {
|
|
var dvs = detailView.scope();
|
|
var rec = $scope.getSelectedRecord() || {};
|
|
detailView.show();
|
|
if (!dvs.record || (dvs.record.id !== rec.id)) {
|
|
dvs.edit(rec);
|
|
}
|
|
});
|
|
};
|
|
|
|
$scope.onItemDblClick = function(event, args) {
|
|
if($scope.canView()){
|
|
$scope.onEdit();
|
|
$scope.$applyAsync();
|
|
}
|
|
};
|
|
|
|
(function (scope) {
|
|
|
|
var dummyId = 0;
|
|
|
|
function ensureIds(records, clone) {
|
|
var items = [];
|
|
angular.forEach(records, function(record){
|
|
var item = clone ? angular.copy(record, {}) : (record || {});
|
|
if (!item.id) {
|
|
item.id = item.$id || (item.$id = --dummyId);
|
|
}
|
|
items.push(item);
|
|
});
|
|
return items;
|
|
}
|
|
|
|
function fetchData() {
|
|
var items = scope.getValue();
|
|
return scope.fetchData(items, function(records){
|
|
scope.setItems(ensureIds(records, true));
|
|
});
|
|
}
|
|
|
|
scope.$$ensureIds = ensureIds;
|
|
scope.$$fetchData = fetchData;
|
|
|
|
})($scope);
|
|
|
|
$scope.reload = function() {
|
|
return $scope.$$fetchData();
|
|
};
|
|
|
|
$scope.filter = function() {
|
|
|
|
};
|
|
|
|
$scope.onSort = function(event, args) {
|
|
|
|
//TODO: implement client side sorting (prevent losing O2M changes).
|
|
if ($scope.isDirty() && !$scope.editorCanSave) {
|
|
return;
|
|
}
|
|
|
|
var records = $scope.getItems();
|
|
if (records == null || records.length === 0)
|
|
return;
|
|
|
|
for (var i = 0; i < records.length; i++) {
|
|
var item = records[i];
|
|
if (!item.id || item.id <= 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var sortBy = [];
|
|
|
|
angular.forEach(args.sortCols, function(column){
|
|
var field = column.sortCol.descriptor;
|
|
var name = column.sortCol.field;
|
|
if (field.jsonField) {
|
|
if (field.type === 'many-to-one' && field.targetName) {
|
|
name = name + "." + field.targetName;
|
|
}
|
|
name += '::' + ('integer,boolean,decimal'.indexOf(field.type) > -1 ? field.type : 'text');
|
|
}
|
|
var spec = column.sortAsc ? name : '-' + name;
|
|
sortBy.push(spec);
|
|
});
|
|
|
|
var ids = _.pluck(records, 'id');
|
|
var criterion = {
|
|
'fieldName': 'id',
|
|
'operator': 'in',
|
|
'value': ids
|
|
};
|
|
|
|
var fields = $scope.selectFields();
|
|
var filter = {
|
|
operator: 'and',
|
|
criteria: [criterion]
|
|
};
|
|
|
|
$scope.selection = [];
|
|
$scope._dataSource.search({
|
|
filter: filter,
|
|
fields: fields,
|
|
sortBy: sortBy,
|
|
archived: true,
|
|
limit: -1,
|
|
domain: null,
|
|
context: null
|
|
});
|
|
};
|
|
|
|
$scope.onShow = function(viewPromise) {
|
|
|
|
};
|
|
|
|
$scope.show();
|
|
}
|
|
|
|
ui.ManyToManyCtrl = ManyToManyCtrl;
|
|
ui.ManyToManyCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
|
|
|
function ManyToManyCtrl($scope, $element, DataSource, ViewService) {
|
|
|
|
OneToManyCtrl.call(this, $scope, $element, DataSource, ViewService, function(){
|
|
$scope.editorCanSave = true;
|
|
$scope.selectEnable = true;
|
|
});
|
|
|
|
var _setValue = $scope.setValue;
|
|
$scope.setValue = function(value, trigger) {
|
|
var compact = _.map(value, function(item) {
|
|
return {
|
|
id: item.id,
|
|
$version: item.version
|
|
};
|
|
});
|
|
_setValue(compact, trigger);
|
|
};
|
|
}
|
|
|
|
ui.formInput('OneToMany', {
|
|
|
|
css: 'one2many-item',
|
|
|
|
transclude: true,
|
|
|
|
showTitle: false,
|
|
|
|
collapseIfEmpty: true,
|
|
|
|
controller: OneToManyCtrl,
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
|
|
scope.ngModel = model;
|
|
scope.title = attrs.title;
|
|
|
|
scope.formPath = scope.formPath ? scope.formPath + "." + attrs.name : attrs.name;
|
|
|
|
var doRenderUnwatch = null;
|
|
var doViewPromised = false;
|
|
|
|
var validate = model.$validators.valid || function () { return true; };
|
|
model.$validators.valid = function(modelValue, viewValue) {
|
|
if (scope.isRequired() && _.isEmpty(viewValue)) return false;
|
|
return validate.call(model.$validators, viewValue);
|
|
};
|
|
|
|
if (scope.field.requiredIf) {
|
|
scope.$watch("attr('required')", function o2mRequiredWatch(required) {
|
|
if (required !== undefined) {
|
|
model.$validate();
|
|
}
|
|
});
|
|
}
|
|
if (scope.field.validIf) {
|
|
scope.$watch("attr('valid')", function o2mValidWatch(valid) {
|
|
if (valid !== undefined) {
|
|
model.$setValidity('invalid', valid);
|
|
}
|
|
});
|
|
}
|
|
|
|
function doRender() {
|
|
if (doRenderUnwatch) {
|
|
return;
|
|
}
|
|
doRenderUnwatch = scope.$watch(function o2mFetchWatch() {
|
|
if (!isVisible() || !doViewPromised) {
|
|
return;
|
|
}
|
|
doRenderUnwatch();
|
|
doRenderUnwatch = null;
|
|
scope.$$fetchData();
|
|
});
|
|
}
|
|
|
|
function isVisible() {
|
|
return !element.is(':hidden');
|
|
}
|
|
|
|
scope._viewPromise.then(function () {
|
|
doViewPromised = true;
|
|
if (doRenderUnwatch) {
|
|
doRenderUnwatch();
|
|
doRenderUnwatch = null;
|
|
doRender();
|
|
}
|
|
});
|
|
|
|
model.$render = doRender;
|
|
|
|
var adjustHeight = false;
|
|
var adjustSize = (function() {
|
|
var rowSize = +(scope.field.rowHeight) || 26;
|
|
var minSize = 56;
|
|
var elem = element;
|
|
if (elem.is('.panel-related')) {
|
|
elem = element.children('.panel-body');
|
|
minSize = 28;
|
|
} else if (scope.$hasPanels) {
|
|
minSize += 28;
|
|
}
|
|
if (elem.is('.picker-input')) {
|
|
elem = null;
|
|
}
|
|
|
|
var inc = 0,
|
|
height = +(scope.field.height) || 10;
|
|
var maxSize = (rowSize * height) + minSize;
|
|
|
|
var unwatch = scope.$watch('schema', function (schema) {
|
|
if (schema) {
|
|
unwatch();
|
|
unwatch = null;
|
|
if (schema.rowHeight && schema.rowHeight !== rowSize) {
|
|
rowSize = schema.rowHeight;
|
|
maxSize = (rowSize * height) + minSize;
|
|
}
|
|
}
|
|
});
|
|
|
|
return function(value) {
|
|
inc = arguments[1] || inc;
|
|
var count = Math.max(_.size(value), scope.dataView.getLength()) + inc, height = minSize;
|
|
if (count > 0) {
|
|
height = (rowSize * count) + (minSize + rowSize);
|
|
}
|
|
if (elem && adjustHeight) {
|
|
elem.css('min-height', count ? Math.min(height, maxSize) : (minSize + 26));
|
|
}
|
|
axelor.$adjustSize();
|
|
};
|
|
})();
|
|
|
|
var collapseIfEmpty = this.collapseIfEmpty;
|
|
scope.$watch(attrs.ngModel, function o2mAdjustWatch(value){
|
|
if (!value) {
|
|
// clear data view
|
|
scope.dataView.setItems([]);
|
|
}
|
|
if (collapseIfEmpty) {
|
|
adjustSize(value);
|
|
}
|
|
});
|
|
|
|
function deleteItemsById(id) {
|
|
var items = scope.dataView.getItems() || [];
|
|
while (items.length > 0) {
|
|
var item = _.findWhere(items, {id: id});
|
|
var index = _.indexOf(items, item);
|
|
if (index === -1) {
|
|
break;
|
|
}
|
|
items.splice(index, 1);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
scope.onGridInit = function(grid, inst) {
|
|
var editIcon = scope.canView() || (!scope.isReadonly() && scope.canEdit());
|
|
var editable = grid.getOptions().editable && !axelor.device.mobile;
|
|
|
|
adjustHeight = true;
|
|
|
|
if (editable) {
|
|
element.addClass('inline-editable');
|
|
scope.$on('on:new', function(event){
|
|
var items = deleteItemsById(0);
|
|
if (items.length === 0) {
|
|
scope.dataView.setItems([]);
|
|
grid.setSelectedRows([]);
|
|
}
|
|
});
|
|
scope.$watch("isReadonly()", function o2mReadonlyWatch(readonly) {
|
|
grid.setOptions({
|
|
editable: !readonly && scope.canEdit()
|
|
});
|
|
|
|
var _editIcon = scope.canView() || (!readonly && scope.canEdit());
|
|
if (_editIcon != editIcon) {
|
|
inst.showColumn('_edit_column', editIcon = _editIcon);
|
|
}
|
|
});
|
|
|
|
adjustSize(scope.getValue(), 1);
|
|
} else {
|
|
adjustSize(scope.getValue());
|
|
}
|
|
|
|
inst.showColumn('_edit_column', editIcon);
|
|
|
|
grid.onAddNewRow.subscribe(function (e, args) {
|
|
var items = scope.getValue() || [];
|
|
var rows = grid.getDataLength();
|
|
adjustSize(items, rows - items.length + 1);
|
|
});
|
|
|
|
scope.dataView.onRowCountChanged.subscribe(function (e, args) {
|
|
adjustSize();
|
|
});
|
|
|
|
if (!(scope._viewParams || {}).summaryView || scope.field.widget === "MasterDetail") {
|
|
return;
|
|
}
|
|
var col = {
|
|
id: '_summary',
|
|
name: '<span> </span>',
|
|
sortable: false,
|
|
resizable: false,
|
|
width: 16,
|
|
formatter: function(row, cell, value, columnDef, dataContext) {
|
|
return '<i class="fa fa-caret-right" style="display: inline-block; cursor: pointer; padding: 4px 10px;"></i>';
|
|
}
|
|
};
|
|
|
|
var cols = grid.getColumns();
|
|
cols.splice(0, 0, col);
|
|
|
|
grid.setColumns(cols);
|
|
grid.onClick.subscribe(function(e, args) {
|
|
if ($(e.target).is('.fa-caret-right'))
|
|
scope.$timeout(function(){
|
|
grid.setSelectedRows([args.row]);
|
|
grid.setActiveCell(args.row, args.cell);
|
|
scope.$timeout(function () {
|
|
scope.onSummary();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
scope.onGridBeforeSave = function(records) {
|
|
if (!scope.editorCanSave) {
|
|
deleteItemsById(0);
|
|
scope.select(records);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
scope.onGridAfterSave = function(records, args) {
|
|
if (scope.editorCanSave) {
|
|
scope.select(records);
|
|
}
|
|
};
|
|
|
|
scope.isDisabled = function() {
|
|
return this.isReadonly();
|
|
};
|
|
|
|
var field = scope.field;
|
|
if (field.widget === 'MasterDetail') {
|
|
setTimeout(function(){
|
|
scope.showDetailView();
|
|
});
|
|
}
|
|
|
|
scope.$watch("attr('title')", function o2mTitleWatch(title){
|
|
scope.title = title;
|
|
});
|
|
},
|
|
|
|
template_editable: null,
|
|
|
|
template_readonly: null,
|
|
|
|
template:
|
|
"<div class='stackbar' ng-class='{noEdit: canView() && !canEdit()}'>" +
|
|
"<div class='navbar'>" +
|
|
"<div class='navbar-inner'>" +
|
|
"<div class='container-fluid'>" +
|
|
"<span class='brand' href='' ui-help-popover ng-bind-html='title'></span>" +
|
|
"<span class='icons-bar dropdown pull-right' ng-show='viewCanCopy() || viewCanExport()'>" +
|
|
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-caret-down'></i></a>" +
|
|
"<ul class='dropdown-menu'>" +
|
|
"<li ng-show='viewCanCopy()'><a href='' ng-click='onCopy()' x-translate>Duplicate</a></li>" +
|
|
"<li ng-show='viewCanExport()'><a href='' ng-click='onExport()' x-translate>Export</a></li>" +
|
|
"</ul>" +
|
|
"</span>" +
|
|
"<span class='icons-bar pull-right' ng-show='!isReadonly()'>" +
|
|
"<a href='' ng-click='onSelect()' ng-show='hasPermission(\"read\") && !isDisabled() && canSelect()'>" +
|
|
"<i class='fa fa-search'></i><span x-translate>Select</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onNew()' ng-show='hasPermission(\"create\") && !isDisabled() && canNew()'>" +
|
|
"<i class='fa fa-plus'></i><span x-translate>New</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onEdit()' ng-show='hasPermission(\"read\") && canShowEdit()'>" +
|
|
"<i class='fa fa-pencil'></i><span x-translate>Edit</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onEdit()' ng-show='hasPermission(\"read\") && canShowView()'>" +
|
|
"<i class='fa fa-file-text-o'></i><span x-translate>Show</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onRemove()' ng-show='hasPermission(\"read\") && !isDisabled() && canRemove()'>" +
|
|
"<i class='fa fa-remove'></i><span x-translate>Remove</span>" +
|
|
"</a>" +
|
|
"<i ng-click='onCopy()' ng-show='hasPermission(\"create\") && !isDisabled() && canCopy()' title='{{\"Duplicate\" | t}}' class='fa fa-files-o'></i>" +
|
|
"</span>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"<div ui-view-grid " +
|
|
"x-view='schema' " +
|
|
"x-data-view='dataView' " +
|
|
"x-handler='this' " +
|
|
"x-no-filter='true' " +
|
|
"x-on-init='onGridInit' " +
|
|
"x-on-before-save='onGridBeforeSave' " +
|
|
"x-on-after-save='onGridAfterSave' " +
|
|
"></div>" +
|
|
"</div>"
|
|
});
|
|
|
|
ui.formInput('ManyToMany', 'OneToMany', {
|
|
|
|
css : 'many2many-item',
|
|
|
|
controller: ManyToManyCtrl
|
|
});
|
|
|
|
var panelRelatedTemplate =
|
|
"<div class='panel panel-related' ng-class='{noEdit: canView() && !canEdit()}'>" +
|
|
"<div class='panel-header'>" +
|
|
"<div class='panel-title'><span ui-help-popover ng-bind-html='title'></span></div>" +
|
|
"<div class='icons-bar' ng-show='!isReadonly()'>" +
|
|
"<a href='' ng-click='onSelect()' ng-show='hasPermission(\"read\") && !isDisabled() && canSelect()'>" +
|
|
"<i class='fa fa-search'></i><span x-translate>Select</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onNew()' ng-show='hasPermission(\"create\") && !isDisabled() && canNew()'>" +
|
|
"<i class='fa fa-plus'></i><span x-translate>New</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onEdit()' ng-show='hasPermission(\"read\") && canShowEdit()'>" +
|
|
"<i class='fa fa-pencil'></i><span x-translate>Edit</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onEdit()' ng-show='hasPermission(\"read\") && canShowView()'>" +
|
|
"<i class='fa fa-file-text-o'></i><span x-translate>Show</span>" +
|
|
"</a>" +
|
|
"<a href='' ng-click='onRemove()' ng-show='hasPermission(\"read\") && !isDisabled() && canRemove()'>" +
|
|
"<i class='fa fa-remove'></i><span x-translate>Remove</span>" +
|
|
"</a>" +
|
|
"</div>" +
|
|
"<div class='icons-bar dropdown' ng-show='viewCanCopy() || viewCanExport()'>" +
|
|
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-caret-down'></i></a>" +
|
|
"<ul class='dropdown-menu'>" +
|
|
"<li ng-show='viewCanCopy()'><a href='' ng-click='onCopy()' x-translate>Duplicate</a></li>" +
|
|
"<li ng-show='viewCanExport()'><a href='' ng-click='onExport()' x-translate>Export</a></li>" +
|
|
"</ul>" +
|
|
"</div>" +
|
|
"</div>" +
|
|
"<div class='panel-body panel-layout'>" +
|
|
"<div ui-view-grid " +
|
|
"x-view='schema' " +
|
|
"x-data-view='dataView' " +
|
|
"x-handler='this' " +
|
|
"x-no-filter='true' " +
|
|
"x-on-init='onGridInit' " +
|
|
"x-on-before-save='onGridBeforeSave' " +
|
|
"x-on-after-save='onGridAfterSave'></div>" +
|
|
"</div>" +
|
|
"</div>";
|
|
|
|
ui.formInput('PanelOneToMany', 'OneToMany', {
|
|
template_editable: null,
|
|
template_readonly: null,
|
|
template: panelRelatedTemplate
|
|
});
|
|
|
|
ui.formInput('PanelManyToMany', 'ManyToMany', {
|
|
template_editable: null,
|
|
template_readonly: null,
|
|
template: panelRelatedTemplate
|
|
});
|
|
|
|
ui.formInput('TagSelect', 'ManyToMany', 'MultiSelect', {
|
|
|
|
css : 'many2many-tags',
|
|
|
|
showTitle: true,
|
|
|
|
init: function(scope) {
|
|
this._super(scope);
|
|
|
|
var nameField = scope.field.targetName || 'id';
|
|
|
|
scope.parse = function(value) {
|
|
return value;
|
|
};
|
|
|
|
scope.formatItem = function(item) {
|
|
if (item && scope._items && item.id in scope._items) {
|
|
item = scope._items[item.id];
|
|
}
|
|
return item ? item[nameField] : item;
|
|
};
|
|
|
|
scope.getItems = function() {
|
|
return _.pluck(this.getSelection(), "value");
|
|
};
|
|
|
|
var _select = scope.select;
|
|
scope.select = function (value) {
|
|
var res = _select.apply(scope, arguments);
|
|
scope.itemsPending = [];
|
|
return res;
|
|
};
|
|
|
|
scope.handleClick = function(e, item) {
|
|
if (scope.field['tag-edit'] && scope.onTagEdit && !scope.isReadonly()) {
|
|
return scope.onTagEdit(e, item);
|
|
}
|
|
scope.showEditor(item);
|
|
};
|
|
},
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
this._super.apply(this, arguments);
|
|
var field = scope.field;
|
|
// special case for json fields
|
|
if (field.jsonField) {
|
|
var nameField = scope.field.targetName;
|
|
var ds = scope._dataSource._new(scope._dataSource._model);
|
|
scope.$watch(attrs.ngModel, function m2mValueWatch(value, old) {
|
|
scope._items = null;
|
|
if (value && value.length && value[0][nameField] === undefined) {
|
|
ds.search({
|
|
fields: ['id', nameField],
|
|
domain: "id in :ids",
|
|
context: { ids: _.pluck(value, 'id') }
|
|
}).success(function (records) {
|
|
scope._items = _.reduce(records, function(memo, item) {
|
|
memo[item.id] = item;
|
|
return memo;
|
|
}, {});
|
|
model.$render();
|
|
});
|
|
}
|
|
}, true);
|
|
}
|
|
},
|
|
|
|
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);
|
|
setTimeout(function() {
|
|
input.focus();
|
|
});
|
|
});
|
|
}
|
|
|
|
scope.loadSelection = function(request, response) {
|
|
|
|
if (!scope.canSelect()) {
|
|
return response([]);
|
|
}
|
|
|
|
this.fetchSelection(request, function(items, page) {
|
|
var term = request.term;
|
|
var text = '<strong><em>' + term + '</em></strong>';
|
|
var canSelect = scope.canSelect() && (items.length < page.total || (request.term && items.length === 0));
|
|
if (field.create && term && scope.canNew()) {
|
|
items.push({
|
|
label : _t('Create "{0}" and select...', text),
|
|
click : function() { create(term); }
|
|
});
|
|
items.push({
|
|
label : _t('Create "{0}"...', text),
|
|
click : function() { create(term, true); }
|
|
});
|
|
}
|
|
if (canSelect) {
|
|
items.push({
|
|
label : _t("Search more..."),
|
|
click : function() { scope.showSelector(); }
|
|
});
|
|
}
|
|
if ((field.create === undefined || (field.create && !term)) && scope.canNew()) {
|
|
items.push({
|
|
label: _t("Create..."),
|
|
click: function() { scope.showPopupEditor(); }
|
|
});
|
|
}
|
|
response(items);
|
|
});
|
|
};
|
|
|
|
scope.matchValues = function(a, b) {
|
|
if (a === b) return true;
|
|
if (!a) return false;
|
|
if (!b) return false;
|
|
return a.id === b.id;
|
|
};
|
|
|
|
var _setValue = scope.setValue;
|
|
scope.setValue = function(value, fireOnChange) {
|
|
var items = _.map(value, function(item) {
|
|
if (item.version === undefined) {
|
|
return item;
|
|
}
|
|
var ver = item.version;
|
|
var val = _.omit(item, "version");
|
|
val.$version = ver;
|
|
return val;
|
|
});
|
|
items = _.isEmpty(items) ? null : items;
|
|
return _setValue.call(this, items, fireOnChange);
|
|
};
|
|
|
|
var _handleSelect = scope.handleSelect;
|
|
scope.handleSelect = function(e, ui) {
|
|
if (ui.item.click) {
|
|
setTimeout(function(){
|
|
input.val("");
|
|
});
|
|
ui.item.click.call(scope);
|
|
return scope.$applyAsync();
|
|
}
|
|
return _handleSelect.apply(this, arguments);
|
|
};
|
|
|
|
var _removeItem = scope.removeItem;
|
|
scope.removeItem = function(e, ui) {
|
|
if (scope.attr('canRemove') === false) return;
|
|
_removeItem.apply(this, arguments);
|
|
};
|
|
|
|
if (scope.field && scope.field['tag-edit']) {
|
|
scope.attachTagEditor(scope, element, attrs);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.InlineOneToManyCtrl = InlineOneToManyCtrl;
|
|
ui.InlineOneToManyCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
|
|
|
function InlineOneToManyCtrl($scope, $element, DataSource, ViewService) {
|
|
|
|
var field = $scope.field || $scope.getViewDef($element);
|
|
var params = {
|
|
model: field.target
|
|
};
|
|
|
|
if (field.editor) {
|
|
params.views = [{
|
|
type: 'grid',
|
|
items: field.editor.items
|
|
}];
|
|
}
|
|
|
|
$scope._viewParams = params;
|
|
|
|
OneToManyCtrl.call(this, $scope, $element, DataSource, ViewService, function(){
|
|
$scope.editorCanSave = false;
|
|
$scope.selectEnable = false;
|
|
});
|
|
}
|
|
|
|
// used in panel form
|
|
ui.formInput('InlineOneToMany', 'OneToMany', {
|
|
|
|
showTitle: true,
|
|
|
|
controller: InlineOneToManyCtrl,
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
this._super.apply(this, arguments);
|
|
|
|
scope.onGridInit = function() {};
|
|
scope.items = [];
|
|
|
|
var showOnNew = (scope.field.editor||{}).showOnNew !== false;
|
|
var unwatch = null;
|
|
|
|
function canAdd() {
|
|
return scope.hasPermission("write") && !scope.isDisabled() && scope.canNew();
|
|
}
|
|
|
|
model.$render = function () {
|
|
if (unwatch) {
|
|
unwatch();
|
|
unwatch = null;
|
|
}
|
|
scope.items = model.$viewValue;
|
|
if ((scope.items && scope.items.length > 0) || scope.$$readonly) {
|
|
return;
|
|
}
|
|
scope.items = showOnNew && canAdd() ? [{}] : [];
|
|
unwatch = scope.$watch('items[0]', function o2mFirstItemWatch(item, old) {
|
|
if (!item) return;
|
|
if (item.$changed) {
|
|
unwatch();
|
|
model.$setViewValue(scope.items);
|
|
}
|
|
item.$changed = true;
|
|
}, true);
|
|
};
|
|
|
|
function isEmpty(record) {
|
|
if (!record || _.isEmpty(record)) return true;
|
|
var values = _.filter(record, function (value, name) {
|
|
return !(/[\$_]/.test(name) || value === null || value === undefined);
|
|
});
|
|
return values.length === 0;
|
|
}
|
|
|
|
function itemsChanged() {
|
|
var items = scope.items;
|
|
if (items && items.length > 0) {
|
|
var changed = false;
|
|
var values = _.filter(items, function (item) {
|
|
if (item.$changed) {
|
|
changed = true;
|
|
}
|
|
return !isEmpty(item);
|
|
});
|
|
if (changed && values.length) {
|
|
model.$setViewValue(values);
|
|
}
|
|
}
|
|
}
|
|
|
|
scope.$watch('items', itemsChanged, true);
|
|
scope.$itemsChanged = itemsChanged;
|
|
|
|
scope.$watch('$$readonly', function o2mReadonlyWatch(readonly, old) {
|
|
if (readonly === undefined) return;
|
|
var items = model.$viewValue;
|
|
if (_.isEmpty(items)) {
|
|
scope.items = (showOnNew && canAdd() && !readonly) ? [{}] : items;
|
|
}
|
|
});
|
|
|
|
scope.$copy = function (record) {
|
|
return angular.copy(record);
|
|
};
|
|
|
|
scope.addItem = function () {
|
|
var items = scope.items || (scope.items = []);
|
|
var item = _.last(items);
|
|
if (items && items.length && isEmpty(item)) {
|
|
return;
|
|
}
|
|
if (canAdd()) {
|
|
items.push({});
|
|
}
|
|
};
|
|
|
|
scope.removeItem = function (index) {
|
|
var items = scope.items;
|
|
items.splice(index, 1);
|
|
var values = _.filter(items, function (item) {
|
|
return !isEmpty(item);
|
|
});
|
|
if (items.length === 0 && showOnNew) {
|
|
scope.addItem();
|
|
}
|
|
model.$setViewValue(values);
|
|
};
|
|
|
|
scope.canRemove = function () {
|
|
return scope.attr('canRemove') !== false;
|
|
};
|
|
|
|
scope.setValidity = function (key, value, record) {
|
|
if (arguments.length === 3) {
|
|
record.$valid = value;
|
|
value = _.all(scope.items, function (x) { return x.$valid; });
|
|
}
|
|
model.$setValidity(key, value);
|
|
};
|
|
|
|
scope.setExclusive = function (name, record) {
|
|
_.each(scope.items, function (item) {
|
|
if (record !== item) {
|
|
item[name] = false;
|
|
}
|
|
});
|
|
};
|
|
},
|
|
|
|
template_readonly:function (scope) {
|
|
var field = scope.field;
|
|
var tmpl = (field.viewer || {}).template;
|
|
if (!tmpl && field.editor && (field.editor.viewer || !field.targetName)) {
|
|
tmpl = '<div class="o2m-editor-form" ui-panel-editor></div>';
|
|
}
|
|
if (!tmpl && field.targetName) {
|
|
tmpl = '{{record.' + field.targetName + '}}';
|
|
}
|
|
tmpl = tmpl || '{{record.id}}';
|
|
return "<div class='o2m-list'>" +
|
|
"<div class='o2m-list-row' ng-class-even=\"'even'\" ng-repeat='record in items' ng-init='$$original = $copy(record)'>" + tmpl + "</div>" +
|
|
"</div>";
|
|
},
|
|
|
|
template_editable: function (scope) {
|
|
return "<div class='o2m-list'>" +
|
|
"<div class='o2m-list-row' ng-class-even=\"'even'\" ng-repeat='record in items' ng-init='$$original = $copy(record)'>" +
|
|
"<div class='o2m-editor-form' ui-panel-editor></div>" +
|
|
"<span class='o2m-list-remove' ng-show='hasPermission(\"remove\") && !isDisabled() && canRemove()'>" +
|
|
"<a tabindex='-1' href='' ng-click='removeItem($index)' title='{{\"Remove\" | t}}'><i class='fa fa-times'></i></a>" +
|
|
"</span>" +
|
|
"</div>" +
|
|
"<div class='o2m-list-row o2m-list-add' ng-show='hasPermission(\"write\") && !isDisabled() && canNew()'>" +
|
|
"<a tabindex='-1' href='' ng-click='addItem()' title='{{\"Add\" | t}}'><i class='fa fa-plus'></i></a>" +
|
|
"</div>" +
|
|
"</div>";
|
|
},
|
|
template: null
|
|
});
|
|
|
|
//used in panel form
|
|
ui.formInput('InlineManyToMany', 'InlineOneToMany', {
|
|
|
|
});
|
|
|
|
// used in editable grid
|
|
ui.formInput('OneToManyInline', 'OneToMany', {
|
|
|
|
css : 'one2many-inline',
|
|
|
|
collapseIfEmpty : false,
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
|
|
this._super.apply(this, arguments);
|
|
|
|
scope.onSort = function() {
|
|
|
|
};
|
|
|
|
var field = scope.field;
|
|
var input = element.children('input');
|
|
var grid = element.children('[ui-slick-grid]');
|
|
|
|
var container = null;
|
|
var wrapper = $('<div class="slick-editor-dropdown"></div>')
|
|
.css("position", "absolute")
|
|
.hide();
|
|
|
|
var render = model.$render,
|
|
renderPending = false;
|
|
model.$render = function() {
|
|
if (wrapper.is(":visible")) {
|
|
renderPending = false;
|
|
render();
|
|
grid.trigger('adjust:size');
|
|
} else {
|
|
renderPending = true;
|
|
}
|
|
};
|
|
|
|
scope.waitForActions(function(){
|
|
container = element.parents('.ui-dialog-content,.view-container').first();
|
|
grid.height(175).appendTo(wrapper);
|
|
wrapper.height(175).appendTo(container);
|
|
});
|
|
|
|
function adjust() {
|
|
if (!wrapper.is(":visible"))
|
|
return;
|
|
if (axelor.device.small) {
|
|
dropdownVisible = false;
|
|
return wrapper.hide();
|
|
}
|
|
wrapper.position({
|
|
my: "left top",
|
|
at: "left bottom",
|
|
of: element,
|
|
within: container
|
|
})
|
|
.zIndex(element.zIndex() + 1)
|
|
.width(element.width());
|
|
}
|
|
|
|
var dropdownVisible = false;
|
|
scope.onDropdown = function () {
|
|
dropdownVisible = !dropdownVisible;
|
|
if (!dropdownVisible) {
|
|
wrapper.hide();
|
|
return;
|
|
}
|
|
if (renderPending) {
|
|
renderPending = false;
|
|
render();
|
|
setTimeout(function () {
|
|
axelor.$adjustSize();
|
|
});
|
|
}
|
|
wrapper.show();
|
|
adjust();
|
|
};
|
|
|
|
scope.canDropdown = function () {
|
|
return !axelor.device.small;
|
|
};
|
|
|
|
scope.canShowAdd = function () {
|
|
return dropdownVisible && scope.canEdit();
|
|
};
|
|
|
|
scope.canShowRemove = function () {
|
|
return dropdownVisible && scope.canRemove() && !_.isEmpty(scope.selection);
|
|
};
|
|
|
|
element.on("hide:slick-editor", function(e){
|
|
dropdownVisible = false;
|
|
wrapper.hide();
|
|
});
|
|
|
|
scope.$onAdjust(adjust, 300);
|
|
|
|
input.on('keydown', function (e) {
|
|
if (e.keyCode === 40 && e.ctrlKey && !dropdownVisible) {
|
|
scope.onDropdown();
|
|
}
|
|
});
|
|
|
|
function hidePopup(e) {
|
|
if (element.is(':hidden')) {
|
|
return;
|
|
}
|
|
var all = element.add(wrapper);
|
|
var elem = $(e.target);
|
|
if (all.is(elem) || all.has(elem).length > 0) return;
|
|
if (elem.zIndex() > element.parents('.slickgrid:first').zIndex()) return;
|
|
if (elem.parents(".ui-dialog:first").zIndex() > element.parents('.slickgrid:first').zIndex()) return;
|
|
|
|
element.trigger('close:slick-editor');
|
|
}
|
|
|
|
$(document).on('mousedown.mini-grid', hidePopup);
|
|
|
|
scope.$watch(attrs.ngModel, function o2mModelWatch(value) {
|
|
var text = "";
|
|
if (value && value.length)
|
|
text = "(" + value.length + ")";
|
|
input.val(text);
|
|
});
|
|
|
|
scope.$watch('schema.loaded', function o2mSchemaWatch(viewLoaded) {
|
|
var schema = scope.schema;
|
|
if (schema && scope.attr('canEdit') === false) {
|
|
schema.editIcon = false;
|
|
}
|
|
});
|
|
|
|
scope.$on("$destroy", function(e){
|
|
wrapper.remove();
|
|
$(document).off('mousedown.mini-grid', hidePopup);
|
|
});
|
|
|
|
scope.canEdit = function () {
|
|
return scope.hasPermission('create') && !scope.isReadonly() && scope.attr('canEdit') !== false;
|
|
};
|
|
|
|
scope.canRemove = function() {
|
|
return scope.hasPermission('create') && !scope.isReadonly() && scope.attr('canEdit') !== false;
|
|
};
|
|
},
|
|
|
|
template_editable: null,
|
|
|
|
template_readonly: null,
|
|
|
|
template:
|
|
'<span class="picker-input picker-icons-2" style="position: absolute;">'+
|
|
'<input type="text" readonly>'+
|
|
'<span class="picker-icons">'+
|
|
'<i class="fa fa-plus" ng-click="onSelect()" ng-show="canShowAdd()" title="{{\'Select\' | t}}"></i>'+
|
|
'<i class="fa fa-minus" ng-click="onRemove()" ng-show="canShowRemove()" title="{{\'Select\' | t}}"></i>'+
|
|
'<i class="fa fa-caret-down" ng-show="canDropdown()" ng-click="onDropdown()" title="{{\'Show\' | t}}"></i>'+
|
|
'</span>'+
|
|
'<div ui-view-grid ' +
|
|
'x-view="schema" '+
|
|
'x-data-view="dataView" '+
|
|
'x-handler="this" '+
|
|
'x-no-filter="true" '+
|
|
'x-on-init="onGridInit" '+
|
|
'x-on-before-save="onGridBeforeSave" '+
|
|
'x-on-after-save="onGridAfterSave" '+
|
|
'></div>'+
|
|
'</span>'
|
|
});
|
|
|
|
ui.formInput('ManyToManyInline', 'OneToManyInline', {
|
|
|
|
css : 'many2many-inline',
|
|
|
|
controller: ManyToManyCtrl,
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
this._super.apply(this, arguments);
|
|
}
|
|
});
|
|
|
|
})();
|