First commit waiting for Budget Alert
This commit is contained in:
133
sophal/js/application.js
Normal file
133
sophal/js/application.js
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
//= lib/underscore/underscore.js
|
||||
//= lib/underscore/underscore.string.js
|
||||
//= lib/moment/moment.js
|
||||
|
||||
//= lib/jquery.ui/js/jquery.js
|
||||
//= lib/jquery.ui/js/jquery-ui.js
|
||||
//= lib/jquery.ui/js/jquery.ui.mask.js
|
||||
//= lib/bootstrap/js/bootstrap.js
|
||||
//= lib/jquery.timepicker/jquery-ui-timepicker-addon.js
|
||||
|
||||
//= lib/slickgrid/lib/jquery.event.drag-2.2.js
|
||||
//= lib/slickgrid/lib/jquery.event.drop-2.2.js
|
||||
//= lib/slickgrid/slick.core.js
|
||||
//= lib/slickgrid/slick.grid.js
|
||||
//= lib/slickgrid/slick.dataview.js
|
||||
//= lib/slickgrid/slick.groupitemmetadataprovider.js
|
||||
//= lib/slickgrid/plugins/slick.headermenu.js
|
||||
//= lib/slickgrid/plugins/slick.rowselectionmodel.js
|
||||
//= lib/slickgrid/plugins/slick.checkboxselectcolumn.js
|
||||
//= lib/slickgrid/plugins/slick.rowmovemanager.js
|
||||
|
||||
//= lib/jquery.treetable/js/jquery.treetable.js
|
||||
|
||||
//= lib/fullcalendar/fullcalendar.js
|
||||
//= lib/fullcalendar/gcal.js
|
||||
|
||||
//= lib/dhtmlxGantt/dhtmlxgantt.js
|
||||
|
||||
//= lib/handsontable/handsontable.full.js
|
||||
|
||||
//= lib/d3/d3.js
|
||||
//= lib/d3/nv/nv.d3.js
|
||||
//= lib/d3/radar/radar-chart.js
|
||||
//= lib/d3/gauge/gauge-chart.js
|
||||
//= lib/d3/funnel/d3-funnel.js
|
||||
|
||||
//= lib/codemirror/codemirror.min.js
|
||||
//= lib/wysiwyg/wysiwyg.js
|
||||
//= lib/wysiwyg/wysiwyg-editor.js
|
||||
|
||||
//= lib/angular/angular.js
|
||||
//= lib/angular/angular-route.js
|
||||
//= lib/angular/angular-resource.js
|
||||
//= lib/angular/angular-sanitize.js
|
||||
//= lib/angular/angular-animate.js
|
||||
|
||||
//= lib/angular-ui/sortable.js
|
||||
|
||||
//= js/axelor.ns.js
|
||||
//= js/axelor.ng.js
|
||||
|
||||
//= js/lib/i18n.js
|
||||
//= js/lib/utils.js
|
||||
//= js/lib/dialogs.js
|
||||
//= js/lib/tabs.js
|
||||
|
||||
//= js/axelor.auth.js
|
||||
//= js/axelor.app.js
|
||||
//= js/axelor.ds.js
|
||||
//= js/axelor.data.js
|
||||
//= js/axelor.ui.js
|
||||
//= js/axelor.nav.js
|
||||
//= js/axelor.prefs.js
|
||||
|
||||
//= js/widget/widget.navtabs.js
|
||||
//= js/widget/widget.navtree.js
|
||||
//= js/widget/widget.navmenu.js
|
||||
//= js/widget/widget.slickgrid.js
|
||||
//= js/widget/widget.dialog.js
|
||||
//= js/widget/widget.update.js
|
||||
//= js/widget/widget.search.js
|
||||
//= js/widget/widget.menubar.js
|
||||
|
||||
//= js/form/form.base.js
|
||||
//= js/form/form.converters.js
|
||||
//= js/form/form.actions.js
|
||||
//= js/form/form.widget.js
|
||||
//= js/form/form.layout.js
|
||||
//= js/form/form.container.js
|
||||
|
||||
//= js/form/form.input.static.js
|
||||
//= js/form/form.input.boolean.js
|
||||
//= js/form/form.input.text.js
|
||||
//= js/form/form.input.html.js
|
||||
//= js/form/form.input.number.js
|
||||
//= js/form/form.input.datetime.js
|
||||
//= js/form/form.input.select.js
|
||||
//= js/form/form.input.progress.js
|
||||
//= js/form/form.input.binary.js
|
||||
//= js/form/form.input.spreadsheet.js
|
||||
|
||||
//= js/form/form.relational.base.js
|
||||
//= js/form/form.relational.single.js
|
||||
//= js/form/form.relational.multiple.js
|
||||
//= js/form/form.relational.nested.js
|
||||
|
||||
//= js/form/form.input.json.js
|
||||
|
||||
//= js/form/form.mail.js
|
||||
//= js/form/form.code.js
|
||||
|
||||
//= js/view/view.base.js
|
||||
//= js/view/view.form.js
|
||||
//= js/view/view.grid.js
|
||||
//= js/view/view.tree.js
|
||||
//= js/view/view.html.js
|
||||
//= js/view/view.search.js
|
||||
//= js/view/view.portal.js
|
||||
//= js/view/view.dashboard.js
|
||||
//= js/view/view.popup.js
|
||||
//= js/view/view.chart.js
|
||||
//= js/view/view.calendar.js
|
||||
//= js/view/view.gantt.js
|
||||
//= js/view/view.kanban.js
|
||||
//= js/view/view.custom.js
|
||||
//= js/view/view.dms.js
|
||||
20
sophal/js/application.login.js
Normal file
20
sophal/js/application.login.js
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
//= lib/jquery.ui/js/jquery.js
|
||||
//= lib/bootstrap/js/bootstrap.js
|
||||
//= js/axelor.ns.js
|
||||
688
sophal/js/axelor.app.js
Normal file
688
sophal/js/axelor.app.js
Normal file
@ -0,0 +1,688 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
/**
|
||||
* Application Module
|
||||
*
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var loadingElem = null,
|
||||
loadingTimer = null,
|
||||
loadingCounter = 0;
|
||||
|
||||
function updateLoadingCounter(val) {
|
||||
loadingCounter += val;
|
||||
loadingCounter = Math.max(0, loadingCounter);
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
if (loadingTimer) {
|
||||
clearTimeout(loadingTimer);
|
||||
loadingTimer = null;
|
||||
}
|
||||
if (loadingCounter > 0) {
|
||||
loadingTimer = _.delay(hideLoading, 500);
|
||||
return;
|
||||
}
|
||||
loadingTimer = _.delay(function () {
|
||||
loadingTimer = null;
|
||||
if (loadingElem) {
|
||||
loadingElem.fadeOut(100);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function onHttpStart() {
|
||||
|
||||
updateLoadingCounter(1);
|
||||
|
||||
if (loadingTimer) {
|
||||
clearTimeout(loadingTimer);
|
||||
loadingTimer = null;
|
||||
}
|
||||
|
||||
if (loadingElem === null) {
|
||||
loadingElem = $('<div><span class="label label-important loading-counter">' + _t('Loading') + '...</span></div>')
|
||||
.css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
'text-align': 'center',
|
||||
'z-index': 2000
|
||||
}).appendTo('body');
|
||||
}
|
||||
loadingElem.show();
|
||||
}
|
||||
|
||||
function onHttpStop() {
|
||||
updateLoadingCounter(-1);
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
axelor.$evalScope = function (scope) {
|
||||
|
||||
var evalScope = scope.$new(true);
|
||||
|
||||
function isValid(name) {
|
||||
if (!name) {
|
||||
if (_.isFunction(scope.isValid)) {
|
||||
return scope.isValid();
|
||||
}
|
||||
return scope.isValid === undefined || scope.isValid;
|
||||
}
|
||||
|
||||
var ctrl = scope.form;
|
||||
if (ctrl) {
|
||||
ctrl = ctrl[name];
|
||||
}
|
||||
if (ctrl) {
|
||||
return ctrl.$valid;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
evalScope.$get = function(n) {
|
||||
var context = this.$context || this.record || {};
|
||||
if (context.hasOwnProperty(n)) {
|
||||
return context[n];
|
||||
}
|
||||
return evalScope.$eval(n, context);
|
||||
};
|
||||
evalScope.$moment = function(d) { return moment(d); }; // moment.js helper
|
||||
evalScope.$number = function(d) { return +d; }; // number helper
|
||||
evalScope.$popup = function() { return scope._isPopup; }; // popup detect
|
||||
evalScope.$iif = function(c, t, f) {
|
||||
console.warn('Use ternary operator instead of $iif() helper.');
|
||||
return c ? t : f;
|
||||
};
|
||||
|
||||
evalScope.$sum = function (items, field, operation, field2) {
|
||||
var total = 0;
|
||||
if (items && items.length) {
|
||||
items.forEach(function (item) {
|
||||
var value = 0;
|
||||
var value2 = 0;
|
||||
if (field in item) {
|
||||
value = +(item[field] || 0);
|
||||
}
|
||||
if (operation && field2 && field2 in item) {
|
||||
value2 = +(item[field2] || 0);
|
||||
switch (operation) {
|
||||
case '*':
|
||||
value = value * value2;
|
||||
break;
|
||||
case '/':
|
||||
value = value2 ? value / value2 : value;
|
||||
break;
|
||||
case '+':
|
||||
value = value + value2;
|
||||
break;
|
||||
case '-':
|
||||
value = value - value2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
total += value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
// current user and group
|
||||
evalScope.$user = axelor.config['user.login'];
|
||||
evalScope.$group = axelor.config['user.group'];
|
||||
evalScope.$userId = axelor.config['user.id'];
|
||||
|
||||
evalScope.$contains = function(iter, item) {
|
||||
if (iter && iter.indexOf)
|
||||
return iter.indexOf(item) > -1;
|
||||
return false;
|
||||
};
|
||||
|
||||
// access json field values
|
||||
evalScope.$json = function (name) {
|
||||
var value = (scope.record || {})[name];
|
||||
if (value) {
|
||||
return angular.fromJson(value);
|
||||
}
|
||||
};
|
||||
|
||||
evalScope.$readonly = scope.isReadonly ? _.bind(scope.isReadonly, scope) : angular.noop;
|
||||
evalScope.$required = scope.isRequired ? _.bind(scope.isRequired, scope) : angular.noop;
|
||||
|
||||
evalScope.$valid = function(name) {
|
||||
return isValid(scope, name);
|
||||
};
|
||||
|
||||
evalScope.$invalid = function(name) {
|
||||
return !isValid(scope, name);
|
||||
};
|
||||
|
||||
return evalScope;
|
||||
};
|
||||
|
||||
axelor.$eval = function (scope, expr, context) {
|
||||
if (!scope || !expr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var evalScope = axelor.$evalScope(scope);
|
||||
evalScope.$context = context;
|
||||
try {
|
||||
return evalScope.$eval(expr, context);
|
||||
} finally {
|
||||
evalScope.$destroy();
|
||||
evalScope = null;
|
||||
}
|
||||
};
|
||||
|
||||
axelor.$adjustSize = _.debounce(function () {
|
||||
$(document).trigger('adjust:size');
|
||||
}, 100);
|
||||
|
||||
var module = angular.module('axelor.app', ['axelor.ng', 'axelor.ds', 'axelor.ui', 'axelor.auth']);
|
||||
|
||||
module.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
|
||||
var tabResource = {
|
||||
action: 'main.tab',
|
||||
controller: 'TabCtrl',
|
||||
template: "<span><!-- dummy template --></span>"
|
||||
};
|
||||
|
||||
$routeProvider
|
||||
|
||||
.when('/preferences', { action: 'preferences' })
|
||||
.when('/about', { action: 'about' })
|
||||
.when('/system', { action: 'system' })
|
||||
.when('/', { action: 'main' })
|
||||
|
||||
.when('/ds/:resource', tabResource)
|
||||
.when('/ds/:resource/:mode', tabResource)
|
||||
.when('/ds/:resource/:mode/:state', tabResource)
|
||||
|
||||
.otherwise({ redirectTo: '/' });
|
||||
}]);
|
||||
|
||||
module.config(['$httpProvider', function(provider) {
|
||||
|
||||
provider.useApplyAsync(true);
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
function isFile(obj) {
|
||||
return toString.call(obj) === '[object File]';
|
||||
}
|
||||
|
||||
function isFormData(obj) {
|
||||
return toString.call(obj) === '[object FormData]';
|
||||
}
|
||||
|
||||
function isBlob(obj) {
|
||||
return toString.call(obj) === '[object Blob]';
|
||||
}
|
||||
|
||||
// restore old behavior
|
||||
// breaking change (https://github.com/angular/angular.js/commit/c054288c9722875e3595e6e6162193e0fb67a251)
|
||||
function jsonReplacer(key, value) {
|
||||
if (typeof key === 'string' && key.charAt(0) === '$') {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function transformRequest(d) {
|
||||
return angular.isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? JSON.stringify(d, jsonReplacer) : d;
|
||||
}
|
||||
|
||||
provider.interceptors.push('httpIndicator');
|
||||
provider.defaults.transformRequest.unshift(transformRequest);
|
||||
provider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
|
||||
provider.useApplyAsync(true);
|
||||
}]);
|
||||
|
||||
// only enable animation on element with ng-animate css class
|
||||
module.config(['$animateProvider', function($animateProvider) {
|
||||
$animateProvider.classNameFilter(/x-animate/);
|
||||
}]);
|
||||
|
||||
module.factory('httpIndicator', ['$rootScope', '$q', function($rootScope, $q){
|
||||
|
||||
var doc = $(document);
|
||||
var body = $('body');
|
||||
var blocker = $('<div class="blocker-overlay"></div>')
|
||||
.appendTo('body')
|
||||
.hide()
|
||||
.css({
|
||||
position: 'absolute',
|
||||
zIndex: 100000,
|
||||
width: '100%', height: '100%'
|
||||
});
|
||||
|
||||
var spinner = $('<div class="blocker-wait"></div>')
|
||||
.append('<div class="blocker-spinner"><i class="fa fa-spinner fa-spin"></div>')
|
||||
.append('<div class="blocker-message">' + _t('Please wait...') + '</div>')
|
||||
.appendTo(blocker);
|
||||
|
||||
var blocked = false;
|
||||
var blockedCounter = 0;
|
||||
var blockedTimer = null;
|
||||
var spinnerTime = 0;
|
||||
|
||||
function block(callback) {
|
||||
if (blocked) return true;
|
||||
if (blockedTimer) { clearTimeout(blockedTimer); blockedTimer = null; }
|
||||
if (loadingCounter > 0 || blockedCounter > 0) {
|
||||
blocked = true;
|
||||
doc.on("keydown.blockui mousedown.blockui", function(e) {
|
||||
if ($('#loginWindow').is(':visible')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
body.css("cursor", "wait");
|
||||
blocker.show();
|
||||
}
|
||||
unblock(callback);
|
||||
return blocked;
|
||||
}
|
||||
|
||||
function unblock(callback) {
|
||||
if (blockedTimer) { clearTimeout(blockedTimer); blockedTimer = null; }
|
||||
if (loadingCounter > 0 || blockedCounter > 0 || loadingTimer) {
|
||||
if (spinnerTime === 0) {
|
||||
spinnerTime = moment();
|
||||
}
|
||||
// show spinner after 5 seconds
|
||||
if (moment().diff(spinnerTime, "seconds") > 5) {
|
||||
blocker.addClass('wait');
|
||||
}
|
||||
if (blockedCounter > 0) {
|
||||
blockedCounter = blockedCounter - 10;
|
||||
}
|
||||
blockedTimer = _.delay(unblock, 200, callback);
|
||||
return;
|
||||
}
|
||||
doc.off("keydown.blockui mousedown.blockui");
|
||||
body.css("cursor", "");
|
||||
blocker.removeClass('wait').hide();
|
||||
spinnerTime = 0;
|
||||
if (callback) {
|
||||
callback(blocked);
|
||||
}
|
||||
blocked = false;
|
||||
}
|
||||
|
||||
axelor.blockUI = function(callback, minimum) {
|
||||
if (minimum && minimum > blockedCounter) {
|
||||
blockedCounter = Math.max(0, blockedCounter);
|
||||
blockedCounter = Math.max(minimum, blockedCounter);
|
||||
}
|
||||
return block(callback);
|
||||
};
|
||||
|
||||
axelor.unblockUI = function() {
|
||||
return unblock();
|
||||
};
|
||||
|
||||
function notSilent(config) {
|
||||
return config && !config.silent;
|
||||
}
|
||||
|
||||
return {
|
||||
request: function(config) {
|
||||
if (notSilent(config)) {
|
||||
onHttpStart();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
response: function(response) {
|
||||
if (notSilent(response.config)) {
|
||||
onHttpStop();
|
||||
}
|
||||
if (response.data) {
|
||||
if (response.data.status === -1) { // STATUS_FAILURE
|
||||
if (notSilent(response.config)) $rootScope.$broadcast('event:http-error', response.data);
|
||||
return $q.reject(response);
|
||||
}
|
||||
if (response.data.status === -7) { // STATUS_LOGIN_REQUIRED
|
||||
if (axelor.config['auth.central.client']) {
|
||||
// redirect to central login page
|
||||
window.location.href = './?client_name=' + axelor.config['auth.central.client']
|
||||
+ "&hash_location=" + encodeURIComponent(window.location.hash);
|
||||
} else if (notSilent(response.config)) {
|
||||
// ajax login
|
||||
$rootScope.$broadcast('event:auth-loginRequired', response.data);
|
||||
}
|
||||
return $q.reject(response);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
responseError: function(error) {
|
||||
if (notSilent(error.config)) {
|
||||
onHttpStop();
|
||||
$rootScope.$broadcast('event:http-error', error);
|
||||
}
|
||||
return $q.reject(error);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
module.filter('unaccent', function() {
|
||||
var source = 'ąàáäâãåæăćčĉęèéëêĝĥìíïîĵłľńňòóöőôõðøśșşšŝťțţŭùúüűûñÿýçżźž';
|
||||
var target = 'aaaaaaaaaccceeeeeghiiiijllnnoooooooossssstttuuuuuunyyczzz';
|
||||
|
||||
source += source.toUpperCase();
|
||||
target += target.toUpperCase();
|
||||
|
||||
var unaccent = function (text) {
|
||||
return typeof(text) !== 'string' ? text : text.replace(/.{1}/g, function(c) {
|
||||
var i = source.indexOf(c);
|
||||
return i === -1 ? c : target[i];
|
||||
});
|
||||
};
|
||||
return function(input) {
|
||||
return unaccent(input);
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('t', function(){
|
||||
return function(input) {
|
||||
var t = _t || angular.nop;
|
||||
return t(input);
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('translate', function(){
|
||||
return function(scope, element, attrs) {
|
||||
var t = _t || angular.nop;
|
||||
setTimeout(function(){
|
||||
element.html(t(element.text()));
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('AppCtrl', AppCtrl);
|
||||
|
||||
AppCtrl.$inject = ['$rootScope', '$exceptionHandler', '$scope', '$http', '$route', 'authService', 'MessageService', 'NavService'];
|
||||
function AppCtrl($rootScope, $exceptionHandler, $scope, $http, $route, authService, MessageService, NavService) {
|
||||
|
||||
function fetchConfig() {
|
||||
return $http.get('ws/app/info').then(function(response) {
|
||||
var config = _.extend(axelor.config, response.data);
|
||||
$scope.$user.id = config["user.id"];
|
||||
$scope.$user.name = config["user.name"];
|
||||
$scope.$user.image = config["user.image"];
|
||||
config.DEV = config['application.mode'] == 'dev';
|
||||
config.PROD = config['application.mode'] == 'prod';
|
||||
|
||||
if (config['view.confirm.yes-no'] === true) {
|
||||
_.extend(axelor.dialogs.config, {
|
||||
yesNo: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openHomeTab() {
|
||||
var path = $scope.routePath;
|
||||
var homeAction = axelor.config["user.action"];
|
||||
if (!homeAction || _.last(path) !== "main") {
|
||||
return;
|
||||
}
|
||||
NavService.openTabByName(homeAction, {
|
||||
__tab_prepend: true,
|
||||
__tab_closable: false
|
||||
});
|
||||
}
|
||||
|
||||
// load app config
|
||||
fetchConfig().then(function () {
|
||||
openHomeTab();
|
||||
});
|
||||
|
||||
$scope.$user = {};
|
||||
$scope.$year = moment().year();
|
||||
$scope.openHomeTab = openHomeTab;
|
||||
$scope.$unreadMailCount = function () {
|
||||
return MessageService.unreadCount();
|
||||
};
|
||||
|
||||
$scope.showMailBox = function() {
|
||||
NavService.openTabByName('mail.inbox');
|
||||
$scope.$timeout(function () {
|
||||
$scope.$broadcast("on:nav-click", NavService.getSelected());
|
||||
});
|
||||
};
|
||||
|
||||
var loginAttempts = 0;
|
||||
var loginWindow = null;
|
||||
var errorWindow = null;
|
||||
|
||||
function showLogin(hide) {
|
||||
|
||||
if (loginWindow === null) {
|
||||
loginWindow = $('#loginWindow')
|
||||
.attr('title', _t('Log in'))
|
||||
.dialog({
|
||||
dialogClass: 'no-close ui-dialog-responsive ui-dialog-small',
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
position: "center",
|
||||
width: "auto",
|
||||
resizable: false,
|
||||
closeOnEscape: false,
|
||||
zIndex: 100001,
|
||||
show: {
|
||||
effect: 'fade',
|
||||
duration: 300
|
||||
},
|
||||
buttons: [{
|
||||
text: _t("Log in"),
|
||||
'class': 'btn btn-primary',
|
||||
click: function(){
|
||||
$scope.doLogin();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
$('#loginWindow input').keypress(function(event){
|
||||
if (event.keyCode === 13)
|
||||
$scope.doLogin();
|
||||
});
|
||||
}
|
||||
return loginWindow.dialog(hide ? 'close' : 'open').height('auto');
|
||||
}
|
||||
|
||||
function showError(hide) {
|
||||
if (errorWindow === null) {
|
||||
errorWindow = $('#errorWindow')
|
||||
.attr('title', _t('Error'))
|
||||
.dialog({
|
||||
dialogClass: 'ui-dialog-error ui-dialog-responsive',
|
||||
draggable: true,
|
||||
resizable: false,
|
||||
closeOnEscape: true,
|
||||
modal: true,
|
||||
zIndex: 1100,
|
||||
width: 420,
|
||||
open: function(e, ui) {
|
||||
setTimeout(function () {
|
||||
if (errorWindow.dialog('isOpen')) {
|
||||
errorWindow.dialog('moveToTop', true);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
close: function() {
|
||||
$scope.httpError = {};
|
||||
$scope.$applyAsync();
|
||||
},
|
||||
show: {
|
||||
effect: 'fade',
|
||||
duration: 300
|
||||
},
|
||||
buttons: [{
|
||||
text: _t("Show Details"),
|
||||
'class': 'btn',
|
||||
click: function(){
|
||||
var elem = $(this);
|
||||
$scope.onErrorWindowShow('stacktrace');
|
||||
$scope.$applyAsync(function () {
|
||||
setTimeout(function () {
|
||||
var maxHeight = $(document).height() - 132;
|
||||
var height = maxHeight;
|
||||
if (height > elem[0].scrollHeight) {
|
||||
height = elem[0].scrollHeight + 8;
|
||||
}
|
||||
elem.height(height);
|
||||
elem.dialog('option', 'position', 'center');
|
||||
elem.dialog('widget').height(elem.dialog('widget').height());
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
text: _t("Close"),
|
||||
'class': 'btn btn-primary',
|
||||
click: function() {
|
||||
errorWindow.dialog('close');
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
return errorWindow
|
||||
.dialog(hide ? 'close' : 'open')
|
||||
.dialog('widget').css('top', 6)
|
||||
.height('auto');
|
||||
}
|
||||
|
||||
function showNotification(options) {
|
||||
axelor.notify.error('<p>' + options.message.replace('\n', '<br>') + '</p>', {
|
||||
title: options.title || options.type || _t('Error')
|
||||
});
|
||||
}
|
||||
|
||||
$scope.doLogin = function() {
|
||||
|
||||
var data = {
|
||||
username: $('#loginWindow form input:first').val(),
|
||||
password: $('#loginWindow form input:last').val()
|
||||
};
|
||||
|
||||
var last = axelor.config["user.login"];
|
||||
|
||||
$http.post('callback', data).then(function(response){
|
||||
authService.loginConfirmed();
|
||||
$('#loginWindow form input').val('');
|
||||
$('#loginWindow .alert').hide();
|
||||
if (last !== data.username) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('event:auth-loginRequired', function(event, status) {
|
||||
$('#loginWindow .alert').hide();
|
||||
showLogin();
|
||||
if (loginAttempts++ > 0)
|
||||
$('#loginWindow .alert.login-failed').show();
|
||||
if (status === 0 || status === 502)
|
||||
$('#loginWindow .alert.login-offline').show();
|
||||
setTimeout(function(){
|
||||
$('#loginWindow input:first').focus();
|
||||
}, 300);
|
||||
});
|
||||
$scope.$on('event:auth-loginConfirmed', function() {
|
||||
showLogin(true);
|
||||
loginAttempts = 0;
|
||||
fetchConfig();
|
||||
});
|
||||
|
||||
$scope.httpError = {};
|
||||
$scope.$on('event:http-error', function(event, data) {
|
||||
var message = _t("Internal Server Error"),
|
||||
report = data.data || data, stacktrace = null, cause = null, exception;
|
||||
|
||||
// unauthorized errors are handled separately
|
||||
if (data.status === 401) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.popup && report.message) {
|
||||
return axelor.dialogs.box(report.message, {
|
||||
title: report.title
|
||||
});
|
||||
} else if (report.stacktrace) {
|
||||
message = report.message || report.string;
|
||||
exception = report['class'] || '';
|
||||
|
||||
if (exception.match(/(OptimisticLockException|StaleObjectStateException)/)) {
|
||||
message = "<b>" + _t('Concurrent updates error') + '</b><br>' + message;
|
||||
}
|
||||
|
||||
stacktrace = report.stacktrace;
|
||||
cause = report.cause;
|
||||
} else if (report.message) {
|
||||
return showNotification(report);
|
||||
} else if (_.isString(report)) {
|
||||
stacktrace = report.replace(/.*<body>|<\/body>.*/g, '');
|
||||
} else {
|
||||
return; // no error report, so ignore
|
||||
}
|
||||
_.extend($scope.httpError, {
|
||||
message: message,
|
||||
stacktrace: stacktrace,
|
||||
cause: cause
|
||||
});
|
||||
showError();
|
||||
});
|
||||
$scope.onErrorWindowShow = function(what) {
|
||||
$scope.httpError.show = what;
|
||||
};
|
||||
|
||||
$scope.$on('$routeChangeSuccess', function(event, current, prev) {
|
||||
|
||||
var route = current.$$route,
|
||||
path = route && route.action ? route.action.split('.') : null;
|
||||
|
||||
if (path) {
|
||||
$scope.routePath = path;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.routePath = ["main"];
|
||||
$route.reload();
|
||||
}
|
||||
|
||||
//trigger adjustSize event on window resize -->
|
||||
$(function(){
|
||||
$(window).resize(function(event){
|
||||
if (!event.isTrigger) {
|
||||
$(document).trigger('adjust:size');
|
||||
}
|
||||
$('body').toggleClass('device-small', axelor.device.small);
|
||||
$('body').toggleClass('device-mobile', axelor.device.mobile);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
103
sophal/js/axelor.auth.js
Normal file
103
sophal/js/axelor.auth.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
/**
|
||||
* @license HTTP Auth Interceptor Module for AngularJS
|
||||
* (c) 2012 Witold Szczerba
|
||||
* License: MIT
|
||||
*/
|
||||
(function () {
|
||||
|
||||
"use strict";
|
||||
|
||||
angular.module('axelor.auth', []).provider('authService', function() {
|
||||
/**
|
||||
* Holds all the requests which failed due to 401 response,
|
||||
* so they can be re-requested in future, once login is completed.
|
||||
*/
|
||||
var buffer = [];
|
||||
|
||||
/**
|
||||
* Required by HTTP interceptor.
|
||||
* Function is attached to provider to be invisible for regular users of this service.
|
||||
*/
|
||||
this.pushToBuffer = function(config, deferred) {
|
||||
buffer.push({
|
||||
config: config,
|
||||
deferred: deferred
|
||||
});
|
||||
};
|
||||
|
||||
this.$get = ['$rootScope','$injector', function($rootScope, $injector) {
|
||||
var $http; //initialized later because of circular dependency problem
|
||||
function retry(config, deferred) {
|
||||
setTimeout(axelor.blockUI);
|
||||
$http = $http || $injector.get('$http');
|
||||
$http(config).then(function(response) {
|
||||
deferred.resolve(response);
|
||||
});
|
||||
}
|
||||
function retryAll() {
|
||||
for (var i = 0; i < buffer.length; ++i) {
|
||||
retry(buffer[i].config, buffer[i].deferred);
|
||||
}
|
||||
buffer = [];
|
||||
}
|
||||
return {
|
||||
loginConfirmed: function() {
|
||||
$rootScope.$broadcast('event:auth-loginConfirmed');
|
||||
retryAll();
|
||||
}
|
||||
};
|
||||
}];
|
||||
})
|
||||
|
||||
/**
|
||||
* $http interceptor.
|
||||
* On 401 response - it stores the request and broadcasts 'event:angular-auth-loginRequired'.
|
||||
*/
|
||||
.config(['$httpProvider', 'authServiceProvider', function($httpProvider, authServiceProvider) {
|
||||
|
||||
$httpProvider.interceptors.push(['$rootScope', '$q', function($rootScope, $q) {
|
||||
return {
|
||||
responseError: function error(response) {
|
||||
if (response.status === 401 && axelor.config['auth.central.client']) {
|
||||
// redirect to central login page
|
||||
window.location.href = './?client_name=' + axelor.config['auth.central.client']
|
||||
+ "&hash_location=" + encodeURIComponent(window.location.hash);
|
||||
return $q.reject(response);
|
||||
}
|
||||
if (response.status === 401 || response.status === 502 || (response.status === 0 && !response.data)) {
|
||||
var deferred = $q.defer();
|
||||
authServiceProvider.pushToBuffer(response.config, deferred);
|
||||
if (!response.config.silent) {
|
||||
$rootScope.$broadcast('event:auth-loginRequired', response.status);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
// redirect to the CAS login page
|
||||
if (response.status === 302 || response.status === 307) {
|
||||
window.location.reload();
|
||||
}
|
||||
// otherwise
|
||||
return $q.reject(response);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
|
||||
})();
|
||||
1067
sophal/js/axelor.data.js
Normal file
1067
sophal/js/axelor.data.js
Normal file
File diff suppressed because it is too large
Load Diff
851
sophal/js/axelor.ds.js
Normal file
851
sophal/js/axelor.ds.js
Normal file
@ -0,0 +1,851 @@
|
||||
/*
|
||||
* 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 ds = angular.module('axelor.ds', ['ngResource']);
|
||||
|
||||
var forEach = angular.forEach,
|
||||
extend = angular.extend,
|
||||
isArray = angular.isArray;
|
||||
|
||||
ds.factory('MenuService', ['$http', function($http) {
|
||||
|
||||
function get(parent) {
|
||||
|
||||
return $http.get('ws/action/menu', {
|
||||
cache: true,
|
||||
params : {
|
||||
parent : parent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function all() {
|
||||
return $http.get('ws/action/menu/all', {
|
||||
cache: true
|
||||
});
|
||||
}
|
||||
|
||||
function tags() {
|
||||
return $http.get('ws/action/menu/tags', {
|
||||
silent: true,
|
||||
transformRequest: []
|
||||
});
|
||||
}
|
||||
|
||||
function action(name, options) {
|
||||
|
||||
return $http.post('ws/action/' + name, {
|
||||
model : 'com.axelor.meta.db.MetaAction',
|
||||
data : options
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
get: get,
|
||||
all: all,
|
||||
tags: tags,
|
||||
action: action
|
||||
};
|
||||
}]);
|
||||
|
||||
ds.factory('TagService', ['$q', '$timeout', '$rootScope', 'MenuService', function($q, $timeout, $rootScope, MenuService) {
|
||||
|
||||
var POLL_INTERVAL = 10000;
|
||||
|
||||
var pollResult = {};
|
||||
var pollPromise = null;
|
||||
var pollIdle = null;
|
||||
|
||||
var listeners = [];
|
||||
|
||||
function cancelPolling() {
|
||||
if (pollPromise) {
|
||||
$timeout.cancel(pollPromise);
|
||||
pollPromise = null;
|
||||
}
|
||||
if (pollIdle) {
|
||||
clearTimeout(pollIdle);
|
||||
pollIdle = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollPromise === null) {
|
||||
findTags();
|
||||
}
|
||||
}
|
||||
|
||||
var starting = false;
|
||||
function findTags() {
|
||||
if (starting) { return; }
|
||||
if (pollPromise) {
|
||||
$timeout.cancel(pollPromise);
|
||||
}
|
||||
starting = true;
|
||||
MenuService.tags().success(function (res) {
|
||||
var data = _.first(res.data);
|
||||
var values = data.values;
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i](values);
|
||||
}
|
||||
pollPromise = $timeout(findTags, POLL_INTERVAL);
|
||||
if (pollIdle === null) {
|
||||
pollIdle = setTimeout(cancelPolling, POLL_INTERVAL * 2);
|
||||
}
|
||||
starting = false;
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", startPolling, false);
|
||||
window.addEventListener("mousedown", startPolling, false);
|
||||
window.addEventListener("keypress", startPolling, false);
|
||||
window.addEventListener("DOMMouseScroll", startPolling, false);
|
||||
window.addEventListener("mousewheel", startPolling, false);
|
||||
window.addEventListener("touchmove", startPolling, false);
|
||||
window.addEventListener("MSPointerMove", startPolling, false);
|
||||
|
||||
// start polling
|
||||
startPolling();
|
||||
|
||||
return {
|
||||
find: findTags,
|
||||
listen: function(listener) {
|
||||
listeners.push(listener);
|
||||
return function () {
|
||||
var i = listeners.indexOf(listener);
|
||||
if (i >= 0) {
|
||||
listeners.splice(i, 1);
|
||||
}
|
||||
return listener;
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ds.factory('ViewService', ['$http', '$q', '$cacheFactory', '$compile', function($http, $q, $cacheFactory, $compile) {
|
||||
|
||||
var ViewService = function() {
|
||||
|
||||
};
|
||||
|
||||
ViewService.prototype.accept = function(params) {
|
||||
var views = {};
|
||||
forEach(params.views, function(view){
|
||||
var type = view.type || view.viewType;
|
||||
params.viewType = params.viewType || type;
|
||||
views[type] = extend({}, view, {
|
||||
deferred: $q.defer()
|
||||
});
|
||||
});
|
||||
return views;
|
||||
};
|
||||
|
||||
ViewService.prototype.compile = function(template) {
|
||||
return $compile(template);
|
||||
};
|
||||
|
||||
ViewService.prototype.process = function(meta, view, parent) {
|
||||
|
||||
var fields = {};
|
||||
|
||||
meta = meta || {};
|
||||
view = view || {};
|
||||
|
||||
if (meta.jsonAttrs && view && view.items) {
|
||||
if (view.type === 'grid') {
|
||||
view.items = (function (items) {
|
||||
var button = _.findWhere(items, { type: 'button' });
|
||||
var index = items.indexOf(button);
|
||||
if (index < 0) {
|
||||
index = items.length;
|
||||
}
|
||||
items.splice(index, 0, {
|
||||
type: 'field',
|
||||
name: 'attrs',
|
||||
jsonFields: meta.jsonAttrs
|
||||
});
|
||||
return items;
|
||||
})(view.items);
|
||||
}
|
||||
if (view.type === 'form') {
|
||||
view.items.push({
|
||||
type: 'panel',
|
||||
title: _t('Attributes'),
|
||||
itemSpan: 12,
|
||||
items: [{
|
||||
type: 'field',
|
||||
name: 'attrs',
|
||||
jsonFields: meta.jsonAttrs
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
view = processJsonForm(view);
|
||||
meta.fields = processFields(meta.fields);
|
||||
|
||||
(function () {
|
||||
var helps = meta.helps = meta.helps || {};
|
||||
var items = [];
|
||||
|
||||
if (view.helpOverride && view.helpOverride.length) {
|
||||
helps = _.groupBy(view.helpOverride || [], 'type');
|
||||
helps = meta.helps = _.object(_.map(helps, function(items, key) {
|
||||
return [key, _.reduce(items, function(memo, item) {
|
||||
memo[item.field] = item;
|
||||
return memo;
|
||||
}, {})];
|
||||
}));
|
||||
|
||||
if (helps.tooltip && helps.tooltip.__top__) {
|
||||
view.help = helps.tooltip.__top__.help;
|
||||
}
|
||||
}
|
||||
|
||||
var help = helps.tooltip || {};
|
||||
var placeholder = helps.placeholder || {};
|
||||
var inline = helps.inline || {};
|
||||
|
||||
forEach(view.items, function (item) {
|
||||
if (help[item.name]) {
|
||||
item.help = help[item.name].help;
|
||||
}
|
||||
if (meta.view && meta.view.type === 'form') {
|
||||
if (placeholder[item.name]) {
|
||||
item.placeholder = placeholder[item.name].help;
|
||||
}
|
||||
if (inline[item.name] && !inline[item.name].used) {
|
||||
inline[item.name].used = true;
|
||||
items.push({
|
||||
type: 'help',
|
||||
text: inline[item.name].help,
|
||||
css: inline[item.name].style,
|
||||
colSpan: 12
|
||||
});
|
||||
}
|
||||
}
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
forEach(view.toolbar, function (item) {
|
||||
if (help[item.name]) {
|
||||
item.help = help[item.name].help;
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length) {
|
||||
view.items = items;
|
||||
}
|
||||
})();
|
||||
|
||||
forEach(view.items || view.pages, function(item) {
|
||||
processWidget(item);
|
||||
processSelection(item);
|
||||
forEach(fields[item.name], function(value, key){
|
||||
if (!item.hasOwnProperty(key)) {
|
||||
item[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
["canNew", "canView", "canEdit", "canRemove", "canSelect"].forEach(function (name) {
|
||||
if (item[name] === "false" || item[name] === "true") {
|
||||
item[name] = item[name] === "true";
|
||||
}
|
||||
});
|
||||
|
||||
if (item.items || item.pages) {
|
||||
ViewService.prototype.process(meta, item, view);
|
||||
}
|
||||
if (item.password) {
|
||||
item.widget = "password";
|
||||
}
|
||||
if (item.jsonFields && item.widget !== 'json-raw') {
|
||||
var editor = {
|
||||
layout: view.type === 'panel-json' ? 'table' : undefined,
|
||||
flexbox: true,
|
||||
items: [],
|
||||
};
|
||||
var panel = null;
|
||||
var panelTab = null;
|
||||
item.jsonFields.sort(function (x, y) { return x.sequence - y.sequence; });
|
||||
item.jsonFields.forEach(function (field) {
|
||||
if (field.widgetAttrs) {
|
||||
field.widgetAttrs = angular.fromJson(field.widgetAttrs);
|
||||
processWidget(field);
|
||||
if (field.widgetAttrs.showTitle !== undefined) {
|
||||
field.showTitle = field.widgetAttrs.showTitle;
|
||||
}
|
||||
if (field.widgetAttrs.multiline) {
|
||||
field.type = 'text';
|
||||
}
|
||||
if (field.widgetAttrs.targetName) {
|
||||
field.targetName = field.widgetAttrs.targetName;
|
||||
}
|
||||
}
|
||||
if (field.type === 'panel' || field.type === 'separator') {
|
||||
field.visibleInGrid = false;
|
||||
}
|
||||
if (field.type === 'panel') {
|
||||
panel = _.extend({}, field, { items: [] });
|
||||
if ((field.widgetAttrs || {}).sidebar && parent) {
|
||||
panel.sidebar = true;
|
||||
parent.width = 'large';
|
||||
}
|
||||
if ((field.widgetAttrs || {}).tab) {
|
||||
panelTab = panelTab || {
|
||||
type: 'panel-tabs',
|
||||
colSpan: 12,
|
||||
items: []
|
||||
};
|
||||
panelTab.items.push(panel);
|
||||
} else {
|
||||
editor.items.push(panel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (field.type !== 'separator') {
|
||||
field.title = field.title || field.autoTitle;
|
||||
}
|
||||
var colSpan = (field.widgetAttrs||{}).colSpan || field.colSpan;
|
||||
if (field.type == 'one-to-many') {
|
||||
field.type = 'many-to-many';
|
||||
field.canSelect = false;
|
||||
}
|
||||
if (field.type == 'separator' || (field.type == 'many-to-many' && !field.widget)) {
|
||||
field.showTitle = false;
|
||||
field.colSpan = colSpan || 12;
|
||||
}
|
||||
if (panel) {
|
||||
panel.items.push(field);
|
||||
} else {
|
||||
editor.items.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (panelTab) {
|
||||
editor.items.push(panelTab);
|
||||
}
|
||||
|
||||
item.widget = 'json-field';
|
||||
item.editor = editor;
|
||||
if (!item.viewer) {
|
||||
item.editor.viewer = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// include json fields in grid
|
||||
if (view.type === 'grid') {
|
||||
var items = [];
|
||||
_.each(view.items, function (item) {
|
||||
if (item.jsonFields) {
|
||||
_.each(item.jsonFields, function (field) {
|
||||
var type = field.type || 'text';
|
||||
if (type.indexOf('-to-many') === -1 && field.visibleInGrid) {
|
||||
items.push(_.extend({}, field, { name: item.name + '.' + field.name }));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
items = items.sort(function (x, y) { return x.columnSequence - y.columnSequence; });
|
||||
view.items = items;
|
||||
}
|
||||
};
|
||||
|
||||
function processJsonForm(view) {
|
||||
if (view.type !== 'form') return view;
|
||||
if (view.model !== 'com.axelor.meta.db.MetaJsonRecord') return view;
|
||||
|
||||
var panel = _.first(view.items) || {};
|
||||
var jsonField = _.first(panel.items) || {};
|
||||
var jsonFields = jsonField.jsonFields || [];
|
||||
|
||||
var first = _.first(jsonFields) || {};
|
||||
if (first.type === 'panel') {
|
||||
panel.type = 'panel-json';
|
||||
if (first.widgetAttrs) {
|
||||
var attrs = angular.fromJson(first.widgetAttrs);
|
||||
view.width = view.width || attrs.width;
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
function processFields(fields) {
|
||||
var result = {};
|
||||
if (isArray(fields)) {
|
||||
forEach(fields, function(field){
|
||||
field.type = _.chain(field.type || 'string').underscored().dasherize().value();
|
||||
field.title = field.title || field.autoTitle;
|
||||
result[field.name] = field;
|
||||
// if nested field then make it readonly
|
||||
if (field.name.indexOf('.') > -1) {
|
||||
field.readonly = true;
|
||||
field.required = false;
|
||||
}
|
||||
processSelection(field);
|
||||
});
|
||||
} else {
|
||||
result = fields || {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function processSelection(field) {
|
||||
_.each(field.selectionList, function (item) {
|
||||
if (_.isString(item.data)) {
|
||||
item.data = angular.fromJson(item.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processWidget(field) {
|
||||
var attrs = {};
|
||||
_.each(field.widgetAttrs || {}, function (value, name) {
|
||||
if (value === "true") value = true;
|
||||
if (value === "false") value = false;
|
||||
if (value === "null") value = null;
|
||||
if (/^(-)?\d+$/.test(value)) value = +(value);
|
||||
attrs[_.str.camelize(name)] = value;
|
||||
});
|
||||
if (field.serverType) {
|
||||
field.serverType = _.chain(field.serverType).underscored().dasherize().value();
|
||||
}
|
||||
field.widgetAttrs = attrs;
|
||||
}
|
||||
|
||||
function useIncluded(view) {
|
||||
|
||||
function useMenubar(menubar) {
|
||||
if (!menubar) return;
|
||||
var my = view.menubar || menubar;
|
||||
if (my !== menubar && menubar) {
|
||||
my = my.concat(menubar);
|
||||
}
|
||||
view.menubar = my;
|
||||
}
|
||||
function useToolbar(toolbar) {
|
||||
if (!toolbar) return;
|
||||
var my = view.toolbar || toolbar;
|
||||
if (my !== toolbar) {
|
||||
my = my.concat(toolbar);
|
||||
}
|
||||
view.toolbar = my;
|
||||
}
|
||||
function useItems(view) {
|
||||
return useIncluded(view);
|
||||
}
|
||||
|
||||
var items = [];
|
||||
|
||||
_.each(view.items, function(item) {
|
||||
if (item.type === "include") {
|
||||
if (item.view) {
|
||||
items = items.concat(useItems(item.view));
|
||||
useMenubar(item.view.menubar);
|
||||
useToolbar(item.view.toolbar);
|
||||
}
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function findFields(view, res) {
|
||||
var result = res || {
|
||||
fields: [],
|
||||
related: {}
|
||||
};
|
||||
var items = result.fields;
|
||||
var fields = view.items || view.pages;
|
||||
|
||||
if (!fields) return items;
|
||||
if (view.items && !view._included) {
|
||||
view._included = true;
|
||||
fields = view.items = useIncluded(view);
|
||||
}
|
||||
|
||||
function acceptEditor(item) {
|
||||
var collect = items;
|
||||
var editor = item.editor;
|
||||
if (item.target) {
|
||||
collect = result.related[item.name] || (result.related[item.name] = []);
|
||||
}
|
||||
if (editor.fields) {
|
||||
editor.fields = processFields(editor.fields);
|
||||
}
|
||||
var acceptItems = function (items) {
|
||||
_.each(items, function (child) {
|
||||
if (child.name && collect.indexOf(child.name) === -1 && child.type === 'field') {
|
||||
collect.push(child.name);
|
||||
} else if (child.type === 'panel') {
|
||||
acceptItems(child.items);
|
||||
}
|
||||
if (/RefSelect|ref-select/.test(child.widget)) {
|
||||
collect.push(child.related);
|
||||
}
|
||||
if (child.depends) {
|
||||
child.depends.split(/\s*,\s*/).forEach(function (name) {
|
||||
collect.push(name);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
acceptItems(editor.items);
|
||||
}
|
||||
|
||||
function acceptViewer(item) {
|
||||
var collect = items;
|
||||
var viewer = item.viewer;
|
||||
if (item.target) {
|
||||
collect = result.related[item.name] || (result.related[item.name] = []);
|
||||
}
|
||||
_.each(viewer.fields, function (item) {
|
||||
collect.push(item.name);
|
||||
});
|
||||
if (viewer.fields) {
|
||||
viewer.fields = processFields(viewer.fields);
|
||||
}
|
||||
}
|
||||
|
||||
_.each(fields, function(item) {
|
||||
if (item.editor) acceptEditor(item);
|
||||
if (item.viewer) acceptViewer(item);
|
||||
if (item.type === 'panel-related') {
|
||||
items.push(item.name);
|
||||
} else if (item.items || item.pages) {
|
||||
findFields(item, result);
|
||||
} else if (item.type === 'field') {
|
||||
items.push(item.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (view.type === "calendar") {
|
||||
items.push(view.eventStart);
|
||||
items.push(view.eventStop);
|
||||
items.push(view.colorBy);
|
||||
}
|
||||
if (view.type === "kanban") {
|
||||
items.push(view.columnBy);
|
||||
items.push(view.sequenceBy);
|
||||
}
|
||||
|
||||
if (view.type === "gantt") {
|
||||
items.push(view.taskUser);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var viewCache = $cacheFactory("viewCache", { capacity: 1000 });
|
||||
|
||||
function createStore(prefix) {
|
||||
var toKey = function (name) {
|
||||
return prefix + ':' + axelor.config['user.id'] + ':' + name;
|
||||
};
|
||||
return {
|
||||
get: function (name) {
|
||||
return new $q(function (resolve) {
|
||||
resolve(angular.copy(viewCache.get(toKey(name))));
|
||||
});
|
||||
},
|
||||
set: function (name, value) {
|
||||
if (value) {
|
||||
return new $q(function (resolve) {
|
||||
var val = viewCache.put(toKey(name), angular.copy(value));
|
||||
resolve(val);
|
||||
});
|
||||
}
|
||||
return $q.resolve(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var PENDING_REQUESTS = {};
|
||||
|
||||
var FIELDS = createStore('f');
|
||||
var VIEWS = createStore('v');
|
||||
var PERMS = createStore('p');
|
||||
|
||||
ViewService.prototype.getMetaDef = function(model, view, context) {
|
||||
var self = this;
|
||||
var deferred = $q.defer();
|
||||
var promise = deferred.promise;
|
||||
|
||||
promise.success = function(fn) {
|
||||
promise.then(function(res) {
|
||||
fn(res.fields, res.view);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
function process(data) {
|
||||
data.view.perms = data.view.perms || data.perms;
|
||||
self.process(data, data.view);
|
||||
|
||||
if (data.perms && data.perms.write === false) {
|
||||
data.view.editable = false;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function updateFields(fetched) {
|
||||
return FIELDS.get(model).then(function (current) {
|
||||
current = current || {};
|
||||
if (current !== fetched.fields) {
|
||||
_.extend(current, _.object(_.pluck(fetched.fields, 'name'), fetched.fields));
|
||||
}
|
||||
return $q.all([FIELDS.set(model, current), PERMS.set(model, fetched.perms)]);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchFields(data) {
|
||||
var fields_data = findFields(data.view);
|
||||
var fields = _.unique(_.compact(fields_data.fields.sort()));
|
||||
|
||||
data.related = fields_data.related;
|
||||
|
||||
if (_.isArray(data.fields) && data.fields.length > 0) {
|
||||
updateFields(data).then(function () {
|
||||
deferred.resolve(process(data));
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
if (!model || _.isEmpty(fields)) {
|
||||
deferred.resolve(data);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function resolve(fetched) {
|
||||
return updateFields(fetched).then(function (res) {
|
||||
var current = res[0];
|
||||
var perms = res[1];
|
||||
var result = _.extend({}, fetched, {
|
||||
fields: _.map(fields, function(n) { return current[n]; }),
|
||||
perms: perms
|
||||
});
|
||||
|
||||
result.view = data.view || view;
|
||||
result.fields = _.compact(result.fields);
|
||||
result.related = data.related;
|
||||
result = process(result);
|
||||
|
||||
deferred.resolve(result);
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
$q.all([FIELDS.get(model), PERMS.get(model)]).then(function (res) {
|
||||
var fetchedFields = res[0] || {};
|
||||
var pendingFields = _.filter(fields, function (n) { return !fetchedFields.hasOwnProperty(n); });
|
||||
if (pendingFields.length == 0) {
|
||||
resolve({
|
||||
fields: _.values(fetchedFields),
|
||||
perms: res[1]
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
var key = _.flatten([model, pendingFields]).join();
|
||||
var pending = PENDING_REQUESTS[key];
|
||||
|
||||
function clear() {
|
||||
delete PENDING_REQUESTS[key];
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
pending.then(clear, clear);
|
||||
pending.then(function (response) {
|
||||
resolve((response.data || {}).data);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
pending = $http.post('ws/meta/view/fields', { model: model, fields: pendingFields }).then(function (response) {
|
||||
resolve((response.data || {}).data);
|
||||
});
|
||||
|
||||
pending.then(clear, clear);
|
||||
PENDING_REQUESTS[key] = pending;
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
function fetchView() {
|
||||
var key = [model, view.type, view.name].join(':');
|
||||
|
||||
function resolve(response) {
|
||||
var result = (response.data || {}).data;
|
||||
if (!result || !result.view) {
|
||||
return deferred.reject('view not found', view);
|
||||
}
|
||||
if (result.searchForm) {
|
||||
result.view.searchForm = result.searchForm;
|
||||
}
|
||||
|
||||
if (_.isArray(result.view.items)) {
|
||||
var fieldsPromise = fetchFields(result, key);
|
||||
if (!_.isArray(view.items)) {
|
||||
// only cache fetched views
|
||||
return fieldsPromise.then(function (res) {
|
||||
return VIEWS.set(key, _.extend({}, res, { view: result.view }));
|
||||
});
|
||||
}
|
||||
return fieldsPromise;
|
||||
}
|
||||
|
||||
result = {
|
||||
fields: result.view.items,
|
||||
view: result.view
|
||||
};
|
||||
|
||||
VIEWS.set(key, result).then(function () {
|
||||
deferred.resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
VIEWS.get(key).then(function (loaded) {
|
||||
if (loaded) {
|
||||
return deferred.resolve(loaded);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
delete PENDING_REQUESTS[key];
|
||||
}
|
||||
|
||||
var pending = PENDING_REQUESTS[key];
|
||||
if (pending) {
|
||||
pending.then(clear, clear);
|
||||
return pending.then(resolve);
|
||||
}
|
||||
|
||||
pending = $http.post('ws/meta/view', {
|
||||
model: model,
|
||||
data: {
|
||||
type: view.type,
|
||||
name: view.name,
|
||||
context: context
|
||||
}
|
||||
});
|
||||
|
||||
pending.then(resolve);
|
||||
|
||||
pending.then(clear, clear);
|
||||
PENDING_REQUESTS[key] = pending;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (_.isArray(view.items)) {
|
||||
return fetchFields({ view: view, fields: view.fields });
|
||||
}
|
||||
return fetchView();
|
||||
};
|
||||
|
||||
ViewService.prototype.defer = function() {
|
||||
return $q.defer();
|
||||
};
|
||||
|
||||
ViewService.prototype.action = function(action, model, context, data) {
|
||||
|
||||
var params = {
|
||||
model: model,
|
||||
action: action,
|
||||
data: data || {
|
||||
context: _.extend({ _model: model }, context)
|
||||
}
|
||||
};
|
||||
|
||||
var promise = $http.post('ws/action', params);
|
||||
promise.success = function(fn) {
|
||||
promise.then(function(response){
|
||||
fn(response.data);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
promise.error = function(fn) {
|
||||
promise.then(null, fn);
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
ViewService.prototype.getFields = function(model, jsonModel) {
|
||||
|
||||
var that = this,
|
||||
promise = $http.get('ws/meta/fields/' + model, {
|
||||
cache: true,
|
||||
params: jsonModel ? {
|
||||
jsonModel: jsonModel
|
||||
} : undefined
|
||||
});
|
||||
|
||||
promise.success = function(fn) {
|
||||
promise.then(function(response) {
|
||||
var res = response.data,
|
||||
data = res.data;
|
||||
that.process(data);
|
||||
fn(data.fields, data.jsonFields);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
promise.error = function(fn) {
|
||||
promise.then(null, fn);
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
ViewService.prototype.save = function(schema) {
|
||||
var promise = $http.post("ws/meta/view/save", {
|
||||
data: schema
|
||||
});
|
||||
|
||||
promise.success = function(fn) {
|
||||
promise.then(function(response) {
|
||||
var res = response.data,
|
||||
data = res.data;
|
||||
fn(data);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
promise.error = function(fn) {
|
||||
promise.then(null, fn);
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
return new ViewService();
|
||||
}]);
|
||||
|
||||
})();
|
||||
609
sophal/js/axelor.nav.js
Normal file
609
sophal/js/axelor.nav.js
Normal file
@ -0,0 +1,609 @@
|
||||
/*
|
||||
* 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 app = angular.module("axelor.app");
|
||||
|
||||
function useSingleTabOnly() {
|
||||
return axelor.device.mobile
|
||||
|| !!axelor.config['view.single.tab']
|
||||
|| axelor.config['user.singleTab']
|
||||
|| +(axelor.config["view.tabs.max"]) === 0
|
||||
|| +(axelor.config["view.tabs.max"]) === 1;
|
||||
}
|
||||
|
||||
app.factory('NavService', ['$location', 'MenuService', function($location, MenuService) {
|
||||
|
||||
var tabs = [];
|
||||
var popups = [];
|
||||
var selected = null;
|
||||
|
||||
var VIEW_TYPES = {
|
||||
'list' : 'grid',
|
||||
'edit' : 'form'
|
||||
};
|
||||
|
||||
function findTab(key) {
|
||||
return _.find(tabs, function(tab){
|
||||
return tab.action == key;
|
||||
});
|
||||
}
|
||||
|
||||
function findTabTitle(tab) {
|
||||
var first;
|
||||
if (tab.title) {
|
||||
return tab.title;
|
||||
}
|
||||
first = _.first(tab.views);
|
||||
if (first) {
|
||||
return first.title || first.name;
|
||||
}
|
||||
return tab.name || "Unknown";
|
||||
}
|
||||
|
||||
function openView(view, options) {
|
||||
if (view && (view.type || view.viewType) == 'html') {
|
||||
var first = _.first(view.views) || view;
|
||||
view.views = [{
|
||||
name: first.name,
|
||||
resource: first.resource,
|
||||
title: first.title,
|
||||
type: 'html'
|
||||
}];
|
||||
if ((view.params||{}).target === "_blank") {
|
||||
var url = first.name || first.resource;
|
||||
return setTimeout(function () {
|
||||
window.open(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var closable = options && options.__tab_closable;
|
||||
if (!closable && view.params && view.params.closable !== undefined) {
|
||||
closable = view.params.closable;
|
||||
}
|
||||
|
||||
view.closable = closable;
|
||||
|
||||
openTab(view, options);
|
||||
}
|
||||
|
||||
function openTabByName(name, options) {
|
||||
|
||||
var tab = findTab(name);
|
||||
if (tab) {
|
||||
return openTab(tab, options);
|
||||
}
|
||||
|
||||
return MenuService.action(name).success(function(result){
|
||||
if (!result.data) {
|
||||
return;
|
||||
}
|
||||
var view = result.data[0].view;
|
||||
view.action = name;
|
||||
return openView(view, options);
|
||||
});
|
||||
}
|
||||
|
||||
function openTabAsPopup(tab, options) {
|
||||
popups.push(tab);
|
||||
}
|
||||
|
||||
function openTab(tab, options) {
|
||||
|
||||
if (tab && tab.$popupParent) {
|
||||
return openTabAsPopup(tab, options);
|
||||
}
|
||||
|
||||
options = options || tab.options;
|
||||
|
||||
if (options && options.mode) {
|
||||
tab.viewType = VIEW_TYPES[options.mode] || options.mode;
|
||||
}
|
||||
|
||||
tab.options = options;
|
||||
tab.title = tab.title || findTabTitle(tab);
|
||||
|
||||
if (tab.action && MenuService.updateTabStyle) {
|
||||
MenuService.updateTabStyle(tab);
|
||||
}
|
||||
|
||||
function __doSelect(found) {
|
||||
|
||||
var lastScope = (selected||{}).$viewScope || {};
|
||||
if (lastScope.$locationChangeOff) {
|
||||
lastScope.$locationChangeOff();
|
||||
}
|
||||
|
||||
found.selected = true;
|
||||
selected = found;
|
||||
|
||||
if (options && tab.$viewScope) {
|
||||
var view = tab.$viewScope._views[tab.viewType],
|
||||
promise = view ? view.deferred.promise : null;
|
||||
if (promise) {
|
||||
promise.then(function(viewScope) {
|
||||
viewScope.setRouteOptions(options);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
}
|
||||
|
||||
function __singleShow(found) {
|
||||
tabs.length = 0;
|
||||
tabs.push(found);
|
||||
return __doSelect(found);
|
||||
}
|
||||
|
||||
var found = findTab(tab.action);
|
||||
|
||||
if (useSingleTabOnly()) {
|
||||
|
||||
if (found) {
|
||||
return __singleShow(found);
|
||||
}
|
||||
|
||||
var last = _.last(tabs);
|
||||
if (last) {
|
||||
return closeTab(last, function () {
|
||||
__singleShow(tab);
|
||||
});
|
||||
}
|
||||
return __singleShow(tab);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
found = tab;
|
||||
__closeUnusedTabs();
|
||||
if (options && options.__tab_prepend) {
|
||||
tabs.unshift(tab);
|
||||
} else {
|
||||
tabs.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
_.each(tabs, function(tab) {
|
||||
tab.selected = false;
|
||||
});
|
||||
|
||||
return __doSelect(found);
|
||||
}
|
||||
|
||||
var MAX_TABS;
|
||||
|
||||
function __closeUnusedTabs() {
|
||||
if (MAX_TABS === undefined) {
|
||||
MAX_TABS = +(axelor.config["view.tabs.max"]) || -1;
|
||||
}
|
||||
if (MAX_TABS <= 0 || tabs.length < MAX_TABS) {
|
||||
return;
|
||||
}
|
||||
|
||||
var all = _.filter(tabs, function (tab) {
|
||||
return !tab.selected && canCloseTab(tab);
|
||||
});
|
||||
var doClose = function doClose(tab) {
|
||||
var index = _.indexOf(tabs, tab);
|
||||
var vs = tab.$viewScope;
|
||||
if (vs && vs.isDirty && vs.isDirty()) return;
|
||||
if (vs && vs.$details && vs.$details.isDirty && vs.$details.isDirty()) return;
|
||||
tabs.splice(index, 1);
|
||||
};
|
||||
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
doClose(all[i]);
|
||||
if (tabs.length === 0) selected = null;
|
||||
if (tabs.length < MAX_TABS) break;
|
||||
}
|
||||
}
|
||||
|
||||
function __closeTab(tab, callback) {
|
||||
|
||||
var all = tab.$popupParent ? popups : tabs;
|
||||
var index = _.indexOf(all, tab);
|
||||
|
||||
// remove tab
|
||||
all.splice(index, 1);
|
||||
|
||||
if (tabs.length === 0) {
|
||||
selected = null;
|
||||
}
|
||||
if (_.isFunction(callback)) {
|
||||
callback();
|
||||
}
|
||||
if (tab.$popupParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab.selected) {
|
||||
if (index == tabs.length)
|
||||
index -= 1;
|
||||
_.each(all, function(tab){
|
||||
tab.selected = false;
|
||||
});
|
||||
var select = all[index];
|
||||
if (select) {
|
||||
select.selected = true;
|
||||
openTab(select);
|
||||
} else {
|
||||
$location.path('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canCloseTab(tab) {
|
||||
return tab.closable === undefined ? true : tab.closable;
|
||||
}
|
||||
|
||||
function closeTab(tab, callback) {
|
||||
var viewScope = tab.$viewScope;
|
||||
if (viewScope && viewScope.confirmDirty) {
|
||||
viewScope.confirmDirty(function(){
|
||||
__closeTab(tab, callback);
|
||||
});
|
||||
} else {
|
||||
__closeTab(tab, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function closeTabs(selection) {
|
||||
var all = _.flatten([selection], true);
|
||||
|
||||
function select(tab) {
|
||||
if (!tab.selected) {
|
||||
tab.selected = true;
|
||||
openTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
function close(tab, ignore) {
|
||||
var at = tabs.indexOf(tab);
|
||||
if (at > -1) {
|
||||
tabs.splice(at, 1);
|
||||
}
|
||||
closeTabs(_.difference(selection, [ignore, tab]));
|
||||
if (tabs.length === 0) {
|
||||
selected = null;
|
||||
}
|
||||
}
|
||||
|
||||
function doConfirm(tab, viewScope) {
|
||||
return viewScope.confirmDirty(function(){
|
||||
return close(tab);
|
||||
}, function() {
|
||||
close(null, tab);
|
||||
viewScope.$applyAsync();
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var tab = all[i];
|
||||
var viewScope = tab.$viewScope;
|
||||
if (viewScope && viewScope.confirmDirty) {
|
||||
select(tab);
|
||||
return doConfirm(tab, viewScope);
|
||||
}
|
||||
return close(tab);
|
||||
}
|
||||
|
||||
if (tabs.indexOf(selected) == -1) {
|
||||
selected = null;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
return openTab(selected);
|
||||
}
|
||||
|
||||
var first = _.first(tabs);
|
||||
if (first && !first.selected) {
|
||||
return openTab(first);
|
||||
}
|
||||
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
|
||||
function closeTabOthers(current) {
|
||||
var rest = _.filter(tabs, function(tab) {
|
||||
return canCloseTab(tab) && tab !== current;
|
||||
});
|
||||
if (current && !current.selected) {
|
||||
current.selected = true;
|
||||
openTab(current);
|
||||
}
|
||||
return closeTabs(rest);
|
||||
}
|
||||
|
||||
function closeTabAll() {
|
||||
closeTabOthers();
|
||||
}
|
||||
|
||||
function reloadTab(current) {
|
||||
var viewScope = current.$viewScope;
|
||||
if (viewScope) {
|
||||
viewScope.$broadcast('on:tab-reload', current);
|
||||
}
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
function getPopups() {
|
||||
return popups;
|
||||
}
|
||||
|
||||
function getSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
return {
|
||||
openTabByName: openTabByName,
|
||||
openTab: openTab,
|
||||
openView: openView,
|
||||
canCloseTab: canCloseTab,
|
||||
reloadTab: reloadTab,
|
||||
closeTab: closeTab,
|
||||
closeTabOthers: closeTabOthers,
|
||||
closeTabAll: closeTabAll,
|
||||
getTabs: getTabs,
|
||||
getPopups: getPopups,
|
||||
getSelected: getSelected
|
||||
};
|
||||
}]);
|
||||
|
||||
NavCtrl.$inject = ['$scope', '$rootScope', '$location', 'NavService'];
|
||||
function NavCtrl($scope, $rootScope, $location, NavService) {
|
||||
|
||||
$scope.singleTabOnly = useSingleTabOnly();
|
||||
|
||||
Object.defineProperty($scope, '$location', {
|
||||
get: function() {
|
||||
return $location;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty($scope, 'navTabs', {
|
||||
get: function() {
|
||||
return NavService.getTabs();
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty($scope, 'navPopups', {
|
||||
get: function() {
|
||||
return NavService.getPopups();
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty($scope, 'selectedTab', {
|
||||
get: function() {
|
||||
return NavService.getSelected();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.hasNabPopups = function () {
|
||||
return $scope.navPopups && $scope.navPopups.length > 0;
|
||||
};
|
||||
|
||||
$scope.menuClick = function(event, record) {
|
||||
|
||||
if (!record.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (axelor.device.small) {
|
||||
$("#offcanvas").removeClass("active");
|
||||
}
|
||||
|
||||
$scope.openTabByName(record.action);
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
|
||||
$scope.navClick = function(tab) {
|
||||
$scope.openTab(tab);
|
||||
$scope.$broadcast("on:nav-click", tab);
|
||||
};
|
||||
|
||||
$scope.$on("on:update-route", update);
|
||||
|
||||
function update(event) {
|
||||
|
||||
var tab = $scope.selectedTab,
|
||||
scope = event.targetScope;
|
||||
|
||||
if (!tab || !tab.action || scope !== tab.$viewScope || !scope.getRouteOptions) {
|
||||
return;
|
||||
}
|
||||
if (tab.action.indexOf('$act') > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path = tab.action,
|
||||
opts = scope.getRouteOptions(),
|
||||
mode = opts.mode,
|
||||
args = opts.args;
|
||||
|
||||
path = "/ds/" + path + "/" + mode;
|
||||
args = _.filter(args, function(arg) {
|
||||
return _.isNumber(args) || arg;
|
||||
});
|
||||
|
||||
if (args.length) {
|
||||
path += "/" + args.join("/");
|
||||
}
|
||||
|
||||
if ($location.$$path !== path) {
|
||||
$location.path(path);
|
||||
$location.search(opts.query || "");
|
||||
}
|
||||
}
|
||||
|
||||
$scope.canCloseTab = function(tab) {
|
||||
return NavService.canCloseTab(tab);
|
||||
};
|
||||
|
||||
$scope.openTab = function(tab, options) {
|
||||
return NavService.openTab(tab, options);
|
||||
};
|
||||
|
||||
$scope.openTabByName = function(name, options) {
|
||||
return NavService.openTabByName(name, options);
|
||||
};
|
||||
|
||||
$scope.closeTab = function(tab, callback) {
|
||||
var wasSelected = tab.selected;
|
||||
if (NavService.canCloseTab(tab)) {
|
||||
NavService.closeTab(tab, callback);
|
||||
if ($scope.selectedTab && wasSelected) {
|
||||
$scope.$broadcast("on:nav-click", $scope.selectedTab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.closeTabOthers = function(tab) {
|
||||
var wasSelected = tab.selected;
|
||||
NavService.closeTabOthers(tab);
|
||||
if ($scope.selectedTab === tab && !wasSelected) {
|
||||
$scope.$broadcast("on:nav-click", tab);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.closeTabAll = function() {
|
||||
return NavService.closeTabAll();
|
||||
};
|
||||
|
||||
$scope.reloadTab = function(tab) {
|
||||
return NavService.reloadTab(tab);
|
||||
};
|
||||
|
||||
$scope.tabTitle = function(tab) {
|
||||
var vs = tab.$viewScope || {};
|
||||
if (vs.viewType === "form") {
|
||||
return vs.viewTitle || tab.title;
|
||||
}
|
||||
return tab.title;
|
||||
};
|
||||
|
||||
$scope.tabDirty = function(tab) {
|
||||
var viewScope = tab.$viewScope;
|
||||
if (viewScope && viewScope.isDirty) {
|
||||
return viewScope.isDirty();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// expose common methods to $rootScope
|
||||
$scope.$root.openTab = $scope.openTab;
|
||||
$scope.$root.openTabByName = $scope.openTabByName;
|
||||
|
||||
$scope.$watch('selectedTab.viewType', function tabViewTypeWatch(viewType){
|
||||
if (viewType) {
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('routePath', function routePathWatch(path) {
|
||||
$scope.openHomeTab();
|
||||
});
|
||||
|
||||
var confirm = _t('Current changes will be lost.');
|
||||
|
||||
function onbeforeunload(e) {
|
||||
var tabs = $scope.navTabs || [];
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
var vs = (tabs[i]||{}).$viewScope;
|
||||
if (vs && vs.$$dirty) {
|
||||
return confirm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
|
||||
// menu toggle logic
|
||||
var menuToggled = false;
|
||||
var navigator = axelor.config["user.navigator"];
|
||||
|
||||
if (navigator !== 'hidden') {
|
||||
$('#offcanvas-toggle').find('a').click(function (e) {
|
||||
var active = ! $("#offcanvas").hasClass('inactive');
|
||||
if (active && axelor.device.small) {
|
||||
active = $("#offcanvas").hasClass('active');
|
||||
}
|
||||
$("#offcanvas").toggleClass("active", !active && axelor.device.small);
|
||||
$("#offcanvas").toggleClass("inactive", active && !axelor.device.small);
|
||||
if (!axelor.device.mobile) {
|
||||
setTimeout(axelor.$adjustSize, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#offcanvas,#offcanvas-toggle").toggleClass("hidden-menu", navigator === "hidden");
|
||||
if (navigator === "collapse") {
|
||||
$("#offcanvas").addClass("inactive");
|
||||
}
|
||||
$scope.ajaxStop(function () {
|
||||
setTimeout(function () {
|
||||
$("#offcanvas,#offcanvas-toggle").removeClass("hidden");
|
||||
}, 100);
|
||||
}, 100);
|
||||
|
||||
$(window).on('resize', _.debounce(function () {
|
||||
$("#offcanvas").removeClass(axelor.device.small ? 'inactive' : 'active');
|
||||
setTimeout(axelor.$adjustSize, 100);
|
||||
}, 100));
|
||||
|
||||
// confirm dirty
|
||||
$(window).on('beforeunload', onbeforeunload);
|
||||
});
|
||||
}
|
||||
|
||||
TabCtrl.$inject = ['$scope', '$location', '$routeParams'];
|
||||
function TabCtrl($scope, $location, $routeParams) {
|
||||
|
||||
var homeAction = axelor.config["user.action"],
|
||||
params = _.clone($routeParams),
|
||||
search = _.clone($location.$$search);
|
||||
|
||||
var opts = {
|
||||
mode: params.mode,
|
||||
state: params.state,
|
||||
search: search
|
||||
};
|
||||
|
||||
if (homeAction === params.resource) {
|
||||
_.extend(opts, {
|
||||
__tab_prepend: true,
|
||||
__tab_closable: false
|
||||
});
|
||||
}
|
||||
|
||||
if (params.resource) {
|
||||
$scope.openTabByName(params.resource, opts);
|
||||
}
|
||||
}
|
||||
|
||||
app.controller("NavCtrl", NavCtrl);
|
||||
app.controller("TabCtrl", TabCtrl);
|
||||
|
||||
})();
|
||||
201
sophal/js/axelor.ng.js
Normal file
201
sophal/js/axelor.ng.js
Normal file
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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 module = angular.module('axelor.ng', []);
|
||||
|
||||
module.config(['$provide', function($provide) {
|
||||
|
||||
$provide.decorator('$rootScope', ['$delegate', '$exceptionHandler', '$injector', function ($rootScope, $exceptionHandler, $injector) {
|
||||
|
||||
var __orig__ = Object.getPrototypeOf($rootScope),
|
||||
__super__ = {},
|
||||
__custom__ = {};
|
||||
|
||||
for (var name in __orig__) {
|
||||
if (angular.isFunction(__orig__[name])) {
|
||||
__super__[name] = __orig__[name];
|
||||
}
|
||||
}
|
||||
|
||||
var $q = null,
|
||||
$http = null,
|
||||
$timeout = null;
|
||||
|
||||
__custom__.ajaxStop = function ajaxStop(callback, context) {
|
||||
var count, wait;
|
||||
|
||||
if ($http === null) {
|
||||
$http = $injector.get('$http');
|
||||
}
|
||||
|
||||
count = _.size($http.pendingRequests || []);
|
||||
wait = _.last(arguments) || 10;
|
||||
|
||||
if (_.isNumber(context)) {
|
||||
context = undefined;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
return _.delay(ajaxStop.bind(this), wait, callback, context);
|
||||
}
|
||||
if (_.isFunction(callback)) {
|
||||
return this.$timeout(callback.bind(context), wait);
|
||||
}
|
||||
};
|
||||
|
||||
// expose _t() to use in template
|
||||
__custom__._t = _t;
|
||||
|
||||
__custom__.$actionPromises = [];
|
||||
__custom__.waitForActions = function waitForActions(callback, wait) {
|
||||
if ($q === null) {
|
||||
$q = $injector.get('$q');
|
||||
}
|
||||
var that = this;
|
||||
var args = arguments;
|
||||
var waitFor = wait || 10;
|
||||
this.$timeout(function () {
|
||||
// wait for any pending ajax requests
|
||||
that.ajaxStop(function () {
|
||||
var all = args.length === 3 ? args[2] : that.$actionPromises;
|
||||
// wait for actions
|
||||
$q.all(all).then(function () {
|
||||
// if new actions are executed, wait for them
|
||||
if (args.length !== 3 && that.$actionPromises.length) {
|
||||
return _.delay(waitForActions.bind(that), 10, callback);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, callback);
|
||||
});
|
||||
}, waitFor);
|
||||
};
|
||||
|
||||
__custom__.$callWhen = function (predicate, callback, wait) {
|
||||
var count = wait || 100;
|
||||
|
||||
function later() {
|
||||
if (count-- === 0 || (_.isFunction(predicate) && predicate())) {
|
||||
return callback();
|
||||
}
|
||||
return _.delay(later, count);
|
||||
}
|
||||
|
||||
this.$timeout(later, wait);
|
||||
};
|
||||
|
||||
__custom__.$timeout = function(func, wait, invokeApply) {
|
||||
if ($timeout === null) {
|
||||
$timeout = $injector.get('$timeout');
|
||||
}
|
||||
if (arguments.length === 0) {
|
||||
return $timeout();
|
||||
}
|
||||
return $timeout.apply(null, arguments);
|
||||
};
|
||||
|
||||
__custom__.$onAdjust = function (events, handler, wait) {
|
||||
var names = events;
|
||||
if (_.isFunction(names)) {
|
||||
wait = handler;
|
||||
handler = names;
|
||||
names = 'adjust:size';
|
||||
} else {
|
||||
names = names.replace(/(\w+)/g, 'adjust:$1');
|
||||
}
|
||||
|
||||
var func = wait ? _.debounce(handler, wait) : handler;
|
||||
|
||||
$(document).on(names, func);
|
||||
this.$on('$destroy', function () {
|
||||
$(document).off(names, func);
|
||||
});
|
||||
};
|
||||
|
||||
__custom__.$new = function $new() {
|
||||
var inst = __super__.$new.apply(this, arguments);
|
||||
|
||||
inst.$$watchChecker = this.$$watchChecker;
|
||||
inst.$$watchInitialized = false;
|
||||
inst.$$childCanWatch = true;
|
||||
inst.$$shouldWatch = false;
|
||||
inst.$$popupStack = this.$$popupStack || (this.$$popupStack = []);
|
||||
return inst;
|
||||
};
|
||||
|
||||
// make sure to patch $rootScope.$digest with
|
||||
// if ((!current.$$canWatch || current.$$canWatch(current)) && (watchers = current.$$watchers)) {
|
||||
// ...
|
||||
// }
|
||||
|
||||
__custom__.$$canWatch = function () {
|
||||
if (!this.$$watchInitialized || !this.$$watchChecker) {
|
||||
this.$$watchInitialized = true;
|
||||
return true;
|
||||
}
|
||||
if (this.$$shouldWatch === true) {
|
||||
return true;
|
||||
}
|
||||
var parent = this.$parent || {};
|
||||
if (parent.$$childCanWatch !== undefined && !parent.$$childCanWatch) {
|
||||
return false;
|
||||
}
|
||||
this.$$childCanWatch = this.$$watchChecker(this);
|
||||
return this.$$childCanWatch;
|
||||
};
|
||||
|
||||
__custom__.$watchChecker = function (checker) {
|
||||
|
||||
var self = this,
|
||||
previous = this.$$watchChecker;
|
||||
|
||||
if (this.$$watchChecker === null) {
|
||||
this.$$watchChecker = checker;
|
||||
} else {
|
||||
this.$$watchChecker = function() {
|
||||
return previous(self) && checker(self);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var __super__$$apply = _.debounce(__super__.$apply, 100);
|
||||
|
||||
__custom__.$apply = function $apply() {
|
||||
return arguments.length === 0
|
||||
? __super__$$apply.apply(this)
|
||||
: __super__.$apply.apply(this, arguments);
|
||||
};
|
||||
|
||||
__custom__.$applyNow = function $applyNow() {
|
||||
return __super__.$apply.apply(this, arguments);
|
||||
};
|
||||
|
||||
angular.extend(__orig__, __custom__);
|
||||
angular.extend($rootScope, __custom__);
|
||||
|
||||
$rootScope.$$watchChecker = null;
|
||||
|
||||
return $rootScope;
|
||||
}]);
|
||||
}]);
|
||||
|
||||
})();
|
||||
126
sophal/js/axelor.ns.js
Normal file
126
sophal/js/axelor.ns.js
Normal file
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
// create global axelor namespace if not exists
|
||||
window.axelor = window.axelor || {};
|
||||
window.axelor.config = {};
|
||||
|
||||
// browser detection (adopted from jquery)
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var browser = {};
|
||||
var match =
|
||||
/(edge)[\/]([\w.]+)/.exec(ua) ||
|
||||
/(opr)[\/]([\w.]+)/.exec(ua) ||
|
||||
/(chrome)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(msie) ([\w.]+)/.exec(ua) ||
|
||||
ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) ||
|
||||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
||||
[];
|
||||
|
||||
var matched = {
|
||||
browser: match[5] || match[3] || match[1] || "",
|
||||
version: match[4] || match[2] || "0"
|
||||
};
|
||||
|
||||
if (matched.browser) {
|
||||
browser[matched.browser] = true;
|
||||
browser.version = matched.version;
|
||||
}
|
||||
if (browser.chrome || browser.opr || browser.safari) {
|
||||
browser.webkit = true;
|
||||
}
|
||||
|
||||
// IE11
|
||||
if (browser.rv) {
|
||||
var ie = "msie";
|
||||
matched.browser = ie;
|
||||
browser[ie] = true;
|
||||
}
|
||||
|
||||
// recent opera
|
||||
if (browser.opr ) {
|
||||
var opera = "opera";
|
||||
matched.browser = opera;
|
||||
browser[opera] = true;
|
||||
}
|
||||
|
||||
// screen size detection
|
||||
var device = {
|
||||
small: false,
|
||||
large: false
|
||||
};
|
||||
|
||||
device.large = $(window).width() > 768;
|
||||
device.small = !device.large;
|
||||
device.mobile = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(ua);
|
||||
|
||||
axelor.browser = browser;
|
||||
axelor.device = device;
|
||||
|
||||
function sanitizeElement(element) {
|
||||
$.each(element.attributes, function() {
|
||||
var attr = this.name;
|
||||
var value = this.value;
|
||||
if (attr.indexOf('xss-on') === 0 || value.indexOf('javascript:') === 0) {
|
||||
$(element).removeAttr(attr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this function removes <script> and event attributes (onerror, onload etc.) from the given html text
|
||||
function sanitizeText(html) {
|
||||
if (typeof html !== 'string') {
|
||||
return html;
|
||||
}
|
||||
var value = "<div>" + html.replace(/(\s)(on(?:\w+))(\s*=)/, '$1xss-$2$3') + "</div>";
|
||||
var elems = $($.parseHTML(value, null, false));
|
||||
|
||||
elems.find('*').each(function() {
|
||||
sanitizeElement(this);
|
||||
});
|
||||
|
||||
return elems.html();
|
||||
}
|
||||
|
||||
function sanitize() {
|
||||
if (arguments.length === 0) {
|
||||
return;
|
||||
}
|
||||
var args = arguments.length === 1 ? arguments[0] : Array.prototype.slice.call(arguments);
|
||||
return Array.isArray(args) ? args.map(function (item) {
|
||||
return Array.isArray(item) ? sanitize(item) : sanitizeText(item);
|
||||
}) : sanitizeText(args);
|
||||
}
|
||||
|
||||
axelor.sanitize = sanitize;
|
||||
|
||||
// sanitize jquery html function
|
||||
var jq = {
|
||||
html: $.fn.html
|
||||
};
|
||||
|
||||
$.fn.html = function html() { return jq.html.apply(this, sanitize(arguments)); };
|
||||
|
||||
})();
|
||||
120
sophal/js/axelor.prefs.js
Normal file
120
sophal/js/axelor.prefs.js
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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");
|
||||
|
||||
function UserCtrl($scope, $element, $location, DataSource, ViewService) {
|
||||
|
||||
$scope._viewParams = {
|
||||
model: 'com.axelor.auth.db.User',
|
||||
views: [{name: 'user-preferences-form', type: 'form'}],
|
||||
recordId: axelor.config['user.id']
|
||||
};
|
||||
|
||||
ui.ViewCtrl($scope, DataSource, ViewService);
|
||||
ui.FormViewCtrl($scope, $element);
|
||||
|
||||
$scope.onClose = function() {
|
||||
$scope.confirmDirty(doClose);
|
||||
};
|
||||
|
||||
var __version = null;
|
||||
|
||||
$scope.$watch('record.version', function recordVersionWatch(value) {
|
||||
if (value === null || value === undefined) return;
|
||||
if (__version !== null) return;
|
||||
__version = value;
|
||||
});
|
||||
|
||||
function doClose() {
|
||||
if (!$scope.isDirty()) {
|
||||
var rec = $scope.record || {};
|
||||
axelor.config["user.action"] = rec.homeAction;
|
||||
}
|
||||
|
||||
window.history.back();
|
||||
|
||||
if (__version === ($scope.record || {}).version) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$scope.isMidForm = function (elem) {
|
||||
return $element.find('form.mid-form').size();
|
||||
};
|
||||
|
||||
$scope.setEditable();
|
||||
$scope.show();
|
||||
|
||||
$scope.ajaxStop(function () {
|
||||
$scope.$applyAsync();
|
||||
});
|
||||
}
|
||||
|
||||
function AboutCtrl($scope) {
|
||||
$scope.appName = axelor.config["application.name"];
|
||||
$scope.appDescription = axelor.config["application.description"];
|
||||
$scope.appVersion = axelor.config["application.version"];
|
||||
$scope.appVersionShort = $scope.appVersion.substring(0, $scope.appVersion.lastIndexOf('.'));
|
||||
$scope.appCopyright = axelor.config["application.copyright"];
|
||||
$scope.appSdk = axelor.config["application.sdk"];
|
||||
$scope.appSdkShort = $scope.appSdk.substring(0, $scope.appSdk.lastIndexOf('.'));
|
||||
$scope.appHome = axelor.config["application.home"];
|
||||
$scope.appHelp = axelor.config["application.help"];
|
||||
$scope.appYear = moment().year();
|
||||
}
|
||||
|
||||
function SystemCtrl($scope, $element, $location, $http) {
|
||||
|
||||
var promise = null;
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
if (promise) {
|
||||
return;
|
||||
}
|
||||
promise = $http.get("ws/app/sysinfo").then(function (res) {
|
||||
var info = res.data;
|
||||
_.each(info.users, function (item) {
|
||||
item.loginTime = moment(item.loginTime).format('L LT');
|
||||
item.accessTime = moment(item.accessTime).format('L LT');
|
||||
});
|
||||
$scope.info = info;
|
||||
promise = null;
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
$scope.onClose = function () {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
$scope.onRefresh();
|
||||
}
|
||||
|
||||
ui.controller("UserCtrl", ['$scope', '$element', '$location', 'DataSource', 'ViewService', UserCtrl]);
|
||||
ui.controller("SystemCtrl", ['$scope', '$element', '$location', '$http', SystemCtrl]);
|
||||
ui.controller("AboutCtrl", ['$scope', AboutCtrl]);
|
||||
|
||||
})();
|
||||
24
sophal/js/axelor.ui.js
Normal file
24
sophal/js/axelor.ui.js
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
angular.module('axelor.ui', [
|
||||
'ngRoute',
|
||||
'ngSanitize',
|
||||
'ngAnimate',
|
||||
'ui.sortable',
|
||||
'axelor.ds'
|
||||
]);
|
||||
1218
sophal/js/form/form.actions.js
Normal file
1218
sophal/js/form/form.actions.js
Normal file
File diff suppressed because it is too large
Load Diff
700
sophal/js/form/form.base.js
Normal file
700
sophal/js/form/form.base.js
Normal file
@ -0,0 +1,700 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* jshint validthis: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
var widgets = {};
|
||||
var registry = {};
|
||||
var metaWidgets = [];
|
||||
|
||||
/**
|
||||
* Perform common compile operations.
|
||||
*
|
||||
* example:
|
||||
* ui.formCompile.call(this, element, attrs)
|
||||
*/
|
||||
ui.formCompile = function(element, attrs, linkerFn) {
|
||||
|
||||
var showTitle = attrs.showTitle || this.showTitle,
|
||||
title = attrs.title || attrs.field;
|
||||
|
||||
attrs.$set('show-title', showTitle, true, 'x-show-title');
|
||||
if (title) {
|
||||
attrs.$set('title', title, true, 'x-title');
|
||||
}
|
||||
if (this.cellCss) {
|
||||
attrs.$set('x-cell-css', this.cellCss);
|
||||
}
|
||||
|
||||
function link(scope, element, attrs, controller) {
|
||||
|
||||
element.addClass(this.css).parent().addClass(this.cellCss);
|
||||
element.data('$attrs', attrs); // store the attrs object for event handlers
|
||||
|
||||
var getViewDef = this.getViewDef || scope.getViewDef || function() { return {}; };
|
||||
|
||||
var field = getViewDef.call(scope, element);
|
||||
var props = _.extend(_.pick(field, 'readonly,required,hidden,collapse,precision,scale,prompt,title,domain,css,icon,selection-in'.split(',')),
|
||||
_.pick(field.widgetAttrs || {}, 'precision,scale,domain'.split(',')));
|
||||
|
||||
var state = _.clone(props);
|
||||
|
||||
function resetAttrs() {
|
||||
var label = element.data('label');
|
||||
state = _.clone(props);
|
||||
state["force-edit"] = false;
|
||||
if (label && state.title) {
|
||||
var span = label.children('span[ui-help-popover]:first');
|
||||
if (span.length === 0) {
|
||||
span = label;
|
||||
}
|
||||
span.html(state.title);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.css) {
|
||||
element.addClass(field.css);
|
||||
}
|
||||
if (field.width && field.width !== '*' && !element.is('label')) {
|
||||
element.width(field.width);
|
||||
}
|
||||
if (field.translatable) {
|
||||
element.addClass("translatable");
|
||||
}
|
||||
|
||||
scope.$events = {};
|
||||
scope.field = field || {};
|
||||
|
||||
scope.$$readonly = undefined;
|
||||
|
||||
scope.attr = function(name) {
|
||||
if (arguments.length > 1) {
|
||||
var old = state[name];
|
||||
state[name] = arguments[1];
|
||||
if (name === "highlight") {
|
||||
setHighlight(state.highlight);
|
||||
}
|
||||
if (old !== state[name]) {
|
||||
scope.$broadcast("on:attrs-changed", {
|
||||
name: name,
|
||||
value: state[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
var res = state[name];
|
||||
if (res === undefined) {
|
||||
res = field[name];
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
scope.$on("on:edit", function(e, rec) {
|
||||
if (angular.equals(rec, {})) {
|
||||
resetAttrs();
|
||||
}
|
||||
scope.$$readonly = scope.$$isReadonly();
|
||||
});
|
||||
|
||||
scope.$on("on:attrs-changed", function(event, attr) {
|
||||
if (attr.name === "readonly" || attr.name === "force-edit") {
|
||||
scope.$$readonly = scope.$$isReadonly();
|
||||
}
|
||||
if (attr.name === "readonly") {
|
||||
element.attr("x-readonly", scope.$$readonly);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch("isEditable()", function isEditableWatch(editable, old) {
|
||||
if (editable === undefined) return;
|
||||
if (editable === old) return;
|
||||
scope.$$readonly = scope.$$isReadonly();
|
||||
});
|
||||
|
||||
// js expressions should be evaluated on dummy value changes
|
||||
if (field.name && field.name[0] === '$') {
|
||||
scope.$watch('record.' + field.name, function fieldValueWatch(a, b) {
|
||||
if (a !== b) {
|
||||
scope.$broadcastRecordChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.isRequired = function() {
|
||||
return this.attr("required") && this.text !== 0 && !this.text;
|
||||
};
|
||||
|
||||
scope.isReadonlyExclusive = function() {
|
||||
var parent = this.$parent || {};
|
||||
var readonly = this.attr("readonly");
|
||||
|
||||
if (scope._isPopup && !parent._isPopup) {
|
||||
return readonly || false;
|
||||
}
|
||||
if (parent.isReadonlyExclusive && parent.isReadonlyExclusive()) {
|
||||
return true;
|
||||
}
|
||||
if (readonly !== undefined) {
|
||||
return readonly || false;
|
||||
}
|
||||
|
||||
return readonly || false;
|
||||
};
|
||||
|
||||
scope.isReadonly = function() {
|
||||
if (scope.$$readonly === undefined) {
|
||||
scope.$$readonly = scope.$$isReadonly();
|
||||
}
|
||||
return scope.$$readonly;
|
||||
};
|
||||
|
||||
scope.$$isReadonly = function() {
|
||||
if ((this.hasPermission && !this.hasPermission('read')) || this.isReadonlyExclusive()) {
|
||||
return true;
|
||||
}
|
||||
if (!this.attr("readonly") && this.attr("force-edit")) {
|
||||
return false;
|
||||
}
|
||||
if (scope.isEditable && !scope.isEditable()) {
|
||||
return true;
|
||||
}
|
||||
return this.attr("readonly") || false;
|
||||
};
|
||||
|
||||
scope.isHidden = function() {
|
||||
return this.attr("hidden") || (this.$parent && this.$parent.isHidden && this.$parent.isHidden()) || false;
|
||||
};
|
||||
|
||||
scope.fireAction = function(name, success, error) {
|
||||
var handler = this.$events[name];
|
||||
if (handler) {
|
||||
return handler().then(success, error);
|
||||
}
|
||||
};
|
||||
|
||||
if (angular.isFunction(this._link_internal)) {
|
||||
this._link_internal.call(this, scope, element, attrs, controller);
|
||||
}
|
||||
if (angular.isFunction(this.init)) {
|
||||
this.init.call(this, scope);
|
||||
}
|
||||
if (angular.isFunction(this.link)) {
|
||||
this.link.call(this, scope, element, attrs, controller);
|
||||
}
|
||||
|
||||
function hideWidget(hidden) {
|
||||
var elem = element,
|
||||
parent = elem.parent('td,.form-item'),
|
||||
label = elem.data('label') || $(),
|
||||
label_parent = label.parent('td,.form-item'),
|
||||
isTable = parent.is('td');
|
||||
|
||||
// label scope should use same isHidden method (#1514)
|
||||
var lScope = label.data('$scope');
|
||||
if (lScope && lScope.isHidden !== scope.isHidden) {
|
||||
lScope.isHidden = scope.isHidden;
|
||||
}
|
||||
|
||||
elem = isTable && parent.length ? parent : elem;
|
||||
label = isTable && label_parent.length ? label_parent : label;
|
||||
|
||||
if (!isTable) {
|
||||
parent.toggleClass("form-item-hidden", hidden);
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
elem.add(label).hide();
|
||||
} else {
|
||||
elem.add(label).show().css('display', ''); //XXX: jquery may add display style
|
||||
}
|
||||
|
||||
return axelor.$adjustSize();
|
||||
}
|
||||
|
||||
var hideFn = _.contains(this.handles, 'isHidden') ? angular.noop : hideWidget;
|
||||
|
||||
var hiddenSet = false;
|
||||
scope.$watch("isHidden()", function isHiddenWatch(hidden, old) {
|
||||
if (hiddenSet && hidden === old) return;
|
||||
hiddenSet = true;
|
||||
return hideFn(hidden);
|
||||
});
|
||||
|
||||
var readonlySet = false;
|
||||
scope.$watch("isReadonly()", function isReadonlyWatch(readonly, old) {
|
||||
if (readonlySet && readonly === old) return;
|
||||
readonlySet = true;
|
||||
element.toggleClass("readonly", readonly);
|
||||
element.toggleClass("editable", !readonly);
|
||||
if (scope.canEdit) {
|
||||
element.toggleClass("no-edit", scope.canEdit() === false);
|
||||
}
|
||||
});
|
||||
|
||||
function setHighlight(args) {
|
||||
|
||||
function doHilite(params, passed) {
|
||||
var label = element.data('label') || $();
|
||||
element.toggleClass(params.css, passed);
|
||||
label.toggleClass(params.css.replace(/(hilite-[^-]+\b(?!-))/g, ''), passed);
|
||||
}
|
||||
|
||||
_.each(field.hilites, function(p) {
|
||||
if (p.css) doHilite(p, false);
|
||||
});
|
||||
|
||||
if (args && args.hilite && args.hilite.css) {
|
||||
doHilite(args.hilite, args.passed);
|
||||
}
|
||||
}
|
||||
|
||||
this.prepare(scope, element, attrs, controller);
|
||||
|
||||
scope.$evalAsync(function() {
|
||||
if (scope.isHidden()) {
|
||||
hideFn(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return angular.bind(this, link);
|
||||
};
|
||||
|
||||
ui.formDirective = function(name, object) {
|
||||
|
||||
if (object.compile === undefined) {
|
||||
object.compile = angular.bind(object, function(element, attrs){
|
||||
return ui.formCompile.apply(this, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
if (object.restrict === undefined) {
|
||||
object.restrict = 'EA';
|
||||
}
|
||||
|
||||
if (object.template && !object.replace) {
|
||||
object.replace = true;
|
||||
}
|
||||
|
||||
if (object.cellCss === undefined) {
|
||||
object.cellCss = 'form-item';
|
||||
}
|
||||
|
||||
if (object.scope === undefined) {
|
||||
object.scope = true;
|
||||
}
|
||||
|
||||
if (object.require === undefined) {
|
||||
object.require = '?ngModel';
|
||||
}
|
||||
|
||||
function prepare_templates($compile) {
|
||||
|
||||
object.prepare = angular.bind(object, function(scope, element, attrs, model) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (!this.template_editable && !this.template_readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.$elem_editable = null;
|
||||
scope.$elem_readonly = null;
|
||||
|
||||
function showEditable() {
|
||||
var template_editable = self.template_editable;
|
||||
if (scope.field && scope.field.editor) {
|
||||
template_editable = $('<div ui-panel-editor>');
|
||||
}
|
||||
if (_.isFunction(self.template_editable)) {
|
||||
template_editable = self.template_editable(scope);
|
||||
}
|
||||
if (!template_editable) {
|
||||
return false;
|
||||
}
|
||||
if (!scope.$elem_editable) {
|
||||
scope.$elem_editable = $compile(template_editable)(scope);
|
||||
if (self.link_editable) {
|
||||
self.link_editable.call(self, scope, scope.$elem_editable, attrs, model);
|
||||
}
|
||||
if (scope.validate) {
|
||||
model.$validators.valid = function(modelValue, viewValue) {
|
||||
return !!scope.validate(viewValue);
|
||||
};
|
||||
}
|
||||
// focus the first input field
|
||||
if (scope.$elem_editable.is('.input-append,.picker-input')) {
|
||||
scope.$elem_editable.on('click', '.btn, i', function(){
|
||||
if (!axelor.device.mobile) {
|
||||
scope.$elem_editable.find('input:first').focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (scope.$elem_editable.is(':input')) {
|
||||
scope.$elem_editable.attr('placeholder', scope.field.placeholder);
|
||||
}
|
||||
|
||||
if (scope.$elem_editable.is('.picker-input:not(.tag-select)')) {
|
||||
scope.$elem_editable.find(':input:first').attr('placeholder', scope.field.placeholder);
|
||||
}
|
||||
}
|
||||
if (scope.$elem_readonly) {
|
||||
scope.$elem_readonly.detach();
|
||||
}
|
||||
element.append(scope.$elem_editable);
|
||||
if (scope.$render_editable) scope.$render_editable();
|
||||
return true;
|
||||
}
|
||||
|
||||
function showReadonly() {
|
||||
var field = scope.field || {};
|
||||
var template_readonly = self.template_readonly;
|
||||
if (field.viewer) {
|
||||
template_readonly = field.viewer.template;
|
||||
scope.$moment = function(d) { return moment(d); };
|
||||
scope.$number = function(d) { return +d; };
|
||||
scope.$image = function (fieldName, imageName) { return ui.formatters.$image(this, fieldName, imageName); };
|
||||
scope.$fmt = function (fieldName, fieldValue) {
|
||||
var args = [this, fieldName];
|
||||
if (arguments.length > 1) {
|
||||
args.push(fieldValue);
|
||||
}
|
||||
return ui.formatters.$fmt.apply(null, args);
|
||||
};
|
||||
} else if (field.editor && field.editor.viewer) {
|
||||
return showEditable();
|
||||
}
|
||||
if (_.isFunction(self.template_readonly)) {
|
||||
template_readonly = self.template_readonly(scope);
|
||||
}
|
||||
if (!template_readonly) {
|
||||
return false;
|
||||
}
|
||||
if (_.isString(template_readonly)) {
|
||||
template_readonly = axelor.sanitize(template_readonly.trim());
|
||||
if (template_readonly[0] !== '<' || $(template_readonly).length > 1) {
|
||||
template_readonly = '<span>' + template_readonly + '</span>';
|
||||
}
|
||||
if (field.viewer) {
|
||||
template_readonly = template_readonly.replace(/^(\s*<\w+)/, '$1 ui-panel-viewer');
|
||||
}
|
||||
}
|
||||
if (!scope.$elem_readonly) {
|
||||
scope.$elem_readonly = $compile(template_readonly)(scope);
|
||||
if (self.link_readonly) {
|
||||
self.link_readonly.call(self, scope, scope.$elem_readonly, attrs, model);
|
||||
}
|
||||
}
|
||||
if (scope.$elem_editable) {
|
||||
scope.$elem_editable.detach();
|
||||
}
|
||||
element.append(scope.$elem_readonly);
|
||||
return true;
|
||||
}
|
||||
|
||||
scope.$watch("isReadonly()", function isReadonlyWatch(readonly) {
|
||||
if (readonly && showReadonly()) {
|
||||
return;
|
||||
}
|
||||
return showEditable();
|
||||
});
|
||||
scope.$watch("isRequired()", function isRequiredWatch(required, old) {
|
||||
if (required === old) return;
|
||||
var elem = element,
|
||||
label = elem.data('label') || $();
|
||||
if (label) {
|
||||
label.toggleClass('required', required);
|
||||
}
|
||||
attrs.$set('required', required);
|
||||
});
|
||||
|
||||
if (scope.field && scope.field.validIf) {
|
||||
scope.$watch("attr('valid')", function attrValidWatch(valid) {
|
||||
if (valid === undefined) return;
|
||||
model.$setValidity('invalid', valid);
|
||||
});
|
||||
}
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
if (scope.$elem_editable) {
|
||||
scope.$elem_editable.remove();
|
||||
scope.$elem_editable = null;
|
||||
}
|
||||
if (scope.$elem_readonly) {
|
||||
scope.$elem_readonly.remove();
|
||||
scope.$elem_readonly = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
return ui.directive(name, ['$compile', function($compile) {
|
||||
return prepare_templates($compile);
|
||||
}]);
|
||||
};
|
||||
|
||||
var FormItem = {
|
||||
|
||||
css: 'form-item',
|
||||
|
||||
cellCss: 'form-item'
|
||||
};
|
||||
|
||||
var FormInput = {
|
||||
|
||||
_link_internal: function(scope, element, attrs, model) {
|
||||
|
||||
scope.format = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.parse = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.validate = function(value) {
|
||||
return true;
|
||||
};
|
||||
|
||||
scope.setValue = function(value, fireOnChange) {
|
||||
|
||||
var val = this.parse(value);
|
||||
var txt = this.format(value);
|
||||
var onChange = this.$events.onChange;
|
||||
|
||||
model.$setViewValue(val);
|
||||
this.text = txt;
|
||||
|
||||
model.$render();
|
||||
if (onChange && fireOnChange) {
|
||||
onChange();
|
||||
}
|
||||
};
|
||||
|
||||
scope.getValue = function() {
|
||||
if (model) {
|
||||
return model.$viewValue;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
scope.getText = function() {
|
||||
return this.text;
|
||||
};
|
||||
|
||||
scope.initValue = function(value) {
|
||||
this.text = this.format(value);
|
||||
};
|
||||
|
||||
model.$render = function() {
|
||||
scope.initValue(scope.getValue());
|
||||
if (scope.$render_editable) {
|
||||
scope.$render_editable();
|
||||
}
|
||||
if (scope.$render_readonly) {
|
||||
scope.$render_readonly();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear invalid fields (use $setPrestine of angular.js 1.1)
|
||||
scope.$on('on:new', function(e, rec) {
|
||||
if (!model.$valid && model.$viewValue) {
|
||||
model.$viewValue = undefined;
|
||||
model.$render();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
|
||||
scope.$render_editable = function() {
|
||||
var value = this.format(this.getValue());
|
||||
element.val(value);
|
||||
};
|
||||
|
||||
function bindListeners() {
|
||||
var onChange = scope.$events.onChange || angular.noop,
|
||||
onChangePending = false;
|
||||
|
||||
function listener() {
|
||||
var value = _.str.trim(element.val()) || null;
|
||||
if (value !== model.$viewValue) {
|
||||
scope.$applyAsync(function() {
|
||||
var val = scope.parse(value);
|
||||
var txt = scope.format(value);
|
||||
if (scope.$$setEditorValue && !scope.record) { // m2o editor with null value?
|
||||
scope.$$setEditorValue({}, false);
|
||||
}
|
||||
model.$setViewValue(val);
|
||||
scope.text = txt;
|
||||
});
|
||||
onChangePending = true;
|
||||
}
|
||||
}
|
||||
|
||||
var field = scope.field || {};
|
||||
if (!field.bind) {
|
||||
element.bind('input', listener);
|
||||
}
|
||||
|
||||
element.change(listener);
|
||||
|
||||
element.blur(function(e){
|
||||
if (onChangePending) {
|
||||
onChangePending = false;
|
||||
setTimeout(onChange);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (element.is(':input')) {
|
||||
setTimeout(bindListeners);
|
||||
// clear input value
|
||||
if (scope.$$setEditorValue) {
|
||||
scope.$on('on:edit', function () {
|
||||
if (model.$viewValue && !scope.record) {
|
||||
model.$setViewValue(undefined);
|
||||
scope.$render_editable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope.$render_editable();
|
||||
},
|
||||
|
||||
link_readonly: function(scope, element, attrs, model) {
|
||||
|
||||
},
|
||||
|
||||
template_editable: '<input type="text">',
|
||||
|
||||
template_readonly: '<span class="display-text">{{text}}</span>',
|
||||
|
||||
template: '<span class="form-item-container"></span>'
|
||||
};
|
||||
|
||||
function inherit(array) {
|
||||
|
||||
var args = _.chain(array).rest(1).flatten(true).value();
|
||||
var last = _.last(args);
|
||||
var base = null;
|
||||
var obj = {};
|
||||
|
||||
_.chain(args).each(function(source, index) {
|
||||
if (_.isString(source)) {
|
||||
source = widgets[source];
|
||||
}
|
||||
if (index === args.length - 2) {
|
||||
base = source;
|
||||
}
|
||||
_.extend(obj, source);
|
||||
});
|
||||
|
||||
if (!base) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function overridden(name) {
|
||||
return name !== "controller" &&
|
||||
_.isFunction(last[name]) && !last[name].$inject &&
|
||||
_.isFunction(base[name]);
|
||||
}
|
||||
|
||||
function override(name, fn){
|
||||
return function() {
|
||||
var tmp = this._super;
|
||||
this._super = base[name];
|
||||
var ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
for(var name in last) {
|
||||
if (overridden(name)) {
|
||||
obj[name] = override(name, obj[name]);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
ui.formWidget = function(name, object) {
|
||||
var obj = inherit(arguments);
|
||||
var widget = _.str.capitalize(name.replace(/^ui/, ''));
|
||||
var directive = "ui" + widget;
|
||||
|
||||
if (obj.metaWidget) {
|
||||
metaWidgets.push(widget);
|
||||
}
|
||||
|
||||
registry[directive] = directive;
|
||||
_.each(obj.widgets, function(alias){
|
||||
registry[alias] = directive;
|
||||
});
|
||||
delete obj.widgets;
|
||||
|
||||
widgets[widget] = _.clone(obj);
|
||||
|
||||
ui.formDirective(directive, obj);
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
ui.formItem = function(name, object) {
|
||||
return ui.formWidget(name, FormItem, _.rest(arguments, 1));
|
||||
};
|
||||
|
||||
ui.formInput = function(name, object) {
|
||||
return ui.formWidget(name, FormItem, FormInput, _.rest(arguments, 1));
|
||||
};
|
||||
|
||||
ui.getWidget = function(type) {
|
||||
var name = type,
|
||||
widget = registry["ui" + name] || registry[name];
|
||||
if (!widget) {
|
||||
name = _.str.classify(name);
|
||||
widget = registry["ui" + name] || registry[name];
|
||||
}
|
||||
if (widget) {
|
||||
widget = widget.replace(/^ui/, '');
|
||||
return _.chain(widget).underscored().dasherize().value();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
ui.getWidgetDef = function (name) {
|
||||
return widgets[name];
|
||||
};
|
||||
|
||||
ui.getMetaWidgets = function () {
|
||||
return metaWidgets;
|
||||
};
|
||||
|
||||
})();
|
||||
134
sophal/js/form/form.code.js
Normal file
134
sophal/js/form/form.code.js
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* jshint newcap: false */
|
||||
/* global CodeMirror: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.formInput('CodeEditor', {
|
||||
|
||||
css: "code-editor",
|
||||
|
||||
metaWidget: true,
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
var editor = null;
|
||||
var loading = false;
|
||||
|
||||
var field = scope.field;
|
||||
var props = {
|
||||
autofocus: true,
|
||||
lineNumbers: true,
|
||||
tabSize : 2,
|
||||
indentUnit : 2,
|
||||
indentWithTabs: false,
|
||||
theme: field.codeTheme || "default",
|
||||
extraKeys: {
|
||||
'Ctrl-F': function () {}
|
||||
}
|
||||
};
|
||||
|
||||
if (field.mode || field.codeSyntax) {
|
||||
props.mode = field.mode || field.codeSyntax;
|
||||
}
|
||||
|
||||
if (props.mode === "xml") {
|
||||
props = _.extend(props, {
|
||||
foldGutter : true,
|
||||
gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
autoCloseBrackets : true,
|
||||
autoCloseTags : true
|
||||
});
|
||||
}
|
||||
|
||||
if (field.height) {
|
||||
element.height(field.height);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
props.readOnly = scope.$$readonly;
|
||||
editor = CodeMirror(element.get(0), props);
|
||||
model.$render();
|
||||
readonlySet(props.readOnly);
|
||||
editor.on("change", changed);
|
||||
});
|
||||
|
||||
scope.$watch('$$readonly', readonlySet);
|
||||
|
||||
model.$render = function() {
|
||||
loading = true;
|
||||
var val = model.$modelValue;
|
||||
if (editor) {
|
||||
editor.setValue(val || "");
|
||||
editor.clearHistory();
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
|
||||
model.$formatters.push(function (value) {
|
||||
return value || '';
|
||||
});
|
||||
|
||||
function readonlySet(readonly) {
|
||||
if (editor) {
|
||||
editor.setOption('readOnly', _.toBoolean(readonly));
|
||||
}
|
||||
}
|
||||
|
||||
function changed(instance, changedObj) {
|
||||
if (loading || !editor) return;
|
||||
var value = editor.getValue();
|
||||
if (value !== model.$viewValue) {
|
||||
model.$setViewValue(value);
|
||||
}
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (element[0].offsetHeight === 0) return; // is hidden?
|
||||
if (editor) {
|
||||
editor.refresh();
|
||||
}
|
||||
element.width('');
|
||||
}
|
||||
|
||||
element.resizable({
|
||||
handles: 's',
|
||||
resize: resize
|
||||
});
|
||||
|
||||
scope.$onAdjust(resize);
|
||||
},
|
||||
|
||||
replace: true,
|
||||
|
||||
transclude: true,
|
||||
|
||||
template_editable: null,
|
||||
|
||||
template_readonly: null,
|
||||
|
||||
template: '<div ng-transclude></div>'
|
||||
});
|
||||
|
||||
})();
|
||||
841
sophal/js/form/form.container.js
Normal file
841
sophal/js/form/form.container.js
Normal file
@ -0,0 +1,841 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
// this directive is used as a replacement for ng-transclude directive
|
||||
// which fails to keep scope hierarchy (see: https://github.com/angular/angular.js/issues/1809)
|
||||
ui.directive('uiTransclude', function() {
|
||||
return {
|
||||
compile: function(tElement, tAttrs, transclude) {
|
||||
return function(scope, element, attrs, ctrl) {
|
||||
transclude(scope.$new(), function(clone) {
|
||||
element.append(clone);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The Group widget.
|
||||
*
|
||||
*/
|
||||
ui.formWidget('Group', {
|
||||
|
||||
css: 'form-item-group',
|
||||
cellCss: 'form-item v-align-top',
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var props = scope.field;
|
||||
|
||||
scope.collapsed = false;
|
||||
|
||||
scope.canCollapse = function() {
|
||||
return props.canCollapse || props.collapseIf;
|
||||
};
|
||||
|
||||
scope.setCollapsed = function(collapsed) {
|
||||
scope.collapsed = collapsed;
|
||||
element.children('legend').nextAll(':not(br)')[collapsed ? 'hide' : 'show']();
|
||||
axelor.$adjustSize();
|
||||
};
|
||||
|
||||
scope.toggle = function() {
|
||||
scope.collapsed = !scope.collapsed;
|
||||
scope.setCollapsed(scope.collapsed);
|
||||
};
|
||||
|
||||
scope.$watch("attr('collapse')", function groupCollapseWatch(collapsed) {
|
||||
scope.setCollapsed(collapsed);
|
||||
});
|
||||
|
||||
// if auto title, then don't show it
|
||||
if (attrs.title === attrs.field) {
|
||||
attrs.$set('title', '');
|
||||
}
|
||||
|
||||
if (props.showTitle !== false) {
|
||||
scope.$watch('attr("title")', function groupTitleWatch(value){
|
||||
scope.title = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
transclude: true,
|
||||
template:
|
||||
'<fieldset ng-class="{\'bordered-box\': title, \'has-title\': title}" x-layout-selector="> div:first">'+
|
||||
'<legend ng-show="title">'+
|
||||
'<i ng-show="canCollapse()" ng-click="toggle()" ng-class="{\'fa fa-plus\': collapsed, \'fa fa-minus\': !collapsed}"></i>'+
|
||||
'<span ng-bind-html="title"></span></legend>'+
|
||||
'<div ui-transclude></div>'+
|
||||
'</fieldset>'
|
||||
});
|
||||
|
||||
ui.formWidget('Portlet', {
|
||||
|
||||
css: 'form-item-portlet',
|
||||
cellCss: 'form-item v-align-top',
|
||||
|
||||
showTitle: false,
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var field = scope.field;
|
||||
|
||||
scope.canSearch = field.canSearch !== "false";
|
||||
scope.actionName = field.action;
|
||||
|
||||
if (field.name) {
|
||||
scope.formPath = field.name;
|
||||
}
|
||||
|
||||
if (field.height) {
|
||||
element.height(field.height);
|
||||
}
|
||||
|
||||
element.resizable({
|
||||
handles: 's',
|
||||
resize: _.debounce(function() {
|
||||
axelor.$adjustSize();
|
||||
element.width('auto');
|
||||
}, 100)
|
||||
});
|
||||
},
|
||||
|
||||
template:
|
||||
'<div>'+
|
||||
'<div ui-view-portlet x-action="{{actionName}}" x-can-search="{{canSearch}}"></div>'+
|
||||
'</div>'
|
||||
});
|
||||
|
||||
ui.formWidget('Dashlet', {
|
||||
|
||||
css: 'dashboard',
|
||||
|
||||
showTitle: false,
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var field = scope.field;
|
||||
var dashlet = _.extend({}, scope.field);
|
||||
|
||||
scope.dashlet = dashlet;
|
||||
scope.formPath = field.name || field.action;
|
||||
|
||||
scope.$watch('attr("title")', function dashletTitleWatch(title, old) {
|
||||
if (title === old) {
|
||||
return;
|
||||
}
|
||||
var dashletScope = element.children('[ui-view-dashlet]').scope();
|
||||
if (dashletScope) {
|
||||
dashletScope.title = title;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
template:
|
||||
'<div>'+
|
||||
'<div ui-view-dashlet></div>'+
|
||||
'</div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Tabs widget (notebook).
|
||||
*/
|
||||
ui.formWidget('Tabs', {
|
||||
|
||||
cellCss: 'form-item v-align-top',
|
||||
|
||||
widgets: ['Notebook'],
|
||||
|
||||
controller: ['$scope', '$element', function($scope, $element) {
|
||||
|
||||
var tabs = $scope.tabs = [],
|
||||
selected = -1;
|
||||
|
||||
var doOnSelectPending = false;
|
||||
var doOnSelect = _.debounce(function () {
|
||||
var select = tabs[selected];
|
||||
if (doOnSelectPending || !select) {
|
||||
return;
|
||||
}
|
||||
doOnSelectPending = true;
|
||||
$scope.waitForActions(function () {
|
||||
if (select.handleSelect) {
|
||||
select.handleSelect();
|
||||
}
|
||||
$scope.waitForActions(function () {
|
||||
doOnSelectPending = false;
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
|
||||
$scope.select = function(tab) {
|
||||
|
||||
var current = selected;
|
||||
|
||||
angular.forEach(tabs, function(tab, i){
|
||||
tab.tabSelected = false;
|
||||
});
|
||||
|
||||
tab.tabSelected = true;
|
||||
selected = _.indexOf(tabs, tab);
|
||||
|
||||
if (current === selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if ($scope.$tabs) {
|
||||
$scope.$tabs.trigger('adjust:tabs');
|
||||
}
|
||||
axelor.$adjustSize();
|
||||
if(current != selected){
|
||||
doOnSelect();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('on:edit', function (e, record) {
|
||||
if ($scope.record === record) {
|
||||
doOnSelect();
|
||||
}
|
||||
});
|
||||
|
||||
this.addTab = function(tab) {
|
||||
if (tabs.length === 0) $scope.select(tab);
|
||||
tab.index = tabs.length;
|
||||
tabs.push(tab);
|
||||
};
|
||||
|
||||
function inRange(index) {
|
||||
return index > -1 && index < tabs.length;
|
||||
}
|
||||
|
||||
function findItem(index) {
|
||||
return $element.find('ul.nav-tabs:first > li:nth-child(' + (index+1) + ')');
|
||||
}
|
||||
|
||||
this.showTab = function(index) {
|
||||
|
||||
if (!inRange(index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tab = tabs[index];
|
||||
var item = findItem(index);
|
||||
|
||||
tab.hidden = false;
|
||||
item.show();
|
||||
|
||||
if (selected == -1 || selected === index) {
|
||||
return $scope.select(tabs[index]);
|
||||
}
|
||||
|
||||
axelor.$adjustSize();
|
||||
};
|
||||
|
||||
this.hideTab = function(index) {
|
||||
|
||||
if (!inRange(index))
|
||||
return;
|
||||
|
||||
var item = findItem(index),
|
||||
tab = tabs[index];
|
||||
|
||||
var wasHidden = item.is(":hidden");
|
||||
|
||||
item.hide();
|
||||
item.removeClass('active');
|
||||
|
||||
tab.hidden = true;
|
||||
tab.tabSelected = false;
|
||||
|
||||
if (!wasHidden && selected > -1 && selected !== index)
|
||||
return axelor.$adjustSize();
|
||||
|
||||
for(var i = 0 ; i < tabs.length ; i++) {
|
||||
tab = tabs[i];
|
||||
if (!tab.hidden) {
|
||||
return $scope.select(tabs[i]);
|
||||
}
|
||||
}
|
||||
selected = -1;
|
||||
};
|
||||
|
||||
$scope.setTitle = function(value,index){
|
||||
var item = findItem(index),
|
||||
pageScope = item.first().data('$scope');
|
||||
|
||||
pageScope.tab.title = value;
|
||||
};
|
||||
}],
|
||||
|
||||
link: function(scope, elem, attrs) {
|
||||
|
||||
var props = scope.field;
|
||||
|
||||
scope.$tabs = $(elem).bsTabs({
|
||||
closable: false
|
||||
});
|
||||
|
||||
elem.on('click', '.dropdown-toggle', function(e){
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
|
||||
// set height (#1011)
|
||||
if (props.height) {
|
||||
elem.children('.tab-content:first').height(props.height);
|
||||
}
|
||||
},
|
||||
transclude: true,
|
||||
template:
|
||||
'<div class="tabbable-tabs">' +
|
||||
'<div class="nav-tabs-wrap">' +
|
||||
'<div class="nav-tabs-scroll-l"><a tabindex="-1" href="#"><i class="fa fa-chevron-left"></i></a></div>' +
|
||||
'<div class="nav-tabs-scroll-r"><a tabindex="-1" href="#"><i class="fa fa-chevron-right"></i></a></div>' +
|
||||
'<div class="nav-tabs-strip">' +
|
||||
'<ul class="nav nav-tabs">' +
|
||||
'<li tabindex="-1" ng-repeat="tab in tabs" ng-class="{active:tab.tabSelected}">'+
|
||||
'<a tabindex="-1" href="" ng-click="select(tab)">'+
|
||||
'<img class="prefix-icon" ng-show="tab.icon" ng-src="{{tab.icon}}">'+
|
||||
'<span ng-bind-html="tab.title"></span>'+
|
||||
'</a>' +
|
||||
'</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<div class="nav-tabs-menu">'+
|
||||
'<div class="dropdown pull-right">'+
|
||||
'<a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="caret"></i></a>'+
|
||||
'<ul class="dropdown-menu" role="menu">'+
|
||||
'<li ng-repeat="tab in tabs">'+
|
||||
'<a tabindex="-1" href="javascript: void(0)" ng-click="select(tab)" ng-bind-html="tab.title"></a>'+
|
||||
'</li>' +
|
||||
'</ul>' +
|
||||
'</a>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'<div class="tab-content" ui-transclude></div>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Tab widget (notebook page).
|
||||
*/
|
||||
ui.formWidget('Tab', {
|
||||
|
||||
require: '^uiTabs',
|
||||
|
||||
widgets: ['Page'],
|
||||
|
||||
handles: ['isHidden'],
|
||||
|
||||
link: function(scope, elem, attrs, tabs) {
|
||||
|
||||
scope.tabSelected = false;
|
||||
scope.icon = scope.field && scope.field.icon;
|
||||
|
||||
tabs.addTab(scope);
|
||||
|
||||
scope.$watch('attr("title")', function tabTitleWatch(value){
|
||||
scope.title = value;
|
||||
});
|
||||
|
||||
scope.$watch("isHidden()", function tabHiddenWatch(hidden, old) {
|
||||
if (hidden) {
|
||||
return tabs.hideTab(scope.index);
|
||||
}
|
||||
return tabs.showTab(scope.index);
|
||||
});
|
||||
|
||||
scope.handleSelect = function () {
|
||||
var onSelect = scope.$events.onSelect;
|
||||
if (onSelect && !elem.is(":hidden")) {
|
||||
onSelect();
|
||||
}
|
||||
};
|
||||
},
|
||||
cellCss: 'form-item v-align-top',
|
||||
transclude: true,
|
||||
template: '<div ui-actions class="tab-pane" ng-class="{active: tabSelected}" x-layout-selector="> div:first">'+
|
||||
'<div ui-transclude></div>'+
|
||||
'</div>'
|
||||
});
|
||||
|
||||
ui.formWidget('ButtonGroup', {
|
||||
|
||||
link: function (scope, element, attrs) {
|
||||
function adjustButtons() {
|
||||
var visible = element.children('.btn:visible').length;
|
||||
if (visible) {
|
||||
element.children('.btn:visible')
|
||||
.css('max-width', (100.00/visible) + '%')
|
||||
.css('width', (100.00/visible) + '%');
|
||||
}
|
||||
}
|
||||
scope.$watch(adjustButtons);
|
||||
scope.$callWhen(function () {
|
||||
return element.is(':visible');
|
||||
}, adjustButtons);
|
||||
},
|
||||
transclude: true,
|
||||
template_editable: null,
|
||||
template_readonly: null,
|
||||
template:
|
||||
"<div class='btn-group' ui-transclude></div>"
|
||||
});
|
||||
|
||||
ui.formWidget('Panel', {
|
||||
|
||||
showTitle: false,
|
||||
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var field = scope.field || {};
|
||||
var body = element.children(".panel-body");
|
||||
|
||||
element.addClass(field.serverType);
|
||||
if (field.sidebar && !attrs.itemSpan) {
|
||||
attrs.$set('itemSpan', 12, true, 'x-item-span');
|
||||
}
|
||||
|
||||
scope.menus = null;
|
||||
if (field.menu) {
|
||||
scope.menus = [field.menu];
|
||||
}
|
||||
|
||||
scope.canCollapse = function() {
|
||||
return field.canCollapse || field.collapseIf;
|
||||
};
|
||||
|
||||
scope.setCollapsed = function(collapsed) {
|
||||
var old = scope.collapsed;
|
||||
var action = collapsed ? "hide" : "show";
|
||||
|
||||
scope.collapsed = collapsed;
|
||||
scope.collapsedIcon = collapsed ? 'fa-chevron-down' : 'fa-chevron-up';
|
||||
|
||||
if (collapsed === old) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.removeClass("collapsed");
|
||||
body[action]("blind", 200, function () {
|
||||
element.toggleClass("collapsed", !!collapsed);
|
||||
if (body.css('display') !== 'none' && action === 'hide') {
|
||||
body.hide();
|
||||
}
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
};
|
||||
|
||||
scope.toggle = function() {
|
||||
if (scope.canCollapse()) {
|
||||
scope.setCollapsed(!scope.collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
scope.$watch("attr('collapse')", function panelCollapseWatch(collapsed) {
|
||||
scope.setCollapsed(collapsed);
|
||||
});
|
||||
|
||||
var nested = element.parents('.panel:first').length > 0;
|
||||
if (nested) {
|
||||
element.addClass("panel-nested");
|
||||
}
|
||||
if (field.showFrame === false) {
|
||||
element.addClass('noframe');
|
||||
}
|
||||
scope.notitle = field.showFrame === false || field.showTitle === false;
|
||||
scope.title = field.title;
|
||||
scope.$watch('attr("title")', function panelTitleWatch(title, old) {
|
||||
if (title === undefined || title === old) return;
|
||||
scope.title = title;
|
||||
});
|
||||
|
||||
var icon = field.icon;
|
||||
var iconBg = field.iconBackground;
|
||||
|
||||
if (icon && icon.indexOf('fa-') === 0) {
|
||||
scope.icon = icon;
|
||||
} else if (icon) {
|
||||
scope.image = icon;
|
||||
}
|
||||
|
||||
if (scope.icon && iconBg) {
|
||||
setTimeout(function() {
|
||||
var iconElem = element.children('.panel-header').children('.panel-icon');
|
||||
if (iconBg.indexOf("#") === 0) {
|
||||
iconElem.css('background-color', iconBg);
|
||||
} else {
|
||||
iconElem.addClass('bg-' + iconBg);
|
||||
}
|
||||
iconElem.addClass('has-bg');
|
||||
iconElem.find('i').addClass('fg-white');
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
var nestedJson = element.parents('.panel-json:first').length > 0;
|
||||
if (nestedJson) {
|
||||
element.removeClass("panel-nested");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
transclude: true,
|
||||
template:
|
||||
"<div class='panel panel-default'>" +
|
||||
"<div class='panel-header' ng-click='toggle()' ng-if='!notitle && field.title' ng-class=\"{'clickable-header' : canCollapse()}\" tabindex='-1'>" +
|
||||
"<div class='panel-icon' ng-if='icon'><i class='fa' ng-class='icon'></i></div>" +
|
||||
"<img class='panel-image' ng-if='image' ng-src='{{image}}'>" +
|
||||
"<div class='panel-title'>{{title}}</div>" +
|
||||
"<div ng-if='menus' ui-menu-bar menus='menus' handler='this'></div>" +
|
||||
"<div ng-show='canCollapse()' class='panel-icons'>" +
|
||||
"<a href=''><i class='fa' ng-class='collapsedIcon'></i></a>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"<div class='panel-body' ui-transclude></div>" +
|
||||
"</div>"
|
||||
});
|
||||
|
||||
ui.formWidget('PanelStack', {
|
||||
transclude: true,
|
||||
template: "<div class='panel-stack'>" +
|
||||
"<span ui-transclude></span>" +
|
||||
"</div>"
|
||||
});
|
||||
|
||||
ui.formWidget('PanelTabs', {
|
||||
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.tabs = [];
|
||||
scope.more = null;
|
||||
|
||||
element.find('> .tab-content > div').each(function (index) {
|
||||
var elem = $(this);
|
||||
var tab = {
|
||||
title: elem.attr('x-title'),
|
||||
selected: false,
|
||||
hidden: false,
|
||||
elem: elem,
|
||||
tabItem: $(),
|
||||
menuItem: $()
|
||||
};
|
||||
scope.tabs.push(tab);
|
||||
});
|
||||
|
||||
var selected = null;
|
||||
var adjustPending = false;
|
||||
|
||||
function findTab(tab) {
|
||||
var found = scope.tabs[tab] || tab;
|
||||
if (!found || _.isNumber(found)) {
|
||||
return null;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
var doOnSelectPending = false;
|
||||
var doOnSelect = _.debounce(function () {
|
||||
if (doOnSelectPending || !selected || !selected.elem) {
|
||||
return;
|
||||
}
|
||||
doOnSelectPending = true;
|
||||
scope.waitForActions(function () {
|
||||
var elemScope = selected.elem.scope();
|
||||
if (elemScope.handleSelect) {
|
||||
elemScope.handleSelect();
|
||||
}
|
||||
doOnSelectPending = false;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
scope.selectTab = function(tab) {
|
||||
var current = selected;
|
||||
var found = findTab(tab);
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
scope.tabs.forEach(function (current) {
|
||||
current.selected = false;
|
||||
current.elem.hide();
|
||||
});
|
||||
|
||||
selected = found;
|
||||
|
||||
found.selected = true;
|
||||
found.elem.show();
|
||||
|
||||
found.elem
|
||||
.add(found.tabItem)
|
||||
.add(found.menuItem)
|
||||
.addClass('active');
|
||||
|
||||
setTimeout(function () {
|
||||
scope.$broadcast('tab:select');
|
||||
elemTabs.removeClass('open');
|
||||
elemMenu.removeClass('open');
|
||||
axelor.$adjustSize();
|
||||
if (current != selected) {
|
||||
doOnSelect();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.showTab = function (tab) {
|
||||
var found = findTab(tab);
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
adjustPending = true;
|
||||
|
||||
found.hidden = false;
|
||||
found.tabItem.show();
|
||||
|
||||
if (!selected || selected === found) {
|
||||
return scope.selectTab(found);
|
||||
}
|
||||
|
||||
found.elem.hide();
|
||||
|
||||
axelor.$adjustSize();
|
||||
};
|
||||
|
||||
scope.hideTab = function (tab) {
|
||||
var found = findTab(tab);
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
adjustPending = true;
|
||||
|
||||
var wasHidden = found.hidden;
|
||||
|
||||
found.hidden = true;
|
||||
found.selected = false;
|
||||
found.elem.hide();
|
||||
|
||||
found.tabItem.add(found.menuItem).hide().removeClass('active');
|
||||
|
||||
if (!wasHidden && selected && selected !== found) {
|
||||
return axelor.$adjustSize();
|
||||
}
|
||||
|
||||
var tabs = scope.tabs;
|
||||
for(var i = 0 ; i < tabs.length ; i++) {
|
||||
tab = tabs[i];
|
||||
if (!tab.hidden) {
|
||||
return scope.selectTab(tabs[i]);
|
||||
}
|
||||
}
|
||||
selected = null;
|
||||
};
|
||||
|
||||
var lastWidth = 0;
|
||||
var lastTab = null;
|
||||
|
||||
var elemTabs = $();
|
||||
var elemMenu = $();
|
||||
var elemMenuTitle = $();
|
||||
var elemMenuItems = $();
|
||||
|
||||
function setup() {
|
||||
elemTabs = element.children('.nav-tabs').children('li:not(.dropdown)');
|
||||
elemMenu = element.children('.nav-tabs').children('li.dropdown');
|
||||
elemMenuTitle = elemMenu.children('a:first').children('span');
|
||||
elemMenuItems = elemMenu.find('li');
|
||||
|
||||
_.each(scope.tabs, function (tab, index) {
|
||||
tab.tabItem = $(elemTabs[index]);
|
||||
tab.menuItem = $(elemMenuItems[index]);
|
||||
});
|
||||
}
|
||||
|
||||
var setMenuTitle = (function() {
|
||||
var setActive = _.debounce(function(selected) {
|
||||
elemMenu.toggleClass('active', !!selected);
|
||||
});
|
||||
return function setMenuTitle(selected) {
|
||||
elemMenu.show();
|
||||
elemMenuTitle.html(selected && selected.title);
|
||||
setActive(selected);
|
||||
};
|
||||
}());
|
||||
|
||||
function adjust() {
|
||||
if (elemTabs === null || !scope.tabs || element.is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentWidth = element.width() - 2;
|
||||
if (parentWidth === lastWidth && lastTab === selected && !adjustPending) {
|
||||
return;
|
||||
}
|
||||
lastWidth = parentWidth;
|
||||
lastTab = selected;
|
||||
|
||||
elemTabs.parent().css('visibility', 'hidden');
|
||||
elemMenu.hide();
|
||||
|
||||
// show visible tabs
|
||||
scope.tabs.forEach(function (tab, i) {
|
||||
if (tab.hidden) {
|
||||
$(elemTabs[i]).hide();
|
||||
} else {
|
||||
$(elemTabs[i]).show();
|
||||
}
|
||||
});
|
||||
|
||||
if (elemTabs.parent().width() <= parentWidth) {
|
||||
elemTabs.parent().css('visibility', '');
|
||||
return;
|
||||
}
|
||||
|
||||
setMenuTitle(null);
|
||||
|
||||
var elem = null;
|
||||
var index = elemTabs.length;
|
||||
var selectedIndex = scope.tabs.indexOf(selected);
|
||||
|
||||
while (elemTabs.parent().width() > parentWidth) {
|
||||
elem = $(elemTabs[--index]);
|
||||
elem.hide();
|
||||
if (index === selectedIndex) {
|
||||
setMenuTitle(selected);
|
||||
}
|
||||
}
|
||||
|
||||
elemMenuItems.hide();
|
||||
var tab = null;
|
||||
while(index < scope.tabs.length) {
|
||||
tab = scope.tabs[index++];
|
||||
if (!tab.hidden) {
|
||||
tab.menuItem.show();
|
||||
}
|
||||
}
|
||||
|
||||
elemTabs.parent().css('visibility', '');
|
||||
}
|
||||
|
||||
var adjusting = false;
|
||||
scope.$onAdjust(function() {
|
||||
if (adjusting) { return; }
|
||||
try {
|
||||
adjusting = true;
|
||||
adjust();
|
||||
} finally {
|
||||
adjusting = false;
|
||||
adjustPending = false;
|
||||
}
|
||||
}, 10);
|
||||
|
||||
scope.$timeout(function() {
|
||||
setup();
|
||||
var first = _.find(scope.tabs, function (tab) {
|
||||
return !tab.hidden;
|
||||
});
|
||||
if (first) {
|
||||
scope.selectTab(first);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('on:edit', function (e, record) {
|
||||
if (scope.record === record && !doOnSelectPending) {
|
||||
scope.ajaxStop(doOnSelect, 100);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch(function tabsWatch() {
|
||||
var hidden = scope.attr('hidden');
|
||||
if (hidden) return;
|
||||
// show selected tab only
|
||||
scope.tabs.forEach(function (tab) {
|
||||
if (!tab.selected) tab.elem.hide();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
transclude: true,
|
||||
template:
|
||||
"<div class='panel-tabs tabbable-tabs'>" +
|
||||
"<ul class='nav nav-tabs nav-tabs-responsive'>" +
|
||||
"<li ng-repeat='tab in tabs' ng-class='{active: tab.selected}'>" +
|
||||
"<a tabindex='-1' href='' ng-click='selectTab(tab)' ng-bind-html='tab.title'></a>" +
|
||||
"</li>" +
|
||||
"<li class='dropdown' style='display: none'>" +
|
||||
"<a tabindex='-1' href='' class='dropdown-toggle' data-toggle='dropdown'><span></span><b class='caret'></b></a>" +
|
||||
"<ul class='dropdown-menu pull-right'>" +
|
||||
"<li ng-repeat='tab in tabs' ng-class='{active: tab.selected}'>" +
|
||||
"<a tabindex='-1' href='' ng-click='selectTab(tab)' ng-bind-html='tab.title'></a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"<div class='tab-content' ui-transclude></div>" +
|
||||
"</div>"
|
||||
});
|
||||
|
||||
ui.formWidget('PanelTab', {
|
||||
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var index = element.parent().children().index(element);
|
||||
var tab = null;
|
||||
var isHidden = scope.isHidden;
|
||||
|
||||
function findTab() {
|
||||
return tab || (tab = (scope.tabs||[])[index]) || {};
|
||||
}
|
||||
|
||||
scope.handleSelect = function () {
|
||||
var onTabSelect = scope.$events.onTabSelect;
|
||||
if (onTabSelect && !element.is(":hidden")) {
|
||||
onTabSelect();
|
||||
}
|
||||
};
|
||||
|
||||
scope.isHidden = function () {
|
||||
var tab = findTab();
|
||||
return !tab.selected || isHidden.call(scope);
|
||||
};
|
||||
|
||||
scope.$watch("attr('title')", function tabTitleWatch(value) {
|
||||
var tab = findTab();
|
||||
tab.title = value;
|
||||
});
|
||||
|
||||
scope.$watch("attr('hidden')", function tabHiddenWatch(hidden, old) {
|
||||
scope.$evalAsync(function () {
|
||||
if (hidden) {
|
||||
return scope.hideTab(index);
|
||||
}
|
||||
return scope.showTab(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
234
sophal/js/form/form.converters.js
Normal file
234
sophal/js/form/form.converters.js
Normal file
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 currencySymbols = {
|
||||
en: '\u0024',
|
||||
fr: '\u20AC'
|
||||
};
|
||||
|
||||
var thousandSeparator = {
|
||||
en: ',',
|
||||
fr: ' '
|
||||
};
|
||||
|
||||
function addCurrency(value, symbol) {
|
||||
if (value && symbol) {
|
||||
var val = '' + value;
|
||||
if (axelor.config['user.lang'] === 'fr' ) {
|
||||
return val.endsWith(symbol) ? val : val + ' ' + symbol;
|
||||
}
|
||||
return val.startsWith(symbol) ? val : symbol + val;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function canSetNested(record, name) {
|
||||
if (record && name && name in record) {
|
||||
return true;
|
||||
}
|
||||
if (name) {
|
||||
var path = name.split('.');
|
||||
var val = record || {};
|
||||
var idx = 0;
|
||||
while (idx < path.length - 1) {
|
||||
val = val[path[idx++]];
|
||||
if (!val) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function findNested(record, name) {
|
||||
if (record && name && name in record) {
|
||||
return record[name] === undefined ? null : record[name];
|
||||
}
|
||||
if (name) {
|
||||
var path = name.split('.');
|
||||
var val = record || {};
|
||||
var idx = 0;
|
||||
while (val && idx < path.length) {
|
||||
val = val[path[idx++]];
|
||||
}
|
||||
if (idx === path.length) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setNested(record, name, value) {
|
||||
if (!record || !name) return record;
|
||||
var path = name.split('.');
|
||||
var nested = record;
|
||||
var idx = -1;
|
||||
while (++idx < path.length) {
|
||||
var key = path[idx];
|
||||
if (idx !== path.length - 1) {
|
||||
nested = nested[key] || (nested[key] = {});
|
||||
} else {
|
||||
nested[key] = value;
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
// override angular.js currency filter
|
||||
ui.filter('currency', function () {
|
||||
return addCurrency;
|
||||
});
|
||||
|
||||
function formatNumber(field, value, scale) {
|
||||
var num = +(value);
|
||||
if ((value === null || value === undefined) && !field.defaultValue) {
|
||||
return value;
|
||||
}
|
||||
if (num === 0 || num) {
|
||||
var lang = axelor.config['user.lang'];
|
||||
var tsep = thousandSeparator[lang] || thousandSeparator.en;
|
||||
return _.numberFormat(num, scale, '.', tsep);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
ui.findNested = findNested;
|
||||
ui.setNested = setNested;
|
||||
ui.canSetNested = canSetNested;
|
||||
|
||||
ui.formatters = {
|
||||
|
||||
"string": function(field, value, context) {
|
||||
if (field.translatable && value && context) {
|
||||
var key = '$t:' + field.name;
|
||||
return context[key] || value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
"integer": function(field, value) {
|
||||
return formatNumber(field, value);
|
||||
},
|
||||
|
||||
"decimal": function(field, value, context) {
|
||||
var scale = (field.widgetAttrs||{}).scale || field.scale || 2;
|
||||
var currency = (field.widgetAttrs||{}).currency || field.currency;
|
||||
|
||||
var text = formatNumber(field, value, scale);
|
||||
if (text && currency) {
|
||||
text = addCurrency(text, findNested(context, currency));
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
"boolean": function(field, value) {
|
||||
return value;
|
||||
},
|
||||
|
||||
"duration": function(field, value) {
|
||||
return ui.formatDuration(field, value);
|
||||
},
|
||||
|
||||
"date": function(field, value) {
|
||||
return value ? moment(value).format('DD/MM/YYYY') : "";
|
||||
},
|
||||
|
||||
"time": function(field, value) {
|
||||
return value ? value : "";
|
||||
},
|
||||
|
||||
"datetime": function(field, value) {
|
||||
return value ? moment(value).format('DD/MM/YYYY HH:mm') : "";
|
||||
},
|
||||
|
||||
"many-to-one": function(field, value) {
|
||||
return value
|
||||
? (field.targetName ? value[field.targetName] : (value.name || value.code || value.id || ""))
|
||||
: "";
|
||||
},
|
||||
|
||||
"one-to-many": function(field, value) {
|
||||
return value ? '(' + value.length + ')' : "";
|
||||
},
|
||||
|
||||
"many-to-many": function(field, value) {
|
||||
return value ? '(' + value.length + ')' : "";
|
||||
},
|
||||
|
||||
"selection": function(field, value) {
|
||||
var cmp = field.type === "integer" ? function(a, b) { return a == b ; } : _.isEqual;
|
||||
var res = _.find(field.selectionList, function(item){
|
||||
return cmp(item.value, value);
|
||||
}) || {};
|
||||
return res.title;
|
||||
}
|
||||
};
|
||||
|
||||
ui.formatters["enum"] = ui.formatters.selection;
|
||||
|
||||
function findField(scope, name) {
|
||||
if (scope.field && scope.field.target) {
|
||||
return ((scope.field.viewer||{}).fields||{})[name]
|
||||
|| ((scope.field.editor||{}).fields||{})[name];
|
||||
}
|
||||
return (scope.viewItems || scope.fields || {})[name];
|
||||
}
|
||||
|
||||
ui.formatters.$image = function (scope, fieldName, imageName) {
|
||||
var record = scope.record || {};
|
||||
var model = scope._model;
|
||||
|
||||
if (fieldName) {
|
||||
var field = (scope.fields||{})[fieldName];
|
||||
if (field && field.target) {
|
||||
record = record[fieldName] || {};
|
||||
model = field.target;
|
||||
}
|
||||
}
|
||||
|
||||
var v = record.version || record.$version || 0;
|
||||
var n = record.id;
|
||||
if (n > 0) {
|
||||
return "ws/rest/" + model + "/" + n + "/" + imageName + "/download?image=true&v=" + v
|
||||
+ "&parentId=" + scope.record.id + "&parentModel=" + scope._model;
|
||||
}
|
||||
return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
};
|
||||
|
||||
ui.formatters.$fmt = function (scope, fieldName, fieldValue, record) {
|
||||
var context = record || scope.record || {};
|
||||
var value = arguments.length === 2 ? context[fieldName] : fieldValue;
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
var field = findField(scope, fieldName);
|
||||
if (!field) {
|
||||
return value;
|
||||
}
|
||||
var type = field.selection ? "selection" : field.type;
|
||||
var formatter = ui.formatters[type];
|
||||
if (formatter) {
|
||||
return formatter(field, value, context);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
})();
|
||||
447
sophal/js/form/form.input.binary.js
Normal file
447
sophal/js/form/form.input.binary.js
Normal file
@ -0,0 +1,447 @@
|
||||
/*
|
||||
* 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 BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
var META_FILE = "com.axelor.meta.db.MetaFile";
|
||||
var META_JSON_RECORD = "com.axelor.meta.db.MetaJsonRecord";
|
||||
|
||||
function makeURL(model, field, recordOrId, version, scope) {
|
||||
var value = recordOrId;
|
||||
if (!value) return null;
|
||||
var id = value.id ? value.id : value;
|
||||
var ver = version;
|
||||
if (ver === undefined || ver === null) ver = value.version;
|
||||
if (ver === undefined || ver === null) ver = value.$version;
|
||||
if (ver === undefined || ver === null) ver = (new Date()).getTime();
|
||||
if (!id || id <= 0) return null;
|
||||
var url = "ws/rest/" + model + "/" + id + "/" + field + "/download?v=" + ver;
|
||||
if (scope && scope.record) {
|
||||
var parentId = scope.record.id;
|
||||
if (!parentId && scope.field && scope._jsonContext && scope._jsonContext.$record) {
|
||||
parentId = scope._jsonContext.$record.id;
|
||||
}
|
||||
url += "&parentId=" + parentId + "&parentModel=" + scope._model;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
ui.makeImageURL = makeURL;
|
||||
|
||||
ui.formInput('ImageLink', {
|
||||
css: 'image-item',
|
||||
cssClass: 'from-item image-item',
|
||||
metaWidget: true,
|
||||
controller: ['$scope', '$element', '$interpolate', function($scope, $element, $interpolate) {
|
||||
|
||||
$scope.parseText = function(text) {
|
||||
if (!text) return BLANK;
|
||||
if (!text.match(/{{.*?}}/)) {
|
||||
return text;
|
||||
}
|
||||
return $interpolate(text)($scope.record);
|
||||
};
|
||||
}],
|
||||
|
||||
init: function(scope) {
|
||||
var field = scope.field;
|
||||
|
||||
var width = field.width || 140;
|
||||
var height = field.height || '100%';
|
||||
|
||||
scope.styles = [{
|
||||
'width': width,
|
||||
'max-width': '100%',
|
||||
'max-height': '100%'
|
||||
}, {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'max-width': '100%',
|
||||
'max-height': '100%'
|
||||
}];
|
||||
|
||||
if (field.noframe) {
|
||||
_.extend(scope.styles[1], {
|
||||
border: 0,
|
||||
padding: 0,
|
||||
background: 'none',
|
||||
boxShadow: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
link_readonly: function(scope, element, attrs, model) {
|
||||
|
||||
var image = element.children('img:first');
|
||||
var update = scope.$render_readonly = function () {
|
||||
if (scope.isReadonly()) {
|
||||
image.get(0).src = scope.parseText(model.$viewValue) || BLANK;
|
||||
}
|
||||
};
|
||||
|
||||
scope.$watch("record.id", update);
|
||||
scope.$watch("record.version", update);
|
||||
scope.$watch("isReadonly()", update);
|
||||
},
|
||||
template_editable: '<input type="text">',
|
||||
template_readonly:
|
||||
'<div ng-style="styles[0]">'+
|
||||
'<img class="img-polaroid" ng-style="styles[1]">'+
|
||||
'</div>'
|
||||
});
|
||||
|
||||
ui.formInput('Image', 'ImageLink', {
|
||||
|
||||
init: function(scope) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var field = scope.field;
|
||||
var isBinary = field.serverType === 'binary';
|
||||
|
||||
if (!isBinary && field.target !== META_FILE) {
|
||||
throw new Error("Invalid field type for Image widget.");
|
||||
}
|
||||
|
||||
scope.parseText = function (value) {
|
||||
return scope.getLink(value);
|
||||
};
|
||||
|
||||
scope.getLink = function (value) {
|
||||
var record = scope.record || {};
|
||||
var model = scope._model;
|
||||
if (value === null) return BLANK;
|
||||
if (isBinary) {
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (record.id) {
|
||||
return makeURL(model, field.name, record, undefined, scope) + "&image=true";
|
||||
}
|
||||
return BLANK;
|
||||
}
|
||||
return value ? makeURL(META_FILE, "content", (value.id || value), value.version || value.$version, scope) : BLANK;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
|
||||
var field = scope.field;
|
||||
var input = element.children('input:first');
|
||||
var image = element.children('img:first');
|
||||
var buttons = element.children('.btn-group');
|
||||
|
||||
var isBinary = field.serverType === 'binary';
|
||||
var timer = null;
|
||||
|
||||
input.add(buttons).hide();
|
||||
element.on('mouseenter', function (e) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
buttons.slideDown();
|
||||
}, 500);
|
||||
});
|
||||
element.on('mouseleave', function (e) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
buttons.slideUp();
|
||||
});
|
||||
|
||||
scope.doSelect = function() {
|
||||
input.click();
|
||||
};
|
||||
|
||||
scope.doSave = function() {
|
||||
var content = image.get(0).src;
|
||||
if (content) {
|
||||
window.open(content);
|
||||
}
|
||||
};
|
||||
|
||||
scope.doRemove = function() {
|
||||
image.get(0).src = "";
|
||||
input.val(null);
|
||||
update(null);
|
||||
};
|
||||
|
||||
input.change(function(e, ui) {
|
||||
var file = input.get(0).files[0];
|
||||
var uploadSize = +(axelor.config["file.upload.size"]) || 0;
|
||||
|
||||
// reset file selection
|
||||
input.get(0).value = null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(uploadSize > 0 && file.size > 1048576 * uploadSize) {
|
||||
return axelor.dialogs.say(_t("You are not allow to upload a file bigger than") + ' ' + uploadSize + 'MB');
|
||||
}
|
||||
|
||||
if (!isBinary) {
|
||||
return doUpload(file);
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
update(e.target.result, file.name);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
function doUpload(file) {
|
||||
var ds = scope._dataSource._new(META_FILE);
|
||||
var value = field.target === META_FILE ? (scope.getValue()||{}) : {};
|
||||
var record = {
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
fileSize: file.size,
|
||||
id: value.id,
|
||||
version: value.version || value.$version
|
||||
};
|
||||
|
||||
record.$upload = {
|
||||
file: file
|
||||
};
|
||||
|
||||
ds.save(record).success(function (saved) {
|
||||
update(saved);
|
||||
});
|
||||
}
|
||||
|
||||
function doUpdate(value) {
|
||||
image.get(0).src = scope.getLink(value);
|
||||
model.$setViewValue(getData(value));
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
scope.$applyAsync(function() {
|
||||
doUpdate(value);
|
||||
});
|
||||
}
|
||||
|
||||
function getData(value) {
|
||||
if (!value || isBinary) {
|
||||
return value;
|
||||
}
|
||||
return {
|
||||
id: value.id
|
||||
};
|
||||
}
|
||||
|
||||
var updateLink = scope.$render_editable = function() {
|
||||
image.get(0).src = scope.getLink(model.$viewValue);
|
||||
};
|
||||
|
||||
scope.$watch("record.id", updateLink);
|
||||
scope.$watch("record.version", updateLink);
|
||||
|
||||
if (field.accept) {
|
||||
input.attr('accept', field.accept);
|
||||
}
|
||||
},
|
||||
template_editable:
|
||||
'<div ng-style="styles[0]" class="image-wrapper">' +
|
||||
'<input type="file" accept="image/*">' +
|
||||
'<img class="img-polaroid" ng-style="styles[1]" style="display: inline-block;">' +
|
||||
'<div class="btn-group">' +
|
||||
'<button ng-click="doSelect()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
|
||||
'<button ng-click="doRemove()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
ui.formInput('Binary', {
|
||||
|
||||
css: 'file-item',
|
||||
cellCss: 'form-item file-item',
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
var field = scope.field;
|
||||
var input = element.children('input:first').hide();
|
||||
|
||||
scope.doSelect = function() {
|
||||
input.click();
|
||||
};
|
||||
|
||||
scope.doSave = function() {
|
||||
var record = scope.record;
|
||||
var model = scope._model;
|
||||
var url = makeURL(model, field.name, record, undefined, scope);
|
||||
ui.download(url, record.fileName || field.name);
|
||||
};
|
||||
|
||||
scope.doRemove = function() {
|
||||
var record = scope.record;
|
||||
input.val(null);
|
||||
model.$setViewValue(null);
|
||||
record.$upload = null;
|
||||
if(scope._model === META_FILE) {
|
||||
record.fileName = null;
|
||||
record.fileType = null;
|
||||
}
|
||||
record.fileSize = null;
|
||||
};
|
||||
|
||||
scope.canDownload = function() {
|
||||
var record = scope.record || {};
|
||||
if (!record.id) return false;
|
||||
if (scope._model === META_FILE) {
|
||||
return !!record.fileName;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
input.change(function(e) {
|
||||
var file = input.get(0).files[0];
|
||||
var record = scope.record;
|
||||
|
||||
// reset file selection
|
||||
input.get(0).value = null;
|
||||
|
||||
if (file) {
|
||||
record.$upload = {
|
||||
field: field.name,
|
||||
file: file
|
||||
};
|
||||
if(scope._model === META_FILE) {
|
||||
record.fileName = file.name;
|
||||
}
|
||||
scope.$applyAsync(function() {
|
||||
record.fileType = file.type;
|
||||
record.fileSize = file.size;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (field.accept) {
|
||||
input.attr('accept', field.accept);
|
||||
}
|
||||
},
|
||||
template_readonly: null,
|
||||
template_editable: null,
|
||||
template:
|
||||
'<div>' +
|
||||
'<input type="file">' +
|
||||
'<div class="btn-group">' +
|
||||
'<button ng-click="doSelect()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
|
||||
'<button ng-click="doSave()" ng-show="canDownload()" class="btn" type="button"><i class="fa fa-arrow-circle-down"></i></button>' +
|
||||
'<button ng-click="doRemove()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
ui.formInput('BinaryLink', {
|
||||
|
||||
css: 'file-item',
|
||||
cellCss: 'form-item file-item',
|
||||
metaWidget: true,
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
var field = scope.field;
|
||||
var input = element.children('input:first').hide();
|
||||
|
||||
if (field.target !== META_FILE) {
|
||||
throw new Error("BinaryLink widget can be used with MetaFile field only.");
|
||||
}
|
||||
|
||||
scope.doSelect = function() {
|
||||
input.click();
|
||||
};
|
||||
|
||||
scope.doRemove = function() {
|
||||
input.val(null);
|
||||
scope.setValue(null, true);
|
||||
};
|
||||
|
||||
scope.canDownload = function() {
|
||||
var value = model.$viewValue;
|
||||
return value && value.id > 0;
|
||||
};
|
||||
|
||||
scope.format = function (value) {
|
||||
if (value) {
|
||||
return value.fileName;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.doSave = function() {
|
||||
var value = model.$viewValue;
|
||||
var version = value ? (value.version || value.$version) : undefined;
|
||||
var url = makeURL(META_FILE, "content", value, version, scope);
|
||||
ui.download(url, scope.text);
|
||||
};
|
||||
|
||||
input.change(function(e) {
|
||||
var file = input.get(0).files[0];
|
||||
|
||||
// reset file selection
|
||||
input.get(0).value = null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ds = scope._dataSource._new(META_FILE);
|
||||
var value = scope.getValue() || {};
|
||||
var record = _.extend({
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
fileSize: file.size
|
||||
}, {
|
||||
id: value.id,
|
||||
version: value.version || value.$version
|
||||
});
|
||||
|
||||
record.$upload = {
|
||||
file: file
|
||||
};
|
||||
|
||||
ds.save(record).success(function (rec) {
|
||||
scope.setValue(rec, true);
|
||||
});
|
||||
});
|
||||
|
||||
if (field.accept) {
|
||||
input.attr('accept', field.accept);
|
||||
}
|
||||
},
|
||||
template_readonly: null,
|
||||
template_editable: null,
|
||||
template:
|
||||
'<div>' +
|
||||
'<input type="file">' +
|
||||
'<div class="btn-group">' +
|
||||
'<button ng-click="doSelect()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
|
||||
'<button ng-click="doRemove()" ng-show="canDownload() && !isReadonly()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
|
||||
'</div> ' +
|
||||
'<a ng-show="text" href="javascript:" ng-click="doSave()">{{text}}</a>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
})();
|
||||
228
sophal/js/form/form.input.boolean.js
Normal file
228
sophal/js/form/form.input.boolean.js
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
/**
|
||||
* The Boolean input widget.
|
||||
*/
|
||||
ui.formInput('Boolean', {
|
||||
|
||||
css: 'boolean-item',
|
||||
|
||||
cellCss: 'form-item boolean-item',
|
||||
|
||||
link: function (scope, element, attrs, model) {
|
||||
|
||||
element.on('click', 'input:not(.no-toggle)', function (e) {
|
||||
var value = $(e.target).data('value');
|
||||
var checked = value === undefined ? e.target.checked : value;
|
||||
scope.setValue(checked, true);
|
||||
});
|
||||
|
||||
Object.defineProperty(scope, '$value', {
|
||||
get: function () {
|
||||
return model.$viewValue;
|
||||
},
|
||||
set: function(value) {
|
||||
model.$setViewValue(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
template_readonly: null,
|
||||
template_editable:
|
||||
"<label class='ibox'>" +
|
||||
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
|
||||
"<span class='box'></span>" +
|
||||
"</label>"
|
||||
});
|
||||
|
||||
/**
|
||||
* The Boolean widget with label on right.
|
||||
*/
|
||||
ui.formInput('InlineCheckbox', 'Boolean', {
|
||||
css: 'checkbox-inline',
|
||||
metaWidget: true,
|
||||
showTitle: false,
|
||||
link: function (scope, element, attrs, model) {
|
||||
this._super.apply(this, arguments);
|
||||
scope.$watch('attr("title")', function booleanTitleWatch(title) {
|
||||
scope.label = title;
|
||||
});
|
||||
},
|
||||
template_readonly: null,
|
||||
template_editable:
|
||||
"<label class='ibox'>" +
|
||||
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
|
||||
"<div class='box'></div>" +
|
||||
"<span class='title' ui-help-popover>{{label}}</span>" +
|
||||
"</label>"
|
||||
});
|
||||
|
||||
ui.formInput('Toggle', 'Boolean', {
|
||||
cellCss: 'form-item toggle-item',
|
||||
metaWidget: true,
|
||||
link: function (scope, element, attrs, model) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var field = scope.field;
|
||||
var icon = element.find('i');
|
||||
|
||||
scope.icon = function () {
|
||||
return model.$viewValue && field.iconActive ? field.iconActive : field.icon;
|
||||
};
|
||||
|
||||
scope.toggle = function () {
|
||||
var value = !model.$viewValue;
|
||||
if (scope.setExclusive && field.exclusive) {
|
||||
scope.setExclusive(field.name, scope.record);
|
||||
}
|
||||
scope.setValue(value, true);
|
||||
};
|
||||
|
||||
if (field.help || field.title) {
|
||||
element.attr('title', field.help || field.title);
|
||||
}
|
||||
},
|
||||
template_readonly: null,
|
||||
template_editable:
|
||||
"<button tabindex='-1' class='btn btn-default' ng-class='{active: $value}' ng-click='toggle()'>" +
|
||||
"<i class='fa {{icon()}}'></i>" +
|
||||
"</button>"
|
||||
});
|
||||
|
||||
ui.formInput('BooleanSelect', 'Boolean', {
|
||||
css: 'form-item boolean-select-item',
|
||||
metaWidget: true,
|
||||
init: function (scope) {
|
||||
var field = scope.field;
|
||||
var trueText = _t((field.widgetAttrs||{}).trueText) || _t('Yes');
|
||||
var falseText = _t((field.widgetAttrs||{}).falseText) || _t('No');
|
||||
|
||||
scope.$items = [trueText, falseText];
|
||||
scope.$selection = [{ value: trueText, val: true}, { value: falseText, val: false }];
|
||||
if (field.nullable) {
|
||||
scope.$selection.unshift({ value: '', val: null });
|
||||
}
|
||||
scope.format = function (value) {
|
||||
if (field.nullable && (value === null || value === undefined)) {
|
||||
return "";
|
||||
}
|
||||
return value ? scope.$items[0] : scope.$items[1];
|
||||
};
|
||||
},
|
||||
link_editable: function (scope, element, attrs, model) {
|
||||
var input = element.find('input');
|
||||
var items = scope.$items;
|
||||
|
||||
input.autocomplete({
|
||||
minLength: 0,
|
||||
source: scope.$selection,
|
||||
select: function (e, u) {
|
||||
scope.setValue(u.item.val, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
}).click(function (e) {
|
||||
input.autocomplete("search" , '');
|
||||
});
|
||||
|
||||
scope.doShowSelect = function () {
|
||||
input.autocomplete("search" , '');
|
||||
};
|
||||
|
||||
scope.$render_editable = function () {
|
||||
var value = model.$viewValue;
|
||||
var text = scope.format(value);
|
||||
input.val(text);
|
||||
};
|
||||
|
||||
scope.$watch('isReadonly()', function booleanReadonlyWatch(readonly) {
|
||||
input.autocomplete(readonly ? "disable" : "enable");
|
||||
input.toggleClass('not-readonly', !readonly);
|
||||
});
|
||||
},
|
||||
template: "<span class='form-item-container'></span>",
|
||||
template_readonly: '<span>{{text}}</span>',
|
||||
template_editable: "<span class='picker-input'>" +
|
||||
"<input type='text' readonly='readonly' class='no-toggle'>" +
|
||||
"<span class='picker-icons picker-icons-1'>" +
|
||||
"<i class='fa fa-caret-down' ng-click='doShowSelect()'></i>" +
|
||||
"</span>" +
|
||||
"</span>"
|
||||
});
|
||||
|
||||
ui.formInput('BooleanRadio', 'BooleanSelect', {
|
||||
css: 'form-item boolean-radio-item',
|
||||
metaWidget: true,
|
||||
link_editable: function (scope, element, attrs, model) {
|
||||
var field = scope.field;
|
||||
|
||||
if (field.direction === "vertical") {
|
||||
element.addClass("boolean-radio-vertical");
|
||||
}
|
||||
|
||||
var inputName = _.uniqueId('boolean-radio');
|
||||
var trueInput = $('<input type="radio" data-value="true" name="' + inputName + '">');
|
||||
var falseInput = $('<input type="radio" data-value="false" name="' + inputName + '">');
|
||||
|
||||
var items = scope.$items;
|
||||
|
||||
$('<li>').append(
|
||||
$('<label class="ibox round">')
|
||||
.append(trueInput)
|
||||
.append($('<i class="box">'))
|
||||
.append($('<span class="title">').text(items[0]))
|
||||
).appendTo(element);
|
||||
|
||||
$('<li>').append(
|
||||
$('<label class="ibox round">')
|
||||
.append(falseInput)
|
||||
.append($('<i class="box">'))
|
||||
.append($('<span class="title">').text(items[1]))
|
||||
).appendTo(element);
|
||||
|
||||
scope.$render_editable = function () {
|
||||
var value = model.$viewValue || false;
|
||||
var input = value ? trueInput : falseInput;
|
||||
input.prop('checked', true);
|
||||
};
|
||||
|
||||
element.on('change', 'input', function (e) {
|
||||
var value = $(this).data('value') === true;
|
||||
scope.setValue(value, true);
|
||||
scope.$applyAsync();
|
||||
});
|
||||
},
|
||||
template_editable: "<ul class='boolean-radio'></ul>"
|
||||
});
|
||||
|
||||
ui.formInput('BooleanSwitch', 'Boolean', {
|
||||
css: 'form-item',
|
||||
metaWidget: true,
|
||||
template_readonly: null,
|
||||
template_editable:
|
||||
"<label class='iswitch'>" +
|
||||
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
|
||||
"<span class='box'></span>" +
|
||||
"</label>"
|
||||
});
|
||||
|
||||
})();
|
||||
635
sophal/js/form/form.input.datetime.js
Normal file
635
sophal/js/form/form.input.datetime.js
Normal file
@ -0,0 +1,635 @@
|
||||
/*
|
||||
* 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 regional = {
|
||||
monthNames: [
|
||||
_t('January'),
|
||||
_t('February'),
|
||||
_t('March'),
|
||||
_t('April'),
|
||||
_t('May'),
|
||||
_t('June'),
|
||||
_t('July'),
|
||||
_t('August'),
|
||||
_t('September'),
|
||||
_t('October'),
|
||||
_t('November'),
|
||||
_t('December')],
|
||||
|
||||
monthNamesShort: [
|
||||
_t('Jan'),
|
||||
_t('Feb'),
|
||||
_t('Mar'),
|
||||
_t('Apr'),
|
||||
_t('May'),
|
||||
_t('Jun'),
|
||||
_t('Jul'),
|
||||
_t('Aug'),
|
||||
_t('Sep'),
|
||||
_t('Oct'),
|
||||
_t('Nov'),
|
||||
_t('Dec')],
|
||||
|
||||
dayNames: [
|
||||
_t('Sunday'),
|
||||
_t('Monday'),
|
||||
_t('Tuesday'),
|
||||
_t('Wednesday'),
|
||||
_t('Thursday'),
|
||||
_t('Friday'),
|
||||
_t('Saturday')],
|
||||
|
||||
dayNamesShort : [_t('Sun'), _t('Mon'), _t('Tue'), _t('Wed'), _t('Thu'), _t('Fri'), _t('Sat')],
|
||||
dayNamesMin : [_t('Su'), _t('Mo'), _t('Tu'), _t('We'), _t('Th'), _t('Fr'), _t('Sa')],
|
||||
weekHeader : _t('Wk'),
|
||||
hourText : _t('Hour'),
|
||||
minuteText : _t('Minute'),
|
||||
secondText : _t('Second'),
|
||||
currentText : _t('Now'),
|
||||
closeText : _t('Done'),
|
||||
prevText : _t('Prev'),
|
||||
nextText : _t('Next'),
|
||||
firstDay: 1
|
||||
};
|
||||
|
||||
// configure datepicker
|
||||
$.timepicker.setDefaults(regional);
|
||||
$.datepicker.setDefaults(regional);
|
||||
|
||||
// configure moment.js
|
||||
moment.locale('en', {
|
||||
months: regional.monthNames,
|
||||
monthsShort: regional.monthNamesShort,
|
||||
weekdays: regional.dayNames,
|
||||
weekdaysShort: regional.dayNamesShort,
|
||||
weekdaysMin: regional.dayNamesMin,
|
||||
calendar : {
|
||||
sameDay : _t('[Today at] LT'),
|
||||
nextDay : _t('[Tomorrow at] LT'),
|
||||
nextWeek : _t('dddd [at] LT'),
|
||||
lastDay : _t('[Yesterday at] LT'),
|
||||
lastWeek : _t('[Last] dddd [at] LT'),
|
||||
sameElse : _t('L')
|
||||
},
|
||||
relativeTime : {
|
||||
future : _t("in %s"),
|
||||
past : _t("%s ago"),
|
||||
s : _t("a few seconds"),
|
||||
m : _t("a minute"),
|
||||
mm : _t("%d minutes"),
|
||||
h : _t("an hour"),
|
||||
hh : _t("%d hours"),
|
||||
d : _t("a day"),
|
||||
dd : _t("%d days"),
|
||||
M : _t("a month"),
|
||||
MM : _t("%d months"),
|
||||
y : _t("a year"),
|
||||
yy : _t("%d years")
|
||||
}
|
||||
});
|
||||
|
||||
// configure ui.mask
|
||||
function createTwoDigitDefinition( maximum ) {
|
||||
return function( value ) {
|
||||
var number = parseInt( value, 10 );
|
||||
|
||||
if (value === "" || /\D/.test(value) || _.isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// pad to 2 characters
|
||||
number = ( number < 10 ? "0" : "" ) + number;
|
||||
if ( number <= maximum ) {
|
||||
return number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function yearsDefinition( value ) {
|
||||
var temp;
|
||||
|
||||
// if the value is empty, or contains a non-digit, it is invalid
|
||||
if ( value === "" || /\D/.test( value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
temp = parseInt( value, 10 );
|
||||
|
||||
// convert 2 digit years to 4 digits, this allows us to type 80 <right>
|
||||
if ( value.length <= 2 ) {
|
||||
// before "32" we assume 2000's otherwise 1900's
|
||||
if ( temp < 10 ) {
|
||||
return "200" + temp;
|
||||
} else if ( temp < 32 ) {
|
||||
return "20" + temp;
|
||||
} else {
|
||||
return "19" + temp;
|
||||
}
|
||||
}
|
||||
if ( temp === 0 ) {
|
||||
return "2000";
|
||||
}
|
||||
if ( value.length === 3 ) {
|
||||
return "0"+value;
|
||||
}
|
||||
if ( value.length === 4 ) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
$.extend($.ui.mask.prototype.options.definitions, {
|
||||
"MM": createTwoDigitDefinition( 12 ),
|
||||
"DD": createTwoDigitDefinition( 31 ),
|
||||
"YYYY": yearsDefinition,
|
||||
"HH": createTwoDigitDefinition( 23 ),
|
||||
"mm": createTwoDigitDefinition( 59 )
|
||||
});
|
||||
|
||||
// datepicker keyboad navigation hack
|
||||
var _doKeyDown = $.datepicker._doKeyDown;
|
||||
$.extend($.datepicker, {
|
||||
_doKeyDown: function(event) {
|
||||
var inst = $.datepicker._getInst(event.target),
|
||||
handled = false;
|
||||
inst._keyEvent = true;
|
||||
if ($.datepicker._datepickerShowing) {
|
||||
switch (event.keyCode) {
|
||||
case 36: // home
|
||||
$.datepicker._gotoToday(event.target);
|
||||
handled = true;
|
||||
break;
|
||||
case 37: // left
|
||||
$.datepicker._adjustDate(event.target, -1, "D");
|
||||
handled = true;
|
||||
break;
|
||||
case 38: // up
|
||||
$.datepicker._adjustDate(event.target, -7, "D");
|
||||
handled = true;
|
||||
break;
|
||||
case 39: // right
|
||||
$.datepicker._adjustDate(event.target, +1, "D");
|
||||
handled = true;
|
||||
break;
|
||||
case 40: // down
|
||||
$.datepicker._adjustDate(event.target, +7, "D");
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
if (handled) {
|
||||
event.ctrlKey = true;
|
||||
}
|
||||
} else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home
|
||||
$.datepicker._showDatepicker(this);
|
||||
handled = true;
|
||||
}
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
_doKeyDown(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _updateDatepicker = $.datepicker._updateDatepicker;
|
||||
$.datepicker._updateDatepicker = function(inst) {
|
||||
if (!$.datepicker._noUpdate) {
|
||||
return _updateDatepicker.call($.datepicker, inst);
|
||||
}
|
||||
};
|
||||
|
||||
var _parseDate = $.datepicker.parseDate;
|
||||
$.datepicker.parseDate = function (format, value, settings) {
|
||||
if (_.isString(value) && value.indexOf('_') > -1) {
|
||||
return;
|
||||
}
|
||||
return _parseDate.call($.datepicker, format, value, settings);
|
||||
};
|
||||
|
||||
// suppress annoying logs
|
||||
$.timepicker.log = function () {};
|
||||
|
||||
/**
|
||||
* The DateTime input widget.
|
||||
*/
|
||||
ui.formInput('DateTime', {
|
||||
|
||||
css : 'datetime-item',
|
||||
|
||||
format: 'DD/MM/YYYY HH:mm',
|
||||
mask: 'DD/MM/YYYY HH:mm',
|
||||
|
||||
widgets: ['Datetime'],
|
||||
|
||||
init: function(scope) {
|
||||
|
||||
var isDate = this.isDate,
|
||||
format = this.format;
|
||||
|
||||
scope.parse = function(value) {
|
||||
if (angular.isDate(value)) {
|
||||
return isDate ? moment(value).startOf('day').format('YYYY-MM-DD') : value.toISOString();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.format = function(value) {
|
||||
if (value) {
|
||||
return moment(value).format(format);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
var input = element.children('input:first');
|
||||
var button = element.find('i:first');
|
||||
var onChange = scope.$events.onChange;
|
||||
var props = scope.field;
|
||||
var isDate = this.isDate;
|
||||
var isShowing = false;
|
||||
var lastValue = null;
|
||||
|
||||
var options = {
|
||||
dateFormat: 'dd/mm/yy',
|
||||
showButtonsPanel: false,
|
||||
showTime: false,
|
||||
showOn: null,
|
||||
beforeShow: function (e, ui) {
|
||||
lastValue = input.mask("value") || '';
|
||||
isShowing = true;
|
||||
},
|
||||
onClose: function (e, ui) {
|
||||
lastValue = null;
|
||||
isShowing = false;
|
||||
},
|
||||
onSelect: function(dateText, inst) {
|
||||
input.mask('value', dateText);
|
||||
updateModel();
|
||||
if (!inst.timeDefined) {
|
||||
input.datetimepicker('hide');
|
||||
setTimeout(function(){
|
||||
input.focus().select();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.isDate) {
|
||||
options.showTimepicker = false;
|
||||
}
|
||||
|
||||
input.datetimepicker(options);
|
||||
input.mask({
|
||||
mask: this.mask
|
||||
});
|
||||
|
||||
var changed = false;
|
||||
var rendering = false;
|
||||
|
||||
input.on('change', function(e, ui){
|
||||
if (changed) return;
|
||||
if (isShowing) {
|
||||
changed = lastValue !== (input.mask("value") || '');
|
||||
} else {
|
||||
changed = !rendering;
|
||||
}
|
||||
});
|
||||
input.on('blur', function() {
|
||||
if (changed) {
|
||||
changed = false;
|
||||
updateModel();
|
||||
}
|
||||
});
|
||||
input.on('grid:check', function () {
|
||||
updateModel();
|
||||
});
|
||||
input.on('keydown', function(e){
|
||||
|
||||
if (e.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === $.ui.keyCode.DOWN) {
|
||||
input.datetimepicker('show');
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (e.keyCode === $.ui.keyCode.ENTER && $(this).datepicker("widget").is(':visible')) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (e.keyCode === $.ui.keyCode.ENTER || e.keyCode === $.ui.keyCode.TAB) {
|
||||
if (changed) updateModel();
|
||||
}
|
||||
});
|
||||
button.click(function(e, ui){
|
||||
if (scope.isReadonly()) {
|
||||
return;
|
||||
}
|
||||
var mobile = axelor.device.mobile;
|
||||
if (mobile) {
|
||||
input.attr('disabled', 'disabled')
|
||||
.addClass('mobile-disabled-input');
|
||||
}
|
||||
input.datetimepicker('show');
|
||||
if (mobile) {
|
||||
input.removeAttr('disabled')
|
||||
.removeClass('mobile-disabled-input');
|
||||
}
|
||||
});
|
||||
|
||||
scope.$onAdjust('size scroll', function () {
|
||||
if (isShowing) {
|
||||
input.datepicker('widget').hide();
|
||||
input.datetimepicker('hide');
|
||||
}
|
||||
});
|
||||
|
||||
function updateModel() {
|
||||
var masked = input.mask("value") || '',
|
||||
value = input.datetimepicker('getDate') || null,
|
||||
oldValue = scope.getValue() || null;
|
||||
|
||||
if (_.isEmpty(masked)) {
|
||||
value = null;
|
||||
}
|
||||
if (!input.mask("valid")) {
|
||||
model.$setViewValue(value); // force validation
|
||||
model.$render();
|
||||
scope.$applyAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
value = scope.parse(value);
|
||||
|
||||
if (angular.equals(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.setValue(value, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
scope.validate = function(value) {
|
||||
var minSize = props.minSize === 'now' ? moment() : props.minSize,
|
||||
maxSize = props.maxSize,
|
||||
val = moment(value),
|
||||
valid = true;
|
||||
|
||||
var masked = input.mask('value');
|
||||
if (masked && !input.mask("valid")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(isDate) {
|
||||
if(minSize) minSize = moment(minSize).startOf('day');
|
||||
if(maxSize) maxSize = moment(maxSize).startOf('day');
|
||||
}
|
||||
else {
|
||||
if(minSize) minSize = moment(minSize);
|
||||
if(maxSize) maxSize = moment(maxSize);
|
||||
}
|
||||
|
||||
if(minSize) {
|
||||
if(!val) return false;
|
||||
valid = !val.isBefore(minSize) ;
|
||||
}
|
||||
if(valid && maxSize) {
|
||||
if(!val) return true;
|
||||
valid = !val.isAfter(maxSize) ;
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
scope.$render_editable = function() {
|
||||
rendering = true;
|
||||
try {
|
||||
var value = scope.getText();
|
||||
if (value) {
|
||||
input.mask('value', value);
|
||||
try {
|
||||
$.datepicker._noUpdate = true;
|
||||
$.datepicker._setDateDatepicker(input[0], moment(scope.getValue()).toDate());
|
||||
input.mask('refresh');
|
||||
} finally {
|
||||
$.datepicker._noUpdate = false;
|
||||
}
|
||||
} else {
|
||||
input.mask('value', '');
|
||||
}
|
||||
} finally {
|
||||
rendering = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
template_editable:
|
||||
'<span class="picker-input">'+
|
||||
'<input type="text" autocomplete="off">'+
|
||||
'<span class="picker-icons">'+
|
||||
'<i class="fa fa-calendar"></i>'+
|
||||
'</span>'+
|
||||
'</span>'
|
||||
});
|
||||
|
||||
ui.formInput('Date', 'DateTime', {
|
||||
format: 'DD/MM/YYYY',
|
||||
mask: 'DD/MM/YYYY',
|
||||
isDate: true
|
||||
});
|
||||
|
||||
ui.formInput('Time', 'DateTime', {
|
||||
|
||||
css: 'time-item',
|
||||
mask: 'HH:mm',
|
||||
|
||||
init: function(scope) {
|
||||
this._super(scope);
|
||||
|
||||
scope.parse = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.format = function(value) {
|
||||
return value;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
|
||||
element.mask({
|
||||
mask: this.mask
|
||||
});
|
||||
|
||||
element.change(function(e, ui) {
|
||||
updateModel();
|
||||
});
|
||||
|
||||
element.on('keydown', function(e){
|
||||
if (e.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === $.ui.keyCode.ENTER || e.keyCode === $.ui.keyCode.TAB) {
|
||||
updateModel();
|
||||
}
|
||||
});
|
||||
|
||||
element.on('grid:check', function () {
|
||||
updateModel();
|
||||
});
|
||||
|
||||
scope.validate = function(value) {
|
||||
return !value || /^(\d+:\d+)$/.test(value);
|
||||
};
|
||||
|
||||
function updateModel() {
|
||||
var masked = element.mask("value") || '',
|
||||
value = element.val() || '',
|
||||
oldValue = scope.getValue() || '';
|
||||
|
||||
if (value && !element.mask("valid")) {
|
||||
return model.$setViewValue(value); // force validation
|
||||
}
|
||||
if (_.isEmpty(masked)) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
value = scope.parse(value);
|
||||
|
||||
if (angular.equals(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.setValue(value, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
scope.$render_editable = function() {
|
||||
var value = scope.getText();
|
||||
if (value) {
|
||||
element.mask('value', value);
|
||||
} else {
|
||||
element.mask('value', '');
|
||||
}
|
||||
};
|
||||
},
|
||||
template_editable: '<input type="text">'
|
||||
});
|
||||
|
||||
ui.formInput('RelativeTime', 'DateTime', {
|
||||
metaWidget: true,
|
||||
init: function(scope) {
|
||||
this._super(scope);
|
||||
|
||||
scope.isReadonly = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
scope.format = function(value) {
|
||||
if (value) {
|
||||
return moment(value).fromNow();
|
||||
}
|
||||
return "";
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function formatDuration(field, value) {
|
||||
if (!value || !_.isNumber(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var h = Math.floor(value / 3600);
|
||||
var m = Math.floor((value % 3600) / 60);
|
||||
var s = Math.floor((value % 3600) % 60);
|
||||
|
||||
h = _.str.pad(h, field.big ? 3 : 2, '0');
|
||||
m = _.str.pad(m, 2, '0');
|
||||
s = _.str.pad(s, 2, '0');
|
||||
|
||||
var text = h + ':' + m;
|
||||
|
||||
if (field.seconds) {
|
||||
text = text + ':' + s;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
ui.formatDuration = formatDuration;
|
||||
|
||||
ui.formInput('Duration', 'Time', {
|
||||
metaWidget: true,
|
||||
mask: '99:mm',
|
||||
|
||||
init: function(scope) {
|
||||
this._super(scope);
|
||||
|
||||
var field = scope.field;
|
||||
var pattern = /^\d+:\d+(:\d+)?$/;
|
||||
|
||||
scope.format = function(value) {
|
||||
return formatDuration(field, value);
|
||||
};
|
||||
|
||||
scope.parse = function(value) {
|
||||
if (!value || !_.isString(value)) {
|
||||
return value;
|
||||
}
|
||||
if (!pattern.test(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = value.split(':'),
|
||||
first = +(parts[0]),
|
||||
next = +(parts[1]),
|
||||
last = +(parts[2] || 0);
|
||||
|
||||
return (first * 60 * 60) + (next * 60) + last;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
var field = scope.field || {},
|
||||
mask = this.mask;
|
||||
|
||||
if (field.big) {
|
||||
mask = "999:mm";
|
||||
}
|
||||
if (field.seconds) {
|
||||
mask = mask + ":mm";
|
||||
}
|
||||
|
||||
this.mask = mask;
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
scope.validate = function(value) {
|
||||
return !value || _.isNumber(value);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
499
sophal/js/form/form.input.html.js
Normal file
499
sophal/js/form/form.input.html.js
Normal file
@ -0,0 +1,499 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
function getStylePopup(element, styles) {
|
||||
|
||||
return function($popup, $button) {
|
||||
|
||||
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
|
||||
|
||||
$.each(styles, function(format, name) {
|
||||
var $link = $('<a/>').attr('href', '#').html(name).click(
|
||||
function(event) {
|
||||
$(element).wysiwyg('shell').format(format).closePopup();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$list.append($link);
|
||||
});
|
||||
|
||||
$popup.append($list);
|
||||
};
|
||||
}
|
||||
|
||||
function getFontNamePopup(element, fonts) {
|
||||
|
||||
return function($popup, $button) {
|
||||
|
||||
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
|
||||
|
||||
$.each(fonts, function(font, name) {
|
||||
var $link = $('<a/>').attr('href', '#').html(name).click(
|
||||
function(event) {
|
||||
try {
|
||||
document.execCommand('styleWithCSS', false, false);
|
||||
$(element).wysiwyg('shell').fontName(font).closePopup();
|
||||
} finally {
|
||||
document.execCommand('styleWithCSS', false, true);
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$list.append($link);
|
||||
});
|
||||
|
||||
$popup.append($list);
|
||||
};
|
||||
}
|
||||
|
||||
function getFontSizePopup(element, sizes) {
|
||||
|
||||
return function($popup, $button) {
|
||||
|
||||
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
|
||||
|
||||
$.each(sizes, function(size, name) {
|
||||
var $link = $('<a/>').attr('href', '#').html(name).click(
|
||||
function(event) {
|
||||
$(element).focus();
|
||||
try {
|
||||
document.execCommand('styleWithCSS', false, false);
|
||||
$(element).wysiwyg('shell').fontSize(size).closePopup();
|
||||
} finally {
|
||||
document.execCommand('styleWithCSS', false, true);
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$list.append($link);
|
||||
});
|
||||
|
||||
$popup.append($list);
|
||||
};
|
||||
}
|
||||
|
||||
function getButtons(scope, element) {
|
||||
|
||||
var lite = scope.field.lite;
|
||||
|
||||
return {
|
||||
style: lite ? false : {
|
||||
title: _t('Style'),
|
||||
image: '\uf1dd',
|
||||
popup: getStylePopup(element, {
|
||||
'<p>' : _t('Normal'),
|
||||
'<pre>' : '<pre>' + _t('Formatted') + '</pre>',
|
||||
'<blockquote>' : '<blockquote>' + _t('Blockquote') + '</blockquote>',
|
||||
'<h1>': '<h1>' + _t('Header 1') + '</h1>',
|
||||
'<h2>': '<h2>' + _t('Header 2') + '</h2>',
|
||||
'<h3>': '<h3>' + _t('Header 3') + '</h3>',
|
||||
'<h4>': '<h4>' + _t('Header 4') + '</h4>',
|
||||
'<h5>': '<h5>' + _t('Header 5') + '</h5>',
|
||||
'<h6>': '<h6>' + _t('Header 6') + '</h6>'
|
||||
})
|
||||
},
|
||||
fontName: lite ? false : {
|
||||
title: _t('Font'),
|
||||
image: '\uf031',
|
||||
popup: getFontNamePopup(element, {
|
||||
'"Times New Roman", Times, serif': '<span style="font-family: \"Times New Roman\", Times, serif">Times New Roman</span>',
|
||||
'Arial, Helvetica, sans-serif': '<span style="font-family: Arial, Helvetica, sans-serif">Arial</span>',
|
||||
'"Courier New", Courier, monospace': '<span style="font-family: \"Courier New\", Courier, monospace">Courier New</span>',
|
||||
'Comic Sans, Comic Sans MS, cursive': '<span style="font-family: Comic Sans, Comic Sans MS, cursive">Comic Sans</span>',
|
||||
'Impact, fantasy': '<span style="font-family: Impact, fantasy">Impact</span>'
|
||||
})
|
||||
},
|
||||
fontSize: lite ? false : {
|
||||
title: _t('Font size'),
|
||||
image: '\uf035',
|
||||
popup: getFontSizePopup(element, {
|
||||
'1': '<span style="font-size: x-small">' + _t('Smaller') + '</span>',
|
||||
'2': '<span style="font-size: small">' + _t('Small') + '</span>',
|
||||
'3': '<span style="font-size: medium">' + _t('Medium') + '</span>',
|
||||
'4': '<span style="font-size: large">' + _t('Large') + '</span>',
|
||||
'5': '<span style="font-size: x-large">' + _t('Larger') + '</span>'
|
||||
})
|
||||
},
|
||||
d1: lite ? false : {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
bold: {
|
||||
title: _t('Bold (Ctrl+B)'),
|
||||
image: '\uf032'
|
||||
},
|
||||
italic: {
|
||||
title: _t('Italic (Ctrl+I)'),
|
||||
image: '\uf033'
|
||||
},
|
||||
underline: {
|
||||
title: _t('Underline (Ctrl+U)'),
|
||||
image: '\uf0cd'
|
||||
},
|
||||
strikethrough: {
|
||||
title: _t('Strikethrough (Ctrl+S)'),
|
||||
image: '\uf0cc'
|
||||
},
|
||||
removeformat: {
|
||||
title: _t('Remove format'),
|
||||
image: '\uf12d'
|
||||
},
|
||||
d2: {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
forecolor: lite ? false : {
|
||||
title: _t('Text color'),
|
||||
image: '\uf1fc'
|
||||
},
|
||||
highlight: lite ? false : {
|
||||
title: _t('Background color'),
|
||||
image: '\uf043'
|
||||
},
|
||||
d3: lite ? false : {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
alignleft: {
|
||||
title: _t('Left'),
|
||||
image: '\uf036'
|
||||
},
|
||||
aligncenter: {
|
||||
title: _t('Center'),
|
||||
image: '\uf037'
|
||||
},
|
||||
alignright: {
|
||||
title: _t('Right'),
|
||||
image: '\uf038'
|
||||
},
|
||||
alignjustify: {
|
||||
title: _t('Justify'),
|
||||
image: '\uf039'
|
||||
},
|
||||
d4: {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
orderedList: {
|
||||
title: _t('Ordered list'),
|
||||
image: '\uf0cb'
|
||||
},
|
||||
unorderedList: {
|
||||
title: _t('Unordered list'),
|
||||
image: '\uf0ca'
|
||||
},
|
||||
d6: lite ? false : {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
indent: lite ? false : {
|
||||
title: _t('Indent'),
|
||||
image: '\uf03c'
|
||||
},
|
||||
outdent: lite ? false : {
|
||||
title: _t('Outdent'),
|
||||
image: '\uf03b'
|
||||
},
|
||||
d7: lite ? false : {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
insertimage: lite ? false : {
|
||||
title: _t('Insert image'),
|
||||
image: '\uf030'
|
||||
},
|
||||
insertlink: lite ? false : {
|
||||
title: _t('Insert link'),
|
||||
image: '\uf08e'
|
||||
},
|
||||
d8: lite ? false : {
|
||||
html: $('<span class="wysiwyg-toolbar-divider"></span>')
|
||||
},
|
||||
normalize: lite ? false : {
|
||||
title: _t('Normalize'),
|
||||
image: '\uf0d0',
|
||||
click: function () {
|
||||
scope.normalize();
|
||||
}
|
||||
},
|
||||
showCode: lite ? false : {
|
||||
title: _t('Code'),
|
||||
image: '\uf121',
|
||||
click: function () {
|
||||
scope.toggleCode();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ui.formInput('Html', {
|
||||
|
||||
css: "html-item",
|
||||
metaWidget: true,
|
||||
|
||||
init: function(scope) {
|
||||
|
||||
scope.parse = function(value) {
|
||||
return value ? value : null;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
this._super(scope, element, attrs, model);
|
||||
var textElement = element.find('textarea');
|
||||
var buttons = getButtons(scope, textElement);
|
||||
|
||||
var props = scope.field,
|
||||
minSize = +props.minSize,
|
||||
maxSize = +props.maxSize,
|
||||
height = +(scope.field.height) || null;
|
||||
|
||||
if (height) {
|
||||
height = Math.max(100, height);
|
||||
}
|
||||
|
||||
function isParagraph(node) {
|
||||
return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
|
||||
}
|
||||
|
||||
function findParagraph(node) {
|
||||
if (!node) return null;
|
||||
if (node.classList && node.classList.contains('wysiwyg-editor')) return null;
|
||||
if (isParagraph(node)) return node;
|
||||
return findParagraph(node.parentNode);
|
||||
}
|
||||
|
||||
textElement.wysiwyg({
|
||||
toolbar: 'top',
|
||||
buttons: buttons,
|
||||
submit: {
|
||||
title: _t('Submit'),
|
||||
image: '\uf00c'
|
||||
},
|
||||
selectImage: _t('Click or drop image'),
|
||||
placeholderUrl: 'www.example.com',
|
||||
maxImageSize: [600, 200],
|
||||
hijackContextmenu: false,
|
||||
onKeyPress: function(key, character, shiftKey, altKey, ctrlKey, metaKey) {
|
||||
if (key !== 13 || shiftKey) {
|
||||
return;
|
||||
}
|
||||
var parent = findParagraph(document.getSelection().anchorNode);
|
||||
if (!parent) {
|
||||
document.execCommand('formatBlock', false, '<p>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var shell = textElement.wysiwyg('shell');
|
||||
var shellElement = $(shell.getElement());
|
||||
var shellActive = true;
|
||||
|
||||
shellElement.addClass('html-content');
|
||||
|
||||
if (scope.field.height) {
|
||||
shellElement.parent().height(height).resizable({
|
||||
handles: 's',
|
||||
resize: function () {
|
||||
shellElement.parent().width('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChange(e) {
|
||||
|
||||
var value = shellActive ? shell.getHTML() : textElement.val();
|
||||
var text = (value || '').trim()
|
||||
|
||||
if (text === '<p><br></p>' || text === '<br>') {
|
||||
value = null;
|
||||
}
|
||||
|
||||
var old = scope.getValue() || null;
|
||||
var txt = scope.parse(value) || null;
|
||||
|
||||
if (old === txt) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.setValue(value, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
scope.$render_editable = function () {
|
||||
var value = scope.getValue() || "";
|
||||
scope.text = scope.format(value);
|
||||
|
||||
var current = shellActive ? shell : textElement;
|
||||
var getter = shellActive ? 'getHTML' : 'val';
|
||||
var setter = shellActive ? 'setHTML' : 'val';
|
||||
|
||||
var html = current[getter]();
|
||||
if (value !== html) {
|
||||
current[setter](value);
|
||||
}
|
||||
};
|
||||
|
||||
textElement.on('input paste change blur', _.debounce(onChange, 100));
|
||||
|
||||
textElement.on("focus", _.once(function (e) {
|
||||
|
||||
// Chrome and Edge supports this
|
||||
document.execCommand('defaultParagraphSeparator', false, 'p');
|
||||
|
||||
// firefox uses attributes for some commands
|
||||
document.execCommand('styleWithCSS', false, true);
|
||||
document.execCommand('insertBrOnReturn', false, false);
|
||||
}));
|
||||
|
||||
scope.toggleCode = function () {
|
||||
|
||||
shellActive = !shellActive;
|
||||
|
||||
element.parent().find('.wysiwyg-toolbar-icon')
|
||||
.toggleClass('disabled', !shellActive)
|
||||
.last().removeClass('disabled');
|
||||
|
||||
if (shellActive) {
|
||||
textElement.hide();
|
||||
shellElement.show();
|
||||
shell.setHTML(textElement.val());
|
||||
} else {
|
||||
var height = Math.max(100, shellElement.outerHeight());
|
||||
shellElement.hide();
|
||||
textElement.show();
|
||||
|
||||
if (!scope.field.height) {
|
||||
textElement.height(height);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scope.normalize = function () {
|
||||
|
||||
var html = shell.getHTML();
|
||||
var div = $('<div>').html(html);
|
||||
|
||||
div.find('p').css({
|
||||
'margin-top': 0,
|
||||
'margin-bottom': '1em'});
|
||||
|
||||
div.find('ol,ul').each(function() {
|
||||
var el = $(this);
|
||||
if (el.parents('ol,ul').length) return;
|
||||
el.css({
|
||||
'margin-top': 0,
|
||||
'margin-bottom': '1em'});
|
||||
});
|
||||
|
||||
div.find('blockquote').each(function() {
|
||||
var el = $(this);
|
||||
el.css({
|
||||
'margin': el.parents('blockquote').length ? '0 0 0 2em' : '0 0 1em 2em',
|
||||
'border': 'none',
|
||||
'padding': 0
|
||||
});
|
||||
});
|
||||
|
||||
shellElement.focus();
|
||||
shell.setHTML(div[0].innerHTML);
|
||||
|
||||
div.remove();
|
||||
};
|
||||
|
||||
scope.validate = function(value) {
|
||||
if (_.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
var length = value.length,
|
||||
valid = true;
|
||||
|
||||
if (minSize) {
|
||||
valid = length >= minSize;
|
||||
}
|
||||
if(valid && maxSize) {
|
||||
valid = length <= maxSize;
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
template_readonly:
|
||||
'<div class="form-item-container">'+
|
||||
'<div class="html-viewer html-content" ui-bind-template x-text="text" x-locals="record" x-live="field.live"></div>'+
|
||||
'</div>',
|
||||
template_editable:
|
||||
'<div class="form-item-container">'+
|
||||
'<textarea class="html-editor html-content"></textarea>'+
|
||||
'</div>'
|
||||
|
||||
});
|
||||
|
||||
ui.directive('uiBindTemplate', ['$interpolate', function($interpolate){
|
||||
|
||||
function expand(scope, template) {
|
||||
if (!template || !template.match(/{{.*?}}/)) {
|
||||
return template;
|
||||
}
|
||||
return $interpolate(template)(scope.locals());
|
||||
}
|
||||
|
||||
return {
|
||||
terminal: true,
|
||||
scope: {
|
||||
locals: "&",
|
||||
text: "=text",
|
||||
live: "&"
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var template = null;
|
||||
|
||||
function update() {
|
||||
var output = expand(scope, template) || "";
|
||||
element.html(output);
|
||||
}
|
||||
|
||||
scope.$watch("text", function(text, old) {
|
||||
|
||||
if (text === template) {
|
||||
return;
|
||||
}
|
||||
template = text;
|
||||
update();
|
||||
});
|
||||
|
||||
var live = false;
|
||||
scope.$watch("live()", function(value) {
|
||||
if (live || !value) {
|
||||
return;
|
||||
}
|
||||
live = true;
|
||||
scope.$watch("locals()", function(value) {
|
||||
update();
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
431
sophal/js/form/form.input.json.js
Normal file
431
sophal/js/form/form.input.json.js
Normal file
@ -0,0 +1,431 @@
|
||||
/*
|
||||
* 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.formWidget('PanelJson', {
|
||||
showTitle: false,
|
||||
transclude: true,
|
||||
template: "<div class='panel-json' ui-transclude></div>"
|
||||
});
|
||||
|
||||
ui.formInput('JsonField', 'String', {
|
||||
showTitle: false,
|
||||
link: function (scope, element, attrs, model) {
|
||||
var field = scope.field;
|
||||
var jsonFields = field.jsonFields || [];
|
||||
var jsonNames = _.pluck(jsonFields, 'name');
|
||||
var jsonFix = {};
|
||||
|
||||
jsonFields.forEach(function (item) {
|
||||
if (item.widget && item.showTitle === undefined) {
|
||||
var widget = ui.getWidgetDef(item.widget);
|
||||
if (widget) {
|
||||
item.showTitle = widget.showTitle;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var defaultValues = {};
|
||||
var parentUnwatch = null;
|
||||
var selfUnwatch = null;
|
||||
|
||||
scope.formPath = scope.formPath ? scope.formPath + "." + field.name : field.name;
|
||||
scope.record = {};
|
||||
|
||||
jsonFields.forEach(function (item) {
|
||||
if (item.target === 'com.axelor.meta.db.MetaJsonRecord' &&
|
||||
item.targetName && item.targetName.indexOf('attrs.') === 0) {
|
||||
jsonFix[item.name] = function (v) {
|
||||
if (v) {
|
||||
v[item.targetName.substring(6)] = v[item.targetName];
|
||||
}
|
||||
return v;
|
||||
};
|
||||
}
|
||||
if (item.contextField && item.contextFieldValue) {
|
||||
if (item.showIf === undefined && item.hideIf === undefined && item.hidden) {
|
||||
return;
|
||||
}
|
||||
var condition = "($record." + item.contextField + ".id === " + item.contextFieldValue + ")";
|
||||
if (item.showIf) condition += " && (" + item.showIf + ")";
|
||||
if (item.hideIf) condition += " && !(" + item.hideIf + ")";
|
||||
item.showIf = condition;
|
||||
item.hideIf = null;
|
||||
}
|
||||
});
|
||||
|
||||
function getDefaultValues() {
|
||||
jsonFields.forEach(function (item) {
|
||||
if (item.defaultValue === undefined) return;
|
||||
var value = item.defaultValue;
|
||||
switch(item.type) {
|
||||
case 'integer':
|
||||
value = +(value);
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
value = value === 'now' ? new Date() : moment(value).toDate();
|
||||
break;
|
||||
}
|
||||
defaultValues[item.name] = value;
|
||||
});
|
||||
return angular.copy(defaultValues);
|
||||
}
|
||||
|
||||
function unwatchParent() {
|
||||
if (parentUnwatch) {
|
||||
parentUnwatch();
|
||||
parentUnwatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
function unwatchSelf() {
|
||||
if (selfUnwatch) {
|
||||
selfUnwatch();
|
||||
selfUnwatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
function watchParent() {
|
||||
unwatchParent();
|
||||
parentUnwatch = scope.$watch('$parent.record.' + field.name, function jsonParentWatch(value, old) {
|
||||
if (value === old) return;
|
||||
onRender();
|
||||
});
|
||||
}
|
||||
|
||||
function watchSelf() {
|
||||
unwatchSelf();
|
||||
selfUnwatch = scope.$watch('record', function jsonRecordWatch(record, old) {
|
||||
if (record !== old) {
|
||||
onUpdate();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function format(name, value) {
|
||||
var func = jsonFix[name];
|
||||
return func ? func(value) : value;
|
||||
}
|
||||
|
||||
function onUpdate() {
|
||||
var rec = null;
|
||||
_.each(scope.record, function (v, k) {
|
||||
if (k.indexOf('$') === 0 || v === null || v === undefined) return;
|
||||
if (_.isArray(v)) {
|
||||
if (v.length == 0) return;
|
||||
v = v.map(function (x) {
|
||||
return x.id ? { id: x.id } : x;
|
||||
});
|
||||
}
|
||||
if (rec === null) {
|
||||
rec = {};
|
||||
}
|
||||
rec[k] = format(k, v);
|
||||
});
|
||||
unwatchParent();
|
||||
if (scope.$parent.record[field.name] || rec) {
|
||||
scope.$parent.record[field.name] = rec ? angular.toJson(rec) : rec;
|
||||
}
|
||||
watchParent();
|
||||
}
|
||||
|
||||
function onRender() {
|
||||
var record = scope.$parent.record || {};
|
||||
var value = record[field.name];
|
||||
unwatchSelf();
|
||||
if (value) {
|
||||
scope.record = angular.fromJson(value);
|
||||
} else {
|
||||
scope.record = getDefaultValues();
|
||||
if (!_.isEmpty(scope.record)) {
|
||||
record[field.name] = angular.toJson(scope.record);
|
||||
}
|
||||
onUpdate();
|
||||
}
|
||||
scope._jsonContext = { '$record': record };
|
||||
record['$' + field.name] = scope.record;
|
||||
watchSelf();
|
||||
}
|
||||
|
||||
scope.$on('on:new', onRender);
|
||||
scope.$on('on:edit', function () {
|
||||
if (scope.viewType === 'form' || (!scope.viewType && scope._isPopup)) onRender();
|
||||
});
|
||||
|
||||
scope.updateJsonValues = function (values) {
|
||||
var rec = null;
|
||||
_.each(values, function (v, k) {
|
||||
if (jsonNames.indexOf(k) === -1 && scope.fields[k]) {
|
||||
scope.$parent.record[k] = v;
|
||||
} else {
|
||||
if (rec === null) {
|
||||
rec = {};
|
||||
}
|
||||
rec[k] = v;
|
||||
}
|
||||
});
|
||||
if (rec) {
|
||||
scope.record = _.extend({}, scope.record, rec);
|
||||
}
|
||||
};
|
||||
|
||||
watchParent();
|
||||
|
||||
// hide parent panel if no jsonFields defined
|
||||
scope.$evalAsync(function () {
|
||||
var parent = scope.$parent.field || {};
|
||||
if (parent.type === 'panel' && _.size(parent.items) === 1 && _.isEmpty(field.jsonFields)) {
|
||||
element.parents('.panel:first').addClass('hide').hide();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('on:update-context', function (e, context) {
|
||||
if (context && !context[field.name]) {
|
||||
context[field.name] = angular.toJson(scope.record || {});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.formInput('JsonRaw', 'String', {
|
||||
showTitle: false,
|
||||
link: function (scope, element, attrs, model) {
|
||||
|
||||
scope.placeHolderKey = _t('name');
|
||||
scope.placeHolderVal = _t('value');
|
||||
|
||||
scope.items = [];
|
||||
|
||||
scope.onAdd = function () {
|
||||
var last = _.last(scope.items);
|
||||
if (last && !(_.trim(last.name) && _.trim(last.value))) return;
|
||||
scope.items.push({});
|
||||
};
|
||||
|
||||
scope.onRemove = function (index) {
|
||||
if (scope.items.length > 0) {
|
||||
scope.items.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
var unwatch = null;
|
||||
|
||||
function doWatch () {
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
}
|
||||
unwatch = scope.$watch('items', function jsonItemsWatch(items, old) {
|
||||
if (items === old) return;
|
||||
var record = null;
|
||||
_.each(items, function (item) {
|
||||
if (!_.trim(item.name) || !_.trim(item.value)) return;
|
||||
if (record === null) {
|
||||
record = {};
|
||||
}
|
||||
record[item.name] = item.value;
|
||||
});
|
||||
model.$setViewValue(record ? angular.toJson(record) : null);
|
||||
}, true);
|
||||
}
|
||||
|
||||
model.$render = function () {
|
||||
var value = model.$viewValue;
|
||||
if (value) {
|
||||
value = angular.fromJson(value);
|
||||
} else {
|
||||
value = {};
|
||||
}
|
||||
scope.items = _.map(_.keys(value), function (name) {
|
||||
return { name: name, value: value[name] || '' };
|
||||
});
|
||||
doWatch();
|
||||
};
|
||||
},
|
||||
template_readonly:
|
||||
"<div class='json-editor'>" +
|
||||
"<table class='form-layout'>" +
|
||||
"<tr ng-repeat='(i, item) in items'>" +
|
||||
"<td class='form-label'>" +
|
||||
"<strong class='display-text'>{{item.name}}</strong>:" +
|
||||
"</td>" +
|
||||
"<td class='form-item'>" +
|
||||
"<span class='display-text'>{{item.value}}</span>" +
|
||||
"</td>" +
|
||||
"</tr>" +
|
||||
"</table>" +
|
||||
"</div>",
|
||||
template_editable:
|
||||
"<div class='json-editor'>" +
|
||||
"<table class='form-layout'>" +
|
||||
"<tr ng-repeat='(i, item) in items'>" +
|
||||
"<td class='form-item'><span class='form-item-container'>" +
|
||||
"<input type='text' placeholder='{{placeHolderKey}}' ng-model='item.name'></span>" +
|
||||
"</td>" +
|
||||
"<td class='form-item'><span class='form-item-container'>" +
|
||||
"<input type='text' placeholder='{{placeHolderVal}}' ng-model='item.value'></span>" +
|
||||
"</td>" +
|
||||
"<td><a href='' ng-click='onRemove(i)'><i class='fa fa-minus'></i></a></td>" +
|
||||
"</tr>" +
|
||||
"</table>" +
|
||||
"<a href='' ng-click='onAdd()'><i class='fa fa-plus'></i></a>" +
|
||||
"</div>"
|
||||
});
|
||||
|
||||
ui.formInput('JsonRefSelect', {
|
||||
|
||||
css: 'multi-object-select',
|
||||
|
||||
controller: ['$scope', 'ViewService', function($scope, ViewService) {
|
||||
|
||||
$scope.createElement = function(id, name, selectionList) {
|
||||
|
||||
var elemGroup = $('<div ui-group ui-table-layout cols="2" x-widths="150,*"></div>');
|
||||
var elemSelect = $('<input ui-select showTitle="false">')
|
||||
.attr("name", name + "$model")
|
||||
.attr("x-for-widget", id)
|
||||
.attr("ng-model", "record." + name + ".model");
|
||||
|
||||
var elemSelects = $('<div></div>').attr('ng-switch', "record." + name + ".model");
|
||||
var elemItems = _.map(selectionList, function(s) {
|
||||
return $('<input ui-json-ref-item ng-switch-when="' + s.value +'">')
|
||||
.attr('ng-model', 'record.' + name)
|
||||
.attr('name', name)
|
||||
.attr('x-target', s.value);
|
||||
});
|
||||
|
||||
elemGroup
|
||||
.append($('<div></div>').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;
|
||||
var selectionList = scope.field.selectionList;
|
||||
|
||||
scope.fieldsCache = {};
|
||||
|
||||
scope.refFireEvent = function (name) {
|
||||
var handler = scope.$events[name];
|
||||
if (handler) {
|
||||
return handler();
|
||||
}
|
||||
};
|
||||
|
||||
var elem = scope.createElement(element.attr('id'), name, selectionList);
|
||||
setTimeout(function() {
|
||||
element.append(elem);
|
||||
});
|
||||
|
||||
scope.$watch("record." + name + ".model", function jsonModelWatch(value, old) {
|
||||
if (value === old || old === undefined) return;
|
||||
if (scope.record && scope.record[name]) {
|
||||
scope.record[name] = _.pick(scope.record[name], 'model');
|
||||
if (!scope.record[name].model) {
|
||||
delete scope.record[name];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
template_editable: null,
|
||||
template_readonly: null
|
||||
});
|
||||
|
||||
ui.formInput('JsonRefItem', 'ManyToOne', {
|
||||
|
||||
showTitle: false,
|
||||
|
||||
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.field.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
|
||||
});
|
||||
|
||||
self._link(scope, element, attrs, model);
|
||||
}
|
||||
|
||||
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 name = element.attr('name');
|
||||
|
||||
scope.getValue = function () {
|
||||
return scope.record[name];
|
||||
};
|
||||
|
||||
var __setValue = scope.setValue;
|
||||
|
||||
scope.setValue = function (value) {
|
||||
var val = _.pick(scope.record[name], 'model');
|
||||
val = _.extend(val, value);
|
||||
__setValue.call(scope, val);
|
||||
};
|
||||
|
||||
function doSelect() {
|
||||
var value = (scope.record || {})[name];
|
||||
scope.select(value);
|
||||
}
|
||||
|
||||
scope.$watch("record", doSelect);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
252
sophal/js/form/form.input.number.js
Normal file
252
sophal/js/form/form.input.number.js
Normal file
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* jshint validthis: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
/**
|
||||
* The Numeric input widget.
|
||||
*/
|
||||
ui.formInput('Number', {
|
||||
|
||||
css: 'integer-item',
|
||||
|
||||
widgets: ['Integer', 'Long', 'Decimal'],
|
||||
|
||||
template_readonly: '<span class="display-text">{{localeValue()}}</span>',
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
var props = scope.field,
|
||||
minSize = +props.minSize,
|
||||
maxSize = +props.maxSize;
|
||||
|
||||
var isDecimal = props.serverType === "decimal" || props.widget === "decimal",
|
||||
pattern = isDecimal ? /^(-)?\d+(\.\d+)?$/ : /^\s*-?[0-9]*\s*$/;
|
||||
|
||||
function scale() {
|
||||
var value = scope.attr('scale');
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if ((props.widgetAttrs||{}).scale) {
|
||||
return props.widgetAttrs.scale;
|
||||
}
|
||||
return props.scale || 2;
|
||||
}
|
||||
|
||||
function precision() {
|
||||
var value = scope.attr('precision');
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if ((props.widgetAttrs||{}).precision) {
|
||||
return props.widgetAttrs.precision;
|
||||
}
|
||||
return props.precision || 18;
|
||||
}
|
||||
|
||||
scope.isNumber = function(value) {
|
||||
return _.isEmpty(value) || _.isNumber(value) || pattern.test(value);
|
||||
};
|
||||
|
||||
scope.validate = scope.isValid = function(value) {
|
||||
var valid = scope.isNumber(value);
|
||||
if (valid && isDecimal && _.isString(value)) {
|
||||
value = scope.format(value);
|
||||
valid = _.string.trim(value, '-').length - 1 <= precision();
|
||||
value = +value;
|
||||
}
|
||||
|
||||
if (valid && (minSize || minSize === 0)) {
|
||||
valid = value >= minSize;
|
||||
}
|
||||
if (valid && (maxSize || maxSize === 0)) {
|
||||
valid = value <= maxSize;
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
scope.localeValue = function localeValue() {
|
||||
var value = scope.getValue();
|
||||
var field = isDecimal ? _.extend({}, scope.field, {
|
||||
scale: scale(),
|
||||
precision: precision()
|
||||
}) : scope.field;
|
||||
return isDecimal
|
||||
? ui.formatters.decimal(field, value)
|
||||
: ui.formatters.integer(field, value);
|
||||
};
|
||||
|
||||
scope.format = function format(value) {
|
||||
if (isDecimal && _.isString(value) && value.trim().length > 0) {
|
||||
return parseFloat(value).toFixed(scale());
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.parse = function(value) {
|
||||
if (isDecimal) return value;
|
||||
if (value && _.isString(value)) return +value;
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.$on("on:attrs-changed", function (e, attr) {
|
||||
if (attr.name === 'scale' || attr.name === 'precision') {
|
||||
model.$render();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
|
||||
var props = scope.field;
|
||||
|
||||
var options = {
|
||||
step: 1
|
||||
};
|
||||
|
||||
element.on("spin", onSpin);
|
||||
element.on("spinchange", function(e, row) {
|
||||
updateModel(element.val());
|
||||
});
|
||||
element.on("grid:check", function(e, row) {
|
||||
updateModel(element.val());
|
||||
});
|
||||
|
||||
var pendingChange = false;
|
||||
|
||||
function handleChange(changed) {
|
||||
var onChange = scope.$events.onChange;
|
||||
if (onChange && (changed || pendingChange)) {
|
||||
pendingChange = false;
|
||||
setTimeout(onChange);
|
||||
}
|
||||
}
|
||||
|
||||
function equals(a, b) {
|
||||
if (a === b) return true;
|
||||
if (angular.equals(a, b)) return true;
|
||||
if (a === "" && b === undefined) return true;
|
||||
if (b === "" && a === undefined) return true;
|
||||
if (a === undefined || b === undefined) return false;
|
||||
if (a === null || b === null) return false;
|
||||
if (!scope.isNumber(a) || !scope.isNumber(b)) return false;
|
||||
a = a === "" ? a : ((+a) || 0);
|
||||
b = b === "" ? b : ((+b) || 0);
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function updateModel(value, handle) {
|
||||
if (!scope.isNumber(value)) {
|
||||
return model.$setViewValue(value); // force validation
|
||||
}
|
||||
var val = scope.parse(value);
|
||||
var old = scope.getValue();
|
||||
var text = scope.format(value);
|
||||
|
||||
element.val(text);
|
||||
|
||||
if (equals(val, old)) {
|
||||
return handleChange();
|
||||
}
|
||||
|
||||
scope.setValue(val);
|
||||
scope.$applyAsync();
|
||||
|
||||
pendingChange = true;
|
||||
|
||||
if (handle !== false) {
|
||||
handleChange();
|
||||
}
|
||||
}
|
||||
|
||||
function onSpin(event, ui) {
|
||||
|
||||
var text = this.value,
|
||||
value = ui.value,
|
||||
orig = element.spinner('value'),
|
||||
parts, integer, decimal, min, max, dir = 0;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (!scope.isNumber(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value < orig)
|
||||
dir = -1;
|
||||
if (value > orig)
|
||||
dir = 1;
|
||||
|
||||
parts = text.split(/\./);
|
||||
integer = +parts[0];
|
||||
decimal = parts[1];
|
||||
|
||||
integer += dir;
|
||||
if (parts.length > 1) {
|
||||
value = integer + '.' + decimal;
|
||||
}
|
||||
|
||||
min = options.min;
|
||||
max = options.max;
|
||||
|
||||
if (_.isNumber(min) && value < min)
|
||||
value = min;
|
||||
if (_.isNumber(max) && value > max)
|
||||
value = max;
|
||||
|
||||
updateModel(value, false);
|
||||
}
|
||||
|
||||
if (props.minSize !== undefined)
|
||||
options.min = +props.minSize;
|
||||
if (props.maxSize !== undefined)
|
||||
options.max = +props.maxSize;
|
||||
|
||||
setTimeout(function(){
|
||||
element.spinner(options);
|
||||
scope.$elem_editable = element.parent();
|
||||
model.$render = function() {
|
||||
var value = model.$viewValue;
|
||||
if (value) {
|
||||
value = scope.format(value);
|
||||
}
|
||||
element.val(value);
|
||||
scope.initValue(value);
|
||||
};
|
||||
model.$render();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// fix spinner repeat issue
|
||||
var oldRepeat = $.ui.spinner.prototype._repeat;
|
||||
$.ui.spinner.prototype._repeat = function () {
|
||||
if (this.element.scope().isReadonly()) {
|
||||
return this._stop();
|
||||
}
|
||||
return oldRepeat.apply(this, arguments);
|
||||
};
|
||||
|
||||
})();
|
||||
115
sophal/js/form/form.input.progress.js
Normal file
115
sophal/js/form/form.input.progress.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.ProgressMixin = {
|
||||
|
||||
css: 'progress-item',
|
||||
cellCss: 'form-item progress-item',
|
||||
metaWidget: true,
|
||||
|
||||
link_readonly: function(scope, element, attrs, model) {
|
||||
|
||||
var field = scope.field || {},
|
||||
that = this;
|
||||
|
||||
scope.$watch("getValue()", function progressValueWatch(value, old) {
|
||||
var props = that.compute(field, value);
|
||||
scope.cssClasses = 'progress ' + props.css;
|
||||
scope.styles = {
|
||||
width: props.width + '%'
|
||||
};
|
||||
scope.css = props.css;
|
||||
scope.width = props.width;
|
||||
});
|
||||
},
|
||||
|
||||
compute: function(field, value) {
|
||||
|
||||
var max = +(field.max) || 100,
|
||||
min = +(field.min) || 0;
|
||||
|
||||
var colors = [
|
||||
["r", 24], // 00 - 24 (red)
|
||||
["y", 49], // 25 - 49 (yellow)
|
||||
["b", 74], // 50 - 74 (blue)
|
||||
["g", 100] // 75 - 100 (green)
|
||||
];
|
||||
|
||||
if (field.colors) {
|
||||
colors = _.chain(field.colors.split(/,/)).invoke('split', /:/).value() || [];
|
||||
}
|
||||
|
||||
colors.reverse();
|
||||
|
||||
var styles = {
|
||||
"r": "progress-danger",
|
||||
"y": "progress-warning",
|
||||
"b": "progress-primary",
|
||||
"g": "progress-success"
|
||||
};
|
||||
|
||||
var width = +(value) || 0;
|
||||
var css = "progress-striped";
|
||||
|
||||
width = (width * 100) / (max - min);
|
||||
width = Math.min(Math.round(width), 100);
|
||||
|
||||
var color = "";
|
||||
for(var i = 0 ; i < colors.length; i++) {
|
||||
var c = colors[i][0];
|
||||
var v = +colors[i][1];
|
||||
if (width <= v) {
|
||||
color = styles[c] || "";
|
||||
}
|
||||
}
|
||||
|
||||
css += " " + color;
|
||||
if (width < 100) {
|
||||
css += " " + "active";
|
||||
}
|
||||
|
||||
return {
|
||||
css: css,
|
||||
width: width
|
||||
};
|
||||
},
|
||||
|
||||
template_readonly:
|
||||
'<div ng-class="cssClasses">'+
|
||||
'<div class="bar" ng-style="styles"></div>'+
|
||||
'</div>'
|
||||
};
|
||||
|
||||
/**
|
||||
* The Progress widget with integer/decimal input.
|
||||
*
|
||||
*/
|
||||
ui.formInput('Progress', 'Integer', _.extend({}, ui.ProgressMixin));
|
||||
|
||||
/**
|
||||
* The Progress widget with selection input.
|
||||
*
|
||||
*/
|
||||
ui.formInput('SelectProgress', 'Select', _.extend({}, ui.ProgressMixin));
|
||||
|
||||
})();
|
||||
1011
sophal/js/form/form.input.select.js
Normal file
1011
sophal/js/form/form.input.select.js
Normal file
File diff suppressed because it is too large
Load Diff
165
sophal/js/form/form.input.spreadsheet.js
Normal file
165
sophal/js/form/form.input.spreadsheet.js
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* global Handsontable: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.formInput('Spreadsheet', {
|
||||
|
||||
css: "spreadsheet-item",
|
||||
|
||||
link: function (scope, element, attrs, model) {
|
||||
|
||||
var field = scope.field;
|
||||
var height = field.height || 580;
|
||||
|
||||
var inst;
|
||||
|
||||
scope.$timeout(function () {
|
||||
|
||||
element.height(height).css({
|
||||
"position": "relative",
|
||||
"overflow": "hidden"
|
||||
});
|
||||
|
||||
inst = new Handsontable(element[0], {
|
||||
colWidths: 60,
|
||||
rowHeaders: true,
|
||||
colHeaders: true,
|
||||
contextMenu: true,
|
||||
manualColumnResize: true,
|
||||
manualRowResize: true,
|
||||
afterChange: function (change, source) {
|
||||
if (source !== 'loadData') {
|
||||
update();
|
||||
}
|
||||
},
|
||||
afterCreateCol: update,
|
||||
afterCreateRow: update,
|
||||
afterRemoveCol: update,
|
||||
afterRemoveRow: update
|
||||
});
|
||||
model.$render();
|
||||
});
|
||||
|
||||
element.resizable({
|
||||
handles: 's',
|
||||
resize: function () {
|
||||
if (inst) {
|
||||
inst.render();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function update() {
|
||||
if (!inst) { return; }
|
||||
var current = model.$viewValue;
|
||||
var value = compact(inst.getData());
|
||||
|
||||
value = value ? JSON.stringify(value) : value;
|
||||
if (value === current) {
|
||||
return;
|
||||
}
|
||||
scope.setValue(value, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
function compact(items) {
|
||||
var res = [];
|
||||
var i;
|
||||
|
||||
for (i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (Array.isArray(item)) {
|
||||
item = compact(item);
|
||||
}
|
||||
if (item === "" || item === null || item === undefined || item.length === 0) {
|
||||
continue;
|
||||
}
|
||||
res[i] = item;
|
||||
}
|
||||
|
||||
var n = res.length;
|
||||
for (i = n - 1; i >= 0; i--) {
|
||||
if (res[i] !== null) {
|
||||
n = i+1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
res = res.slice(0, n);
|
||||
return res.length ? res : null;
|
||||
}
|
||||
|
||||
function fill(data) {
|
||||
var cols = 0;
|
||||
var rows = data.length;
|
||||
var i, row;
|
||||
|
||||
for(i = 0; i < data.length; i++) {
|
||||
row = data[i] || (data[i] = []);
|
||||
cols = Math.max(row.length, cols);
|
||||
}
|
||||
|
||||
cols = Math.max(50, cols);
|
||||
rows = Math.max(100, rows);
|
||||
|
||||
for(i = 0; i < rows; i++) {
|
||||
row = data[i] || (data[i] = []);
|
||||
for (var j = 0; j < cols + 1; j++) {
|
||||
if (row[j] === undefined) {
|
||||
row[j] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
model.$render = function () {
|
||||
var value = null;
|
||||
try {
|
||||
value = JSON.parse(model.$viewValue) || null;
|
||||
} catch (e) {
|
||||
}
|
||||
if (inst) {
|
||||
value = fill(value || []);
|
||||
inst.loadData(value || null);
|
||||
setTimeout(function () {
|
||||
inst.render();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
scope.$on("$destroy", function () {
|
||||
if (inst) {
|
||||
inst.destroy();
|
||||
inst = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
template_editable: null,
|
||||
template_readonly: null,
|
||||
template:
|
||||
"<div></div>"
|
||||
});
|
||||
|
||||
})();
|
||||
849
sophal/js/form/form.input.static.js
Normal file
849
sophal/js/form/form.input.static.js
Normal file
@ -0,0 +1,849 @@
|
||||
/*
|
||||
* 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 popoverElem = null;
|
||||
var popoverTimer = null;
|
||||
|
||||
function canDisplayPopover(scope, details) {
|
||||
if (axelor.device.mobile) {
|
||||
return false;
|
||||
}
|
||||
if(!axelor.config['user.technical']) {
|
||||
return details ? false : scope.field && scope.field.help;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function makePopover(scope, element, callback, placement) {
|
||||
|
||||
var mode = axelor.config['application.mode'];
|
||||
var tech = axelor.config['user.technical'];
|
||||
var doc = $(document);
|
||||
|
||||
var table = null;
|
||||
|
||||
function addRow(label, text, klass) {
|
||||
if (table === null) {
|
||||
table = $('<table class="field-details"></table>');
|
||||
}
|
||||
|
||||
var tr = $('<tr></tr>').appendTo(table);
|
||||
if (label) {
|
||||
$('<th></th>').text(label).appendTo(tr);
|
||||
}
|
||||
if (klass == null) {
|
||||
text = '<code>' + text + '</code>';
|
||||
}
|
||||
var td = $('<td></td>').html(text).addClass(klass).appendTo(tr);
|
||||
if (!label) {
|
||||
td.attr('colspan', 2);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
element.popover({
|
||||
html: true,
|
||||
delay: { show: 1000, hide: 100 },
|
||||
animate: true,
|
||||
placement: function() {
|
||||
if (placement) return placement;
|
||||
var coord = $(element.get(0)).offset(),
|
||||
viewport = { height: window.innerHeight, width: window.innerWidth };
|
||||
if(viewport.height < (coord.top + 100))
|
||||
return 'top';
|
||||
if(coord.left > (viewport.width / 2))
|
||||
return 'left';
|
||||
return 'right';
|
||||
},
|
||||
trigger: 'manual',
|
||||
container: 'body',
|
||||
title: function() {
|
||||
return element.text();
|
||||
},
|
||||
content: function() {
|
||||
if (table) {
|
||||
table.remove();
|
||||
table = null;
|
||||
}
|
||||
callback(scope, addRow);
|
||||
if (table) return table;
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
element.on('mouseenter.popover', enter);
|
||||
element.on('mouseleave.popover', leave);
|
||||
|
||||
function selectText(elem) {
|
||||
var el = $(elem).get(0);
|
||||
if (document.selection) {
|
||||
var range = document.body.createTextRange();
|
||||
range.moveToElementText(el);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
function enter(e, show) {
|
||||
if (popoverTimer) {
|
||||
clearTimeout(popoverTimer);
|
||||
}
|
||||
popoverTimer = setTimeout(function () {
|
||||
if (popoverElem === null) {
|
||||
popoverElem = element;
|
||||
popoverElem.popover('show');
|
||||
if (e.ctrlKey) {
|
||||
selectText(table.find('.field-name,.model-name').get(0));
|
||||
}
|
||||
}
|
||||
var tip = element.data('popover').$tip;
|
||||
if (tip) {
|
||||
tip.attr('tabIndex', 0);
|
||||
tip.css('outline', 'none');
|
||||
}
|
||||
}, (e.ctrlKey || show) ? 0 : 1000);
|
||||
}
|
||||
|
||||
function leave(e) {
|
||||
|
||||
if (e.ctrlKey) {
|
||||
doc.off('mousemove.popover');
|
||||
doc.on('mousemove.popover', leave);
|
||||
return;
|
||||
}
|
||||
|
||||
if (popoverTimer) {
|
||||
clearTimeout(popoverTimer);
|
||||
popoverTimer = null;
|
||||
}
|
||||
if (popoverElem) {
|
||||
popoverElem.popover('hide');
|
||||
popoverElem = null;
|
||||
doc.off('mousemove.popover');
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (popoverTimer) {
|
||||
clearTimeout(popoverTimer);
|
||||
popoverTimer = null;
|
||||
}
|
||||
if (element) {
|
||||
element.off('mouseenter.popover');
|
||||
element.off('mouseleave.popover');
|
||||
element.popover('destroy');
|
||||
element = null;
|
||||
}
|
||||
if (table) {
|
||||
table.remove();
|
||||
table = null;
|
||||
}
|
||||
doc.off('mousemove.popover');
|
||||
}
|
||||
|
||||
element.on('$destroy', destroy);
|
||||
scope.$on('$destroy', destroy);
|
||||
}
|
||||
|
||||
function setupPopover(scope, element, getHelp, placement) {
|
||||
|
||||
if (!canDisplayPopover(scope, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
element.on('mouseenter.help.setup', function (e) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(function () {
|
||||
element.off('mouseenter.help.setup');
|
||||
element.off('mouseleave.help.setup');
|
||||
makePopover(scope, element, getHelp, placement);
|
||||
element.trigger('mouseenter.popover', true);
|
||||
}, e.ctrlKey ? 0 : 1000);
|
||||
});
|
||||
element.on('mouseleave.help.setup $destroy', function () {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.directive('uiTabPopover', function() {
|
||||
|
||||
function getHelp(scope, addRow) {
|
||||
var tab = scope.tab || {};
|
||||
var type = tab.viewType;
|
||||
var view = _.findWhere(tab.views, {type: type});
|
||||
|
||||
var viewScope = tab.$viewScope;
|
||||
if (viewScope && viewScope.schema) {
|
||||
view = viewScope.schema;
|
||||
}
|
||||
|
||||
if (tab.action) {
|
||||
addRow(_t('Action'), tab.action);
|
||||
}
|
||||
if (tab.model) {
|
||||
addRow(_t('Object'), '<code>' + tab.model + '</code>', 'model-name');
|
||||
}
|
||||
if (tab.domain) {
|
||||
addRow(_t('Domain'), tab.domain);
|
||||
}
|
||||
if (view && view.name) {
|
||||
addRow(_t('View'), view.name);
|
||||
}
|
||||
}
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
setupPopover(scope, element, getHelp, 'bottom');
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiHelpPopover', function() {
|
||||
|
||||
function getHelp(scope, addRow) {
|
||||
|
||||
var field = scope.field;
|
||||
var text = field.help;
|
||||
if (text) {
|
||||
text = text.replace(/\\n/g, '<br>');
|
||||
addRow(null, text, 'help-text');
|
||||
}
|
||||
|
||||
if(!canDisplayPopover(scope, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
addRow(null, '<hr noshade>', 'help-text');
|
||||
}
|
||||
|
||||
var model = scope._model;
|
||||
if (model === field.target) {
|
||||
model = scope._parentModel || scope.$parent._model;
|
||||
}
|
||||
|
||||
addRow(_t('Object'), model);
|
||||
addRow(_t('Field Name'), '<code>' + field.name + '</code>', 'field-name');
|
||||
addRow(_t('Field Type'), field.serverType);
|
||||
|
||||
if (field.type === 'text') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.domain) {
|
||||
addRow(_t('Filter'), field.domain);
|
||||
}
|
||||
|
||||
if (field.target) {
|
||||
addRow(_t('Reference'), field.target);
|
||||
}
|
||||
|
||||
var value = scope.$eval('$$original.' + field.name);
|
||||
var length;
|
||||
|
||||
if (value && /-one$/.test(field.serverType)) {
|
||||
value = _.compact([value.id, value[field.targetName]]).join(',');
|
||||
value = '(' + value + ')';
|
||||
}
|
||||
if (value && field.type === "password") {
|
||||
value = _.str.repeat('*', value.length);
|
||||
}
|
||||
if (value && /^(string|image|binary)$/.test(field.type)) {
|
||||
length = value.length;
|
||||
value = _.first(value, 50);
|
||||
if (length > 50) {
|
||||
value.push('...');
|
||||
}
|
||||
value = value.join('');
|
||||
}
|
||||
if (value && /(panel-related|one-to-many|many-to-many)/.test(field.serverType)) {
|
||||
length = value.length;
|
||||
value = _.first(value, 5);
|
||||
value = _.map(value, function(v){
|
||||
return v.id;
|
||||
});
|
||||
if (length > 5) {
|
||||
value.push('...');
|
||||
}
|
||||
value = value.join(', ');
|
||||
}
|
||||
|
||||
addRow(_t('Orig. Value'), value);
|
||||
}
|
||||
|
||||
function doLink(scope, element, attrs) {
|
||||
var field = scope.field;
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
if (field.help && axelor.config['user.noHelp'] !== true) {
|
||||
if (element.parent('label').length) {
|
||||
element.parent('label').addClass('has-help');
|
||||
} else {
|
||||
element.addClass('has-help');
|
||||
}
|
||||
}
|
||||
setupPopover(scope, element, getHelp);
|
||||
}
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
var field = scope.field;
|
||||
if (!_.isEmpty(field)) {
|
||||
return doLink(scope, element, attrs);
|
||||
}
|
||||
var unwatch = scope.$watch('field', function popoverFieldWatch(field, old) {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
doLink(scope, element, attrs);
|
||||
}, true);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The Label widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('Label', {
|
||||
|
||||
css: 'label-item',
|
||||
cellCss: 'form-label',
|
||||
|
||||
transclude: true,
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
var field = scope.field;
|
||||
if (field && field.required) {
|
||||
element.addClass('required');
|
||||
}
|
||||
},
|
||||
|
||||
template:
|
||||
"<label><span ui-help-popover ng-transclude></span></label>"
|
||||
});
|
||||
|
||||
ui.directive('uiTranslateIcon', ['$q', function ($q) {
|
||||
return {
|
||||
link: function (scope, element) {
|
||||
var icon = $("<i class='fa fa-flag translate-icon'></i>").attr('title', _t('Show translations.')).appendTo(element);
|
||||
var toggle = function () {
|
||||
icon.toggle(!scope.$$readonlyOrig);
|
||||
};
|
||||
|
||||
scope.$watch("$$readonlyOrig", toggle);
|
||||
scope.$on("on:new", toggle);
|
||||
scope.$on("on:edit", toggle);
|
||||
|
||||
var myDs = scope._dataSource;
|
||||
var trDs = scope._dataSource._new("com.axelor.meta.db.MetaTranslation");
|
||||
trDs._sortBy = ["id"];
|
||||
|
||||
function saveData(value, data, orig, callback) {
|
||||
var changed = [];
|
||||
var removed = [];
|
||||
|
||||
data.forEach(function (item) {
|
||||
var found = _.findWhere(orig, { id: item.id });
|
||||
if (!angular.equals(found, item)) {
|
||||
changed.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
orig.forEach(function (item) {
|
||||
var found = _.findWhere(data, { id: item.id });
|
||||
if (!found) {
|
||||
removed.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
function saveTranslations() {
|
||||
var all = [];
|
||||
|
||||
if (removed.length) {
|
||||
all.push(trDs.removeAll(removed));
|
||||
}
|
||||
if (changed.length) {
|
||||
all.push(trDs.saveAll(changed));
|
||||
}
|
||||
|
||||
if (all.length) {
|
||||
$q.all(all).then(function () {
|
||||
var lang = axelor.config['user.lang'] || en;
|
||||
var key = 'value:' + scope.getValue();
|
||||
var trKey = '$t:' + scope.field.name;
|
||||
return trDs.search({
|
||||
domain: "self.key = :key and self.language = :lang",
|
||||
context: { key: key, lang: lang },
|
||||
limit: 1
|
||||
}).success(function (records) {
|
||||
var record = _.first(records);
|
||||
if (scope.record) {
|
||||
scope.record[trKey] = (record||{}).message;
|
||||
scope.$parent.$parent.text = scope.format(scope.getValue());
|
||||
var rec = scope._dataSource.get(scope.record.id);
|
||||
if (rec) {
|
||||
rec[trKey] = scope.record[trKey];
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(callback, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
if (value !== scope.getValue()) {
|
||||
scope.$parent.$parent.setValue(value, true);
|
||||
scope.waitForActions(function () {
|
||||
scope.$parent.$parent.onSave().then(saveTranslations, callback);
|
||||
});
|
||||
} else {
|
||||
saveTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
function showPopup(data) {
|
||||
|
||||
if (!data || data.length == 0) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
var value = scope.getValue();
|
||||
|
||||
var orig = angular.copy(data);
|
||||
var form = $("<form>");
|
||||
|
||||
var valueInput = (scope.field.multiline
|
||||
? $("<textarea class='span12'>")
|
||||
: $("<input type='text' class='span12'>"))
|
||||
.prop('name', scope.field.name)
|
||||
.prop('required', true)
|
||||
.val(value)
|
||||
.on('input', function () {
|
||||
value = this.value;
|
||||
data.forEach(function (item) {
|
||||
item.key = 'value:' + value;
|
||||
});
|
||||
});
|
||||
|
||||
// add value fields
|
||||
$("<div class='row-fluid'>")
|
||||
.append($("<label class='span12'>").text(_t("Value")))
|
||||
.appendTo(form);
|
||||
$("<div class='row-fluid'>")
|
||||
.append(valueInput)
|
||||
.appendTo(form);
|
||||
|
||||
form.append('<hr>');
|
||||
|
||||
// add translation fields
|
||||
$("<div class='row-fluid'>")
|
||||
.append($("<label class='span8'>").text(_t("Translation")))
|
||||
.append($("<label class='span4'>").text(_t("Language")))
|
||||
.appendTo(form);
|
||||
|
||||
function addRow(item) {
|
||||
var onchange = function () {
|
||||
var v = item[this.name];
|
||||
if (v !== this.value) {
|
||||
item[this.name] = this.value;
|
||||
}
|
||||
};
|
||||
|
||||
item.key = item.key || ('value:' + value);
|
||||
|
||||
var input1 = (scope.field.multiline
|
||||
? $("<textarea class='span8'>")
|
||||
: $("<input type='text' class='span8'>"))
|
||||
.prop("name", "message")
|
||||
.prop("required", true)
|
||||
.val(item.message)
|
||||
.on("input", onchange);
|
||||
var input2 = $("<input type='text' class='span4'>")
|
||||
.prop("name", "language")
|
||||
.prop("required", true)
|
||||
.val(item.language)
|
||||
.on("input", onchange);
|
||||
var row = $("<div class='row-fluid'>")
|
||||
.append(input1)
|
||||
.append(input2)
|
||||
.appendTo(form);
|
||||
|
||||
if (dialog) {
|
||||
input1.focus();
|
||||
}
|
||||
|
||||
// remove icon
|
||||
$("<i class='fa fa-times'>")
|
||||
.add('help', _t('Remove'))
|
||||
.appendTo(row)
|
||||
.click(function () {
|
||||
var i = data.indexOf(item);
|
||||
data.splice(i, 1);
|
||||
row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function addNew() {
|
||||
var item = {};
|
||||
data.push(item);
|
||||
addRow(item);
|
||||
}
|
||||
|
||||
var dialog;
|
||||
|
||||
function validate() {
|
||||
var empty = html.find('input:text[value=""]');
|
||||
if (empty.length) {
|
||||
empty.first().focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var html = $("<div>").append(form);
|
||||
|
||||
// add icon
|
||||
$("<i class='fa fa-plus'>")
|
||||
.attr('help', _t('Add'))
|
||||
.appendTo(html)
|
||||
.click(function () {
|
||||
if (validate()) {
|
||||
addNew();
|
||||
}
|
||||
});
|
||||
|
||||
data.forEach(addRow);
|
||||
|
||||
if (data.length === 0) {
|
||||
addNew();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (dialog) {
|
||||
dialog.dialog('close');
|
||||
}
|
||||
}
|
||||
|
||||
dialog = axelor.dialogs.box(html, {
|
||||
title: _t('Translations'),
|
||||
buttons: [{
|
||||
'text' : _t('Cancel'),
|
||||
'class' : 'btn',
|
||||
'click' : close
|
||||
}, {
|
||||
'text' : _t('OK'),
|
||||
'class' : 'btn btn-primary',
|
||||
'click' : function() {
|
||||
if (validate()) {
|
||||
saveData(value, data, orig, close);
|
||||
}
|
||||
}
|
||||
}]
|
||||
}).addClass('translation-form');
|
||||
}
|
||||
|
||||
icon.click(function (e) {
|
||||
var value = scope.getValue();
|
||||
if (value) {
|
||||
trDs.search({
|
||||
domain: "self.key = :key",
|
||||
context: { key: 'value:' + value }
|
||||
}).success(showPopup);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* The Spacer widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('Spacer', {
|
||||
css: 'spacer-item',
|
||||
template: '<div> </div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Separator widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('Separator', {
|
||||
css: 'separator-item',
|
||||
showTitle: false,
|
||||
template: '<div>{{field.title}}</div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Static Text widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('Static', {
|
||||
css: 'static-item',
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
var field = scope.field;
|
||||
element.html(field.text);
|
||||
},
|
||||
template: '<div></div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Static Label widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('StaticLabel', {
|
||||
css: 'static-item',
|
||||
transclude: true,
|
||||
template: '<label ng-transclude></label>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Help Text widget.
|
||||
*
|
||||
*/
|
||||
ui.formItem('Help', {
|
||||
css: 'help-item',
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
var field = scope.field;
|
||||
var css = "alert alert-info";
|
||||
if (field.css && field.css.indexOf('alert-') > -1) {
|
||||
css = "alert";
|
||||
}
|
||||
element.addClass(css).html(field.text);
|
||||
},
|
||||
template: '<div></div>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The button widget.
|
||||
*/
|
||||
ui.formItem('Button', {
|
||||
css: 'button-item',
|
||||
transclude: true,
|
||||
link: function(scope, element, attrs, model) {
|
||||
var field = scope.field || {};
|
||||
|
||||
var icon = field.icon || "";
|
||||
var iconHover = field.iconHover || "";
|
||||
|
||||
var isIcon = icon.indexOf('fa-') === 0;
|
||||
|
||||
if (isIcon || icon) {
|
||||
element.prepend(' ');
|
||||
}
|
||||
|
||||
var css = field.css || '';
|
||||
if (css.indexOf('btn-') > -1 && css.indexOf('btn-primary') === -1) {
|
||||
element.removeClass('btn-primary');
|
||||
}
|
||||
|
||||
if (field && field.help && axelor.config['user.noHelp'] !== true) {
|
||||
element.addClass('has-help');
|
||||
}
|
||||
|
||||
if (isIcon) {
|
||||
var e = $('<i>').addClass('fa').addClass(icon).prependTo(element);
|
||||
if (iconHover) {
|
||||
e.hover(function() {
|
||||
$(this).removeClass(icon).addClass(iconHover);
|
||||
}, function() {
|
||||
$(this).removeClass(iconHover).addClass(icon);
|
||||
});
|
||||
}
|
||||
} else if (icon) {
|
||||
$('<img>').attr('src', icon).prependTo(element);
|
||||
}
|
||||
|
||||
if (!field.title) {
|
||||
element.addClass("button-icon");
|
||||
}
|
||||
|
||||
if (_.isString(field.link)) {
|
||||
element.removeClass('btn btn-primary').addClass('btn-link');
|
||||
element.attr("href", field.link);
|
||||
}
|
||||
|
||||
element.one('mouseover', function () {
|
||||
element.tooltip({
|
||||
html: true,
|
||||
title: function() {
|
||||
if (field.help) {
|
||||
return field.help;
|
||||
}
|
||||
if (element.innerWidth() < element[0].scrollWidth) {
|
||||
return field.title;
|
||||
}
|
||||
},
|
||||
delay: { show: 1000, hide: 100 },
|
||||
container: 'body'
|
||||
});
|
||||
|
||||
element.on("$destroy", function () {
|
||||
var t = element.data('tooltip');
|
||||
if (t) {
|
||||
t.destroy();
|
||||
t = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
element.on("click", function(e) {
|
||||
|
||||
if (scope.isReadonlyExclusive() || element.hasClass('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function enable() {
|
||||
scope.ajaxStop(function () {
|
||||
setDisabled(scope.isReadonlyExclusive());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function setEnable(p) {
|
||||
if (p && p.then) {
|
||||
p.then(enable, enable);
|
||||
} else {
|
||||
scope.ajaxStop(enable, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function doClick() {
|
||||
setEnable(scope.fireAction("onClick"));
|
||||
}
|
||||
|
||||
setDisabled(true);
|
||||
|
||||
if (scope.waitForActions) {
|
||||
return scope.waitForActions(doClick);
|
||||
}
|
||||
return doClick();
|
||||
});
|
||||
|
||||
function setDisabled(disabled) {
|
||||
if (disabled || disabled === undefined) {
|
||||
return element.addClass("disabled").attr('tabindex', -1);
|
||||
}
|
||||
return element.removeClass("disabled").removeAttr('tabindex');
|
||||
}
|
||||
|
||||
var readonlySet = false;
|
||||
scope.$watch('isReadonlyExclusive()', function buttonReadonlyWatch(readonly, old) {
|
||||
if (readonly === old && readonlySet) return;
|
||||
readonlySet = true;
|
||||
return setDisabled(readonly);
|
||||
});
|
||||
|
||||
scope.$watch('attr("title")', function buttonTitleWatch(title, old) {
|
||||
if (!title || title === old) return;
|
||||
if (element.is('button')) {
|
||||
return element.html(title);
|
||||
}
|
||||
element.children('.btn-text').html(title);
|
||||
});
|
||||
|
||||
scope.$watch('attr("css")', function buttonCssWatch(css, old) {
|
||||
var curr = css || field.css || 'btn-success';
|
||||
var prev = old || field.css || 'btn-success';
|
||||
if (curr !== prev) {
|
||||
element.removeClass(prev || '').addClass(curr);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch('attr("icon")', function buttonIconWatch(icon, old) {
|
||||
if (icon === old || (icon && icon.indexOf('fa-') !== 0)) return;
|
||||
var iconElem = element.find('i.fa:first');
|
||||
if (iconElem.length == 0) {
|
||||
iconElem = $('<i>').addClass('fa').prependTo(element.prepend(' '));
|
||||
}
|
||||
iconElem.removeClass(old || '').addClass(icon || field.icon || '');
|
||||
});
|
||||
},
|
||||
template: '<a href="" class="btn btn-primary">'+
|
||||
'<span class="btn-text" ng-transclude></span>'+
|
||||
'</a>'
|
||||
});
|
||||
|
||||
ui.formItem('InfoButton', 'Button', {
|
||||
link: function (scope, element, attrs) {
|
||||
this._super.apply(this, arguments);
|
||||
var field = scope.field || {};
|
||||
scope.title = field.title;
|
||||
scope.$watch('attr("title")', function infoButtonTitleWatch(title, old) {
|
||||
if (!title || title === old) return;
|
||||
scope.title = title;
|
||||
});
|
||||
Object.defineProperty(scope, 'value', {
|
||||
get: function () {
|
||||
return field.currency
|
||||
? ui.formatters.decimal(field, (scope.record || {})[field.name], scope.record)
|
||||
: ui.formatters.$fmt(scope, field.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='btn info-button'>" +
|
||||
"<div class='info-button-data'>" +
|
||||
"<span class='info-button-value'>{{value}}</span>" +
|
||||
"<small class='info-button-title'>{{title}}</small>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
});
|
||||
|
||||
ui.formItem('ToolButton', 'Button', {
|
||||
|
||||
getViewDef: function(element) {
|
||||
return this.btn;
|
||||
},
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
this._super.apply(this, arguments);
|
||||
var field = scope.field;
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.title = field.title;
|
||||
scope.showTitle = field.showTitle !== false;
|
||||
|
||||
scope.btn.isHidden = function() {
|
||||
return scope.isHidden();
|
||||
};
|
||||
},
|
||||
|
||||
template:
|
||||
'<button class="btn" ui-show="!isHidden()" name="{{btn.name}}" ui-actions ui-widget-states>' +
|
||||
'<span ng-show="showTitle">{{title}}</span>' +
|
||||
'</button>'
|
||||
});
|
||||
|
||||
})();
|
||||
206
sophal/js/form/form.input.text.js
Normal file
206
sophal/js/form/form.input.text.js
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
/**
|
||||
* The String widget.
|
||||
*/
|
||||
ui.formInput('String', {
|
||||
css: 'string-item',
|
||||
|
||||
init: function(scope) {
|
||||
var field = scope.field;
|
||||
var isReadonly = scope.isReadonly;
|
||||
var trKey = "$t:" + field.name;
|
||||
|
||||
scope.isReadonly = function () {
|
||||
scope.$$readonlyOrig = isReadonly.apply(this, arguments);
|
||||
return (scope.record && scope.record[trKey]) || scope.$$readonlyOrig;
|
||||
};
|
||||
|
||||
scope.format = function (value) {
|
||||
if ((scope.record && scope.record[trKey])) {
|
||||
return scope.record[trKey];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
},
|
||||
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var field = scope.field,
|
||||
regex = field.pattern ? new RegExp(field.pattern, 'i') : null,
|
||||
minSize = +(field.minSize),
|
||||
maxSize = +(field.maxSize);
|
||||
|
||||
scope.validate = function(value) {
|
||||
if (_.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
var length = value.length,
|
||||
valid = true;
|
||||
|
||||
if (minSize) {
|
||||
valid = length >= minSize;
|
||||
}
|
||||
if(valid && maxSize) {
|
||||
valid = length <= maxSize;
|
||||
}
|
||||
if (valid && regex) {
|
||||
valid = regex.test(value);
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
},
|
||||
|
||||
template_readonly: '<input type="text" ng-show="text" tabindex="-1" readonly="readonly" class="display-text" value="{{text}}">'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Email input widget.
|
||||
*/
|
||||
ui.formInput('Email', {
|
||||
|
||||
css: 'email-item',
|
||||
|
||||
metaWidget: true,
|
||||
|
||||
pattern: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
|
||||
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
var pattern = this.pattern;
|
||||
|
||||
scope.validate = function(value) {
|
||||
if(_.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
return pattern.test(value);
|
||||
};
|
||||
},
|
||||
|
||||
template_editable: '<input type="email">',
|
||||
template_readonly: '<a target="_blank" ng-show="text" href="mailto:{{text}}">{{text}}</a>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The URL input widget.
|
||||
*/
|
||||
ui.formInput('Url', {
|
||||
css: 'url-item',
|
||||
metaWidget: true,
|
||||
template_editable: '<input type="url">',
|
||||
template_readonly: '<a target="_blank" ng-show="text" href="{{text}}">{{text}}</a>'
|
||||
});
|
||||
|
||||
/**
|
||||
* The Phone input widget.
|
||||
*/
|
||||
ui.formInput('Phone', 'String', {
|
||||
css: 'phone-item',
|
||||
template_editable: '<input type="tel">'
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The Text input widget.
|
||||
*/
|
||||
ui.formInput('Text', {
|
||||
css: 'text-item',
|
||||
link_editable: function(scope, element, attrs, model) {
|
||||
this._super.apply(this, arguments);
|
||||
var field = scope.field,
|
||||
textarea = element.get(0);
|
||||
|
||||
textarea.rows = parseInt(field.height) || 8;
|
||||
|
||||
//Firefox add one more line
|
||||
if (axelor.browser.mozilla) {
|
||||
textarea.rows -= 1;
|
||||
}
|
||||
|
||||
var field = scope.field,
|
||||
regex = field.pattern ? new RegExp(field.pattern, 'i') : null,
|
||||
minSize = +(field.minSize),
|
||||
maxSize = +(field.maxSize);
|
||||
|
||||
scope.validate = function(value) {
|
||||
if (_.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
var length = value.length,
|
||||
valid = true;
|
||||
|
||||
if (minSize) {
|
||||
valid = length >= minSize;
|
||||
}
|
||||
if(valid && maxSize) {
|
||||
valid = length <= maxSize;
|
||||
}
|
||||
if (valid && regex) {
|
||||
valid = regex.test(value);
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
},
|
||||
template_editable: '<textarea></textarea >',
|
||||
template_readonly: '<pre ng-show="text">{{text}}</pre>'
|
||||
});
|
||||
|
||||
ui.formInput('Password', 'String', {
|
||||
|
||||
css: 'password-item',
|
||||
|
||||
metaWidget: true,
|
||||
|
||||
init: function(scope) {
|
||||
|
||||
scope.password = function() {
|
||||
var value = this.getValue() || "";
|
||||
return _.str.repeat('*', value.length);
|
||||
};
|
||||
},
|
||||
template_readonly: '<input type="password" ng-show="text" tabindex="-1" readonly="readonly" class="display-text" value="{{password()}}"></input>',
|
||||
template_editable: '<input type="password" autocomplete="new-password">'
|
||||
});
|
||||
|
||||
ui.directive('uiTextareaAutoSize', function () {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
if (!element.is('textarea')) return;
|
||||
|
||||
function resize() {
|
||||
var diff = element.outerHeight() - element.innerHeight();
|
||||
element.css('height', 'auto').css('height', element[0].scrollHeight + diff);
|
||||
}
|
||||
|
||||
element.on('focus keyup input', resize);
|
||||
setTimeout(resize);
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
602
sophal/js/form/form.layout.js
Normal file
602
sophal/js/form/form.layout.js
Normal file
@ -0,0 +1,602 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* jshint newcap: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
function TableLayout(items, attrs, $scope, $compile) {
|
||||
|
||||
var colWidths = attrs.widths,
|
||||
numCols = +attrs.cols || 4,
|
||||
curCol = 0,
|
||||
layout = [[]];
|
||||
|
||||
function add(item, label) {
|
||||
|
||||
if (item.is('br')) {
|
||||
curCol = 0;
|
||||
item.hide();
|
||||
return layout.push([]);
|
||||
}
|
||||
|
||||
var row = _.last(layout),
|
||||
cell = null,
|
||||
colspan = +item.attr('x-colspan') || 1,
|
||||
rowspan = +item.attr('x-rowspan') || 1;
|
||||
|
||||
if (curCol + colspan >= numCols + 1) {
|
||||
curCol = 0, row = [];
|
||||
layout.push(row);
|
||||
}
|
||||
|
||||
if (label) {
|
||||
cell = {};
|
||||
cell.elem = label;
|
||||
cell.css = label.attr('x-cell-css');
|
||||
row.push(cell);
|
||||
if (rowspan > 1) cell.rowspan = rowspan;
|
||||
if (colspan > 1) colspan -= 1;
|
||||
curCol += 1;
|
||||
}
|
||||
|
||||
cell = {};
|
||||
cell.elem = item;
|
||||
cell.css = item.attr('x-cell-css');
|
||||
if (colspan > 1) cell.colspan = colspan;
|
||||
if (rowspan > 1) cell.rowspan = rowspan;
|
||||
|
||||
row.push(cell);
|
||||
curCol += colspan;
|
||||
}
|
||||
|
||||
if (colWidths && angular.isString(colWidths)) {
|
||||
colWidths = colWidths.trim().split(/\s*,\s*/);
|
||||
for(var i = 0 ; i < colWidths.length; i++) {
|
||||
var width = colWidths[i];
|
||||
if (/^(\d+)$/.test(width)) width = width + 'px';
|
||||
if (width == '*') width = 'auto';
|
||||
colWidths[i] = width;
|
||||
}
|
||||
}
|
||||
|
||||
items.each(function(){
|
||||
var el = $(this),
|
||||
title = el.attr('x-title'),
|
||||
noTitle = el.attr('x-show-title') == 'false';
|
||||
|
||||
var labelScope = el.data('$scope');
|
||||
if (labelScope) {
|
||||
labelScope = labelScope.$new();
|
||||
}
|
||||
|
||||
if (numCols > 1 && !noTitle && title) {
|
||||
var label = $('<label ui-label></label>').html(title).attr('x-for-widget', el.attr('id')),
|
||||
labelElem = $compile(label)(labelScope || $scope);
|
||||
el.data('label', labelElem);
|
||||
return add(el, labelElem);
|
||||
}
|
||||
add(el);
|
||||
});
|
||||
|
||||
var table = $('<table class="form-layout"></table>');
|
||||
|
||||
function isLabel(cell) {
|
||||
return cell.css === "form-label" || (cell.elem && cell.elem.is('label,.spacer-item'));
|
||||
}
|
||||
|
||||
function computeWidths(row) {
|
||||
if (row.length === 1) return null;
|
||||
var widths = [],
|
||||
labelCols = 0,
|
||||
itemCols = 0,
|
||||
emptyCols = 0;
|
||||
|
||||
_.each(row, function(cell) {
|
||||
if (isLabel(cell)) {
|
||||
labelCols += (cell.colspan || 1);
|
||||
} else {
|
||||
itemCols += (cell.colspan || 1);
|
||||
}
|
||||
});
|
||||
|
||||
emptyCols = numCols - (labelCols + itemCols);
|
||||
|
||||
labelCols += (emptyCols / 2);
|
||||
itemCols += (emptyCols / 2) + (emptyCols % 2);
|
||||
|
||||
var labelWidth = labelCols ? Math.min(50, (12 * labelCols)) / labelCols : 0;
|
||||
var itemWidth = (100 - (labelWidth * labelCols)) / itemCols;
|
||||
|
||||
_.each(row, function(cell, i) {
|
||||
var width = ((isLabel(cell) ? labelWidth : itemWidth) * (cell.colspan || 1));
|
||||
widths[i] = width + "%";
|
||||
});
|
||||
|
||||
return widths;
|
||||
}
|
||||
|
||||
_.each(layout, function(row){
|
||||
var tr = $('<tr></tr>'),
|
||||
numCells = 0,
|
||||
widths = colWidths || computeWidths(row);
|
||||
|
||||
_.each(row, function(cell, i) {
|
||||
var el = $('<td></td>')
|
||||
.addClass(cell.css)
|
||||
.attr('colspan', cell.colspan)
|
||||
.attr('rowspan', cell.rowspan)
|
||||
.append(cell.elem)
|
||||
.appendTo(tr);
|
||||
if (_.isArray(widths) && widths[i]) {
|
||||
el.width(widths[i]);
|
||||
}
|
||||
numCells += cell.colspan || 1;
|
||||
});
|
||||
|
||||
// append remaining cells
|
||||
for (var i = numCells ; i < numCols ; i++) {
|
||||
$('<td></td>').appendTo(tr).width((widths||[])[i]);
|
||||
}
|
||||
|
||||
tr.appendTo(table);
|
||||
});
|
||||
|
||||
return table;
|
||||
} //- TableLayout
|
||||
|
||||
|
||||
ui.directive('uiTableLayout', ['$compile', function($compile) {
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
var elem = attrs.layoutSelector ? element.find(attrs.layoutSelector) : element;
|
||||
var items = elem.children();
|
||||
|
||||
var layout = TableLayout(items, attrs, scope, $compile);
|
||||
var brTags = element.children('br:hidden'); // detach all the <br> tags
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
brTags.remove();
|
||||
});
|
||||
|
||||
elem.append(layout);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
function PanelLayout(items, attrs, $scope, $compile) {
|
||||
|
||||
var stacked = attrs.stacked || false,
|
||||
flexbox = attrs.flexbox || false,
|
||||
numCols = 12,
|
||||
numSpan = +(attrs.itemSpan) || 6,
|
||||
curCol = 0,
|
||||
canAddRow = !stacked && !flexbox,
|
||||
rowClass = flexbox ? 'panel-flex' : 'row-fluid',
|
||||
cellClass = flexbox ? 'flex' : 'span',
|
||||
layout = [$('<div>').addClass(rowClass)];
|
||||
|
||||
function add(item, label) {
|
||||
var row = _.last(layout),
|
||||
cell = $('<div>'),
|
||||
span = +item.attr('x-colspan') || numSpan,
|
||||
offset = +item.attr('x-coloffset') || 0;
|
||||
|
||||
span = Math.min(span, numCols);
|
||||
if (stacked) {
|
||||
span = 0;
|
||||
}
|
||||
|
||||
if (curCol + (span + offset) >= numCols + 1 && canAddRow) {
|
||||
curCol = 0, row = $('<div>').addClass(rowClass);
|
||||
layout.push(row);
|
||||
}
|
||||
if (label) {
|
||||
label.appendTo(cell);
|
||||
row.addClass('has-labels');
|
||||
}
|
||||
|
||||
cell.addClass(item.attr('x-cell-css'));
|
||||
|
||||
if (span) {
|
||||
cell.addClass(cellClass + span);
|
||||
}
|
||||
if (offset) {
|
||||
cell.addClass('offset' + offset);
|
||||
}
|
||||
|
||||
cell.append(item);
|
||||
cell.appendTo(row);
|
||||
|
||||
curCol += (span + offset);
|
||||
}
|
||||
|
||||
items.each(function (item, i) {
|
||||
var el = $(this),
|
||||
title = el.attr('x-title'),
|
||||
noTitle = el.attr('x-show-title') == 'false';
|
||||
|
||||
var labelScope = el.data('$scope');
|
||||
if (labelScope) {
|
||||
labelScope = labelScope.$new();
|
||||
}
|
||||
|
||||
if (!noTitle && title) {
|
||||
var label = $('<label ui-label></label>').html(title).attr('x-for-widget', el.attr('id')),
|
||||
labelElem = $compile(label)(labelScope || $scope);
|
||||
el.data('label', labelElem);
|
||||
return add(el, labelElem);
|
||||
}
|
||||
add(el);
|
||||
});
|
||||
|
||||
var container = $('<div class="panel-layout"></div>').append(layout);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
ui.directive('uiPanelLayout', ['$compile', function($compile) {
|
||||
|
||||
return {
|
||||
priority: 1000,
|
||||
link: function(scope, element, attrs) {
|
||||
var elem = element.children('[ui-transclude]:first');
|
||||
var items = elem.children();
|
||||
var layout = PanelLayout(items, attrs, scope, $compile);
|
||||
elem.append(layout);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
function BarLayout(items, attrs, $scope, $compile) {
|
||||
|
||||
var main = $('<div class="bar-main">');
|
||||
var side = $('<div class="bar-side">');
|
||||
var wrap = $('<div class="bar-wrap">').appendTo(main);
|
||||
|
||||
|
||||
items.each(function(item, i) {
|
||||
var elem = $(this);
|
||||
var prop = elem.scope().field || {};
|
||||
if (elem.attr('x-sidebar')) {
|
||||
elem.appendTo(side);
|
||||
} else {
|
||||
elem.appendTo(wrap);
|
||||
}
|
||||
if (prop.attached) {
|
||||
elem.addClass("attached");
|
||||
}
|
||||
});
|
||||
|
||||
var row = $('<div class="bar-container">').append(main);
|
||||
|
||||
if (side && axelor.device.small) {
|
||||
side.children().first().prependTo(wrap);
|
||||
side.children().appendTo(wrap);
|
||||
}
|
||||
|
||||
wrap.children('[ui-panel-mail]').appendTo(main);
|
||||
|
||||
if (side.children().length > 0) {
|
||||
side.appendTo(row.addClass('has-side'));
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
ui.directive('uiBarLayout', ['$compile', function($compile) {
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
var items = element.children();
|
||||
var layout = BarLayout(items, attrs, scope, $compile);
|
||||
var schema = scope.schema || {};
|
||||
var css = null;
|
||||
|
||||
scope._isPanelForm = true;
|
||||
|
||||
element.append(layout);
|
||||
element.addClass('bar-layout');
|
||||
|
||||
if (element.has('[x-sidebar]').length === 0) {
|
||||
css = "mid";
|
||||
}
|
||||
if (element.is('form') && ["mini", "mid", "large"].indexOf(schema.width) > -1) {
|
||||
css = scope.schema.width;
|
||||
}
|
||||
if (css) {
|
||||
element.addClass(css + '-form');
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiPanelViewer', function () {
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
var field = scope.field;
|
||||
var isRelational = /-to-one$/.test(field.type);
|
||||
if (isRelational) {
|
||||
Object.defineProperty(scope, 'record', {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return (scope.$parent.record||{})[field.name];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiPanelEditor', ['$compile', 'ActionService', function($compile, ActionService) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
var field = scope.field;
|
||||
var editor = field.editor;
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
function applyAttrs(item, level) {
|
||||
if (item.showTitle === undefined && !item.items) {
|
||||
item.showTitle = (editor.widgetAttrs||{}).showTitles !== "false";
|
||||
}
|
||||
if (!item.showTitle && !item.items) {
|
||||
var itemField = (editor.fields||scope.fields||{})[item.name] || {};
|
||||
item.placeholder = item.placeholder || itemField.placeholder || item.title || itemField.title || item.autoTitle;
|
||||
}
|
||||
if (editor.itemSpan && !item.colSpan && !level) {
|
||||
item.colSpan = editor.itemSpan;
|
||||
}
|
||||
if (item.items) {
|
||||
_.map(item.items, function (x) {
|
||||
applyAttrs(x, (level||0) + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var items = editor.items || [];
|
||||
var hasColSpan = false;
|
||||
var widths = _.map(items, function (item) {
|
||||
applyAttrs(item);
|
||||
if (item.colSpan) {
|
||||
hasColSpan = true;
|
||||
}
|
||||
var width = item.width || (item.widgetAttrs||{}).width;
|
||||
return width ? width : (item.widget === 'toggle' ? 24 : '*');
|
||||
});
|
||||
|
||||
var schema = hasColSpan ? {
|
||||
cols: 12,
|
||||
items: items
|
||||
} : {
|
||||
cols: items.length,
|
||||
colWidths: widths,
|
||||
items: items
|
||||
};
|
||||
|
||||
if (editor.layout !== 'table') {
|
||||
schema = {
|
||||
items: [{
|
||||
type: 'panel',
|
||||
items: items,
|
||||
flexbox: editor.flexbox
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
scope.fields = editor.fields || scope.fields;
|
||||
|
||||
var form = ui.formBuild(scope, schema, scope.fields);
|
||||
var isRelational = /-to-one$/.test(field.type);
|
||||
|
||||
if (isRelational) {
|
||||
Object.defineProperty(scope, 'record', {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return (scope.$parent.record||{})[field.name];
|
||||
},
|
||||
set: function (value) {
|
||||
scope.setValue(value, true);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(scope, '$$original', {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return (scope.$parent.$$original||{})[field.name];
|
||||
},
|
||||
set: function (value) {}
|
||||
});
|
||||
scope.$$setEditorValue = function (value, fireOnChange) {
|
||||
scope.setValue(value, fireOnChange === undefined ? true: fireOnChange);
|
||||
};
|
||||
}
|
||||
|
||||
if (field.target) {
|
||||
scope.getDummyValues = function() {
|
||||
if (!scope.record) return {};
|
||||
var fields = _.keys(scope.fields);
|
||||
var extra = _.chain(scope.fields_view)
|
||||
.filter(function(f) { return f.name && f.name[0] === '$' && !_.contains(fields, f.name); })
|
||||
.filter(function(f) { return ['$changed', '$editorModel', '$version', '$fetched', '$fetchedRelated'].indexOf(f) === -1; })
|
||||
.pluck('name')
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
if (scope._model === 'com.axelor.auth.db.User') {
|
||||
extra = extra.filter(function (n) {
|
||||
return ['change', 'oldPassword', 'newPassword', 'chkPassword'].indexOf(n) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
return _.pick(scope.record, extra);
|
||||
};
|
||||
|
||||
scope.getContext = function () {
|
||||
var context = _.extend({}, scope.record);
|
||||
var dummy = scope.getDummyValues();
|
||||
context._model = scope._model;
|
||||
context._parent = scope.$parent.getContext();
|
||||
return ui.prepareContext(scope._model, context, dummy);
|
||||
};
|
||||
|
||||
scope.$broadcastRecordChange = function () {
|
||||
scope.$broadcast("on:record-change", scope.record);
|
||||
};
|
||||
|
||||
scope.$on('on:before-save', function watchParentRecord() {
|
||||
var dummyValues = scope.getDummyValues();
|
||||
var watcher = scope.$watch('$parent.record', function (record, old) {
|
||||
if (record === old) return;
|
||||
var value = (record||{})[field.name];
|
||||
if (value && dummyValues) {
|
||||
value = _.extend(value, dummyValues);
|
||||
}
|
||||
dummyValues = null;
|
||||
watcher();
|
||||
});
|
||||
});
|
||||
|
||||
scope.$watch('record', function (record, old) {
|
||||
if (record && !record.$editorModel) {
|
||||
record.$editorModel = scope._model;
|
||||
}
|
||||
});
|
||||
|
||||
// make sure to fetch missing values
|
||||
var fetchMissing = function (value) {
|
||||
var ds = scope._dataSource;
|
||||
var record = scope.record;
|
||||
if (value <= 0 || !value || record.$fetched || record.$fetchedRelated) {
|
||||
return;
|
||||
}
|
||||
var missing = _.filter(_.keys(editor.fields), function (name) {
|
||||
if (!record) return false;
|
||||
if (name.indexOf('.') === -1) {
|
||||
return !record.hasOwnProperty(name);
|
||||
}
|
||||
var path = name.split('.');
|
||||
var nested = record;
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
nested = nested[path[i]];
|
||||
if (!nested) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !nested.hasOwnProperty(path[path.length - 1]);
|
||||
});
|
||||
if (missing.length === 0) {
|
||||
return;
|
||||
}
|
||||
record.$fetchedRelated = true;
|
||||
return ds.read(value, {fields: missing}).success(function(rec) {
|
||||
var values = _.pick(rec, missing);
|
||||
record = _.extend(record, values);
|
||||
});
|
||||
};
|
||||
// make sure to trigger record-change with proper record data
|
||||
var watchRun = function (value, old) {
|
||||
if (value && value !== old) {
|
||||
value.$changed = true;
|
||||
value.version = _.isNumber(value.version) ? value.version : value.$version;
|
||||
}
|
||||
if (value) {
|
||||
// parent form's getContext will check this to prepare context for editor
|
||||
// to have proper selection flags in nest o2m/m2m
|
||||
value.$editorModel = scope._model;
|
||||
fetchMissing(value.id);
|
||||
}
|
||||
scope.$applyAsync(function () {
|
||||
scope.$broadcast("on:record-change", value || {}, true);
|
||||
});
|
||||
// if it's an o2m editor, make sure to update values
|
||||
if (scope.$itemsChanged) {
|
||||
scope.$itemsChanged();
|
||||
}
|
||||
};
|
||||
scope.$watch('record', _.debounce(watchRun, 100), true);
|
||||
scope.$timeout(function () {
|
||||
scope.$broadcast("on:record-change", scope.record || {}, true);
|
||||
});
|
||||
}
|
||||
|
||||
form = $compile(form)(scope);
|
||||
form.removeClass('mid-form mini-form').children('div.row').removeClass('row').addClass('row-fluid');
|
||||
element.append(form);
|
||||
|
||||
if (field.target) {
|
||||
var handler = null;
|
||||
if (editor.onNew) {
|
||||
schema.onNew = editor.onNew;
|
||||
form.data('$editorForm', form);
|
||||
handler = ActionService.handler(scope, form, {
|
||||
action: editor.onNew
|
||||
});
|
||||
}
|
||||
scope.$watch('record.id', function editorRecordIdWatch(value, old) {
|
||||
if (!value && handler) {
|
||||
handler.onNew();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.isValid = function () {
|
||||
return scope.form && scope.form.$valid;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
scope.$watch(function editorValidWatch() {
|
||||
if (isRelational && editor.showOnNew === false && !scope.canShowEditor()) {
|
||||
return;
|
||||
}
|
||||
var valid = scope.isValid();
|
||||
if (!valid && !field.jsonFields && !scope.$parent.isRequired() && isEmpty(scope.record)) {
|
||||
var errors = (scope.form || {}).$error || {};
|
||||
valid = !errors.valid;
|
||||
}
|
||||
if (scope.setValidity) {
|
||||
scope.setValidity('valid', valid, scope.record);
|
||||
element.toggleClass('nested-not-required', valid);
|
||||
} else {
|
||||
scope.$parent.form.$setValidity('valid', valid, scope.form);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function () {
|
||||
if (scope.setValidity) {
|
||||
scope.setValidity('valid', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1209
sophal/js/form/form.mail.js
Normal file
1209
sophal/js/form/form.mail.js
Normal file
File diff suppressed because it is too large
Load Diff
561
sophal/js/form/form.relational.base.js
Normal file
561
sophal/js/form/form.relational.base.js
Normal file
@ -0,0 +1,561 @@
|
||||
/*
|
||||
* 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.RefFieldCtrl = RefFieldCtrl;
|
||||
|
||||
function RefFieldCtrl($scope, $element, DataSource, ViewService, initCallback) {
|
||||
|
||||
var field = $scope.getViewDef($element),
|
||||
params = {
|
||||
model: field.target || $element.attr('x-target'),
|
||||
views: field.views || {},
|
||||
domain: field.domain,
|
||||
context: field.context
|
||||
},
|
||||
views = {};
|
||||
|
||||
if (field.jsonTarget) {
|
||||
params.context = _.extend({}, params.context, { jsonModel: field.jsonTarget });
|
||||
}
|
||||
|
||||
if (!$element.is('fieldset')) {
|
||||
|
||||
_.each(field.views, function(view){
|
||||
views[view.type] = view;
|
||||
});
|
||||
|
||||
var formView = null,
|
||||
gridView = null,
|
||||
summaryView = null;
|
||||
|
||||
if (field.summaryView === "" || field.summaryView === "true") {
|
||||
summaryView = views.form;
|
||||
}
|
||||
|
||||
if (field.gridView) {
|
||||
gridView = {
|
||||
type: 'grid',
|
||||
name: field.gridView
|
||||
};
|
||||
}
|
||||
if (field.formView) {
|
||||
formView = {
|
||||
type: 'form',
|
||||
name: field.formView
|
||||
};
|
||||
}
|
||||
|
||||
if (field.summaryView === "" || field.summaryView === "true") {
|
||||
summaryView = views.form || formView || { type: 'form' };
|
||||
} else if (field.summaryView) {
|
||||
summaryView = {
|
||||
type: "form",
|
||||
name: field.summaryView
|
||||
};
|
||||
}
|
||||
|
||||
views.form = formView || views.form;
|
||||
views.grid = gridView || views.grid;
|
||||
params.summaryView = angular.copy(summaryView);
|
||||
params.summaryViewDefault = params.summaryView || views.form;
|
||||
|
||||
params.views = _.compact([views.grid, views.form]);
|
||||
$scope._viewParams = params;
|
||||
}
|
||||
|
||||
ui.ViewCtrl($scope, DataSource, ViewService);
|
||||
|
||||
$scope.ngModel = null;
|
||||
$scope.editorCanSave = true;
|
||||
$scope.editorCanReload = field.canReload;
|
||||
|
||||
if (initCallback) {
|
||||
initCallback.call(this);
|
||||
}
|
||||
|
||||
var editor = null;
|
||||
var selector = null;
|
||||
var embedded = null;
|
||||
|
||||
$scope.createNestedEditor = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/Hide the nested editor according to the show parameter, if
|
||||
* undefined then toggle.
|
||||
*
|
||||
*/
|
||||
$scope.showNestedEditor = function showNestedEditor(show) {
|
||||
if (!params.summaryView) {
|
||||
return;
|
||||
}
|
||||
if (embedded === null) {
|
||||
embedded = $scope.createNestedEditor();
|
||||
}
|
||||
var es = embedded.data('$scope');
|
||||
if (es !== null) {
|
||||
es.visible = (show === undefined ? !es.visible : show);
|
||||
embedded.toggle(es.visible);
|
||||
}
|
||||
return embedded;
|
||||
};
|
||||
|
||||
$scope.showPopupEditor = function(record) {
|
||||
if (!record && this.isReadonly()) {
|
||||
return;
|
||||
}
|
||||
if (editor == null) {
|
||||
editor = ViewService.compile('<div ui-editor-popup></div>')($scope);
|
||||
editor.data('$target', $element);
|
||||
}
|
||||
|
||||
var popup = editor.isolateScope();
|
||||
popup.show(record);
|
||||
popup._afterPopupShow = function() {
|
||||
if (record == null) {
|
||||
popup.$broadcast("on:new");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function _showEditor(record) {
|
||||
|
||||
if (!$scope._isPopup && field.editWindow === "blank" && record && record.id > 0) {
|
||||
var checkVersion = "" + axelor.config["view.form.check-version"];
|
||||
var context = ($scope.selectedTab || {}).context || {};
|
||||
if (context.__check_version !== undefined) {
|
||||
checkVersion = "" + context.__check_version;
|
||||
}
|
||||
var tab = {
|
||||
action: _.uniqueId('$act'),
|
||||
title: field.title,
|
||||
model: field.target,
|
||||
recordId: record.id,
|
||||
views: [{
|
||||
type: 'form',
|
||||
name: field.formView
|
||||
}, {
|
||||
type: 'grid',
|
||||
name: field.gridView
|
||||
}]
|
||||
};
|
||||
if (checkVersion) {
|
||||
tab.context = { __check_version: checkVersion };
|
||||
}
|
||||
|
||||
return $scope.$root.openTab(tab);
|
||||
}
|
||||
|
||||
if ($scope.editorCanReload && record && record.id) {
|
||||
var parent = $scope.$parent;
|
||||
if (parent && parent.canSave()) {
|
||||
var opts = {
|
||||
callOnSave: field.callOnSave
|
||||
};
|
||||
return parent.onSave(opts).then(function(){
|
||||
$scope.showPopupEditor(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
return $scope.showPopupEditor(record);
|
||||
}
|
||||
|
||||
$scope.showEditor = function(record) {
|
||||
var perm = record ? "read" : "create";
|
||||
var id = (record||{}).id;
|
||||
|
||||
if (perm === 'read' && (!id || id < 0)) {
|
||||
return _showEditor(record);
|
||||
}
|
||||
return $scope.isPermitted(perm, record, function(){
|
||||
_showEditor(record);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.parentReload = function() {
|
||||
var parent = $scope.$parent;
|
||||
if (parent) {
|
||||
parent.reload();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showSelector = function() {
|
||||
if (this.isReadonly()) {
|
||||
return;
|
||||
}
|
||||
function doShow() {
|
||||
if (selector == null) {
|
||||
selector = $('<div ui-selector-popup></div>').attr('x-select-mode', $scope.selectMode || "multi");
|
||||
selector = ViewService.compile(selector)($scope);
|
||||
selector.data('$target', $element);
|
||||
}
|
||||
var popup = selector.isolateScope();
|
||||
popup._domain = $scope._domain; // make sure that popup uses my domain (#1233)
|
||||
popup.show();
|
||||
}
|
||||
|
||||
var onSelect = this.$events.onSelect;
|
||||
if (onSelect) {
|
||||
onSelect().then(function(){
|
||||
doShow();
|
||||
});
|
||||
} else {
|
||||
doShow();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on("on:edit", function(record){
|
||||
var domain = ($scope.field||field).domain;
|
||||
var context = ($scope.field||field).context;
|
||||
if (domain !== undefined) $scope._domain = domain;
|
||||
if (context !== undefined) $scope._context = context;
|
||||
});
|
||||
|
||||
$scope.setDomain = function(domain, context) {
|
||||
if (domain !== undefined) $scope._domain = domain;
|
||||
if (context !== undefined) $scope._context = context;
|
||||
};
|
||||
|
||||
$scope.getDomain = function() {
|
||||
return {
|
||||
_domain: $scope._domain,
|
||||
_context: $scope._context
|
||||
};
|
||||
};
|
||||
|
||||
var fetchDS = (function () {
|
||||
var fds = null;
|
||||
return function () {
|
||||
if (fds) return fds;
|
||||
var ds = $scope._dataSource;
|
||||
return fds = DataSource.create(ds._model, {
|
||||
domain: ds._domain,
|
||||
context: ds._context
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
$scope.fetchData = function(value, success) {
|
||||
|
||||
var records = $.makeArray(value),
|
||||
ids = [];
|
||||
|
||||
_.each(records, function(item) {
|
||||
if (_.isNumber(item)) {
|
||||
return ids.push(item);
|
||||
}
|
||||
if (_.isNumber(item.id) && item.id > 0 &&
|
||||
_.isUndefined(item.version) &&
|
||||
_.isUndefined(item.$fetched)) {
|
||||
return ids.push(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (ids.length === 0) {
|
||||
return success(value);
|
||||
}
|
||||
|
||||
var fields = $scope.selectFields();
|
||||
|
||||
function doFetch(view) {
|
||||
var domain = "self.id in (:_field_ids)";
|
||||
var context = _.pick($scope.getContext(), ['id', '_model']);
|
||||
|
||||
var sortBy = view.sortBy || view.orderBy;
|
||||
if (sortBy) {
|
||||
sortBy = sortBy.split(",");
|
||||
}
|
||||
if (view.canMove && fields.indexOf('sequence') === -1) {
|
||||
fields.push('sequence');
|
||||
}
|
||||
|
||||
context._field = field.name;
|
||||
context._field_ids = ids;
|
||||
|
||||
return fetchDS().search({
|
||||
fields: fields,
|
||||
sortBy: fetchDS()._sortBy || sortBy,
|
||||
archived: true,
|
||||
limit: -1,
|
||||
domain: domain,
|
||||
context: context
|
||||
}).success(function(records, page){
|
||||
// only edited records should have version property
|
||||
var items = _.map(records, function(item){
|
||||
item.$version = item.version;
|
||||
item.$fetched = false;
|
||||
delete item.version;
|
||||
return item;
|
||||
});
|
||||
success(items, page);
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.isHidden()) {
|
||||
return doFetch($scope.view || {});
|
||||
}
|
||||
|
||||
return $scope._viewPromise.then(function(view) {
|
||||
return doFetch(view || {});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.fetchSelection = function(request, response) {
|
||||
var fn = fetchSelection.bind(this);
|
||||
var onSelect = this.$events.onSelect;
|
||||
if (onSelect) {
|
||||
return onSelect(true).then(function() {
|
||||
return fn(request, response);
|
||||
});
|
||||
}
|
||||
return fn(request, response);
|
||||
};
|
||||
|
||||
function fetchSelection(request, response) {
|
||||
|
||||
/* jshint validthis: true */
|
||||
|
||||
var field = this.field;
|
||||
var nameField = field.targetName || 'id',
|
||||
fields = field.targetSearch || [],
|
||||
filter = {},
|
||||
limit = field.limit || (axelor.device.small ? 6 : 10),
|
||||
sortBy = field.orderBy;
|
||||
|
||||
fields = ["id", nameField].concat(fields);
|
||||
fields = _.chain(fields).compact().unique().value();
|
||||
|
||||
_.each(fields, function(name){
|
||||
if (name !== "id" && request.term) {
|
||||
filter[name] = request.term;
|
||||
}
|
||||
});
|
||||
|
||||
var domain = this._domain,
|
||||
context = this._context;
|
||||
|
||||
if (domain !== undefined && this.getContext) {
|
||||
context = _.extend({}, context, this.getContext());
|
||||
}
|
||||
|
||||
if (sortBy) {
|
||||
sortBy = sortBy.split(",");
|
||||
}
|
||||
|
||||
var params = {
|
||||
filter: filter,
|
||||
fields: fields,
|
||||
sortBy: sortBy,
|
||||
limit: limit
|
||||
};
|
||||
|
||||
if (domain !== undefined) {
|
||||
params.domain = domain;
|
||||
params.context = context;
|
||||
}
|
||||
|
||||
fetchDS().search(params).success(function(records, page){
|
||||
var trKey = '$t:' + nameField;
|
||||
var items = _.map(records, function(record) {
|
||||
return {
|
||||
label: record[trKey] || record[nameField],
|
||||
value: record
|
||||
};
|
||||
});
|
||||
response(items, page);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.createOnTheFly = function (term, popup, onSaveCallback) {
|
||||
|
||||
var field = $scope.field;
|
||||
var targetFields = null;
|
||||
var requiredFields = (field.create||"").split(/,\s*/);
|
||||
|
||||
function createItem(fields, term, popup) {
|
||||
var ds = $scope._dataSource,
|
||||
data = { $forceDirty: true }, missing = false;
|
||||
|
||||
_.each(fields, function(field) {
|
||||
if (field.name === "name") return data["name"] = term;
|
||||
if (field.name === "code") return data["code"] = term;
|
||||
if (field.nameColumn) return data[field.name] = term;
|
||||
if (requiredFields.indexOf(field.name) > -1) {
|
||||
return data[field.name] = term;
|
||||
}
|
||||
if (field.required) {
|
||||
missing = true;
|
||||
}
|
||||
});
|
||||
if (popup || missing || _.isEmpty(data)) {
|
||||
return $scope.showPopupEditor(data);
|
||||
}
|
||||
return ds.save(data).success(onSaveCallback);
|
||||
}
|
||||
|
||||
|
||||
if (targetFields) {
|
||||
return createItem(targetFields, term, popup);
|
||||
}
|
||||
|
||||
return $scope.loadView("form").success(function(fields, view){
|
||||
targetFields = fields;
|
||||
return createItem(fields, term, popup);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.attachTagEditor = function attachTagEditor(scope, element, attrs) {
|
||||
|
||||
var field = scope.field;
|
||||
var input = null;
|
||||
|
||||
if (!field.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onTagEdit(e, item) {
|
||||
|
||||
var elem = $(e.target);
|
||||
var field = scope.field;
|
||||
var value = item[field.targetName];
|
||||
|
||||
function onKeyDown(e) {
|
||||
|
||||
// enter key
|
||||
if (e.keyCode === 13) {
|
||||
item[field.targetName] = input.val();
|
||||
saveAndSelect(item);
|
||||
hideEditor();
|
||||
}
|
||||
|
||||
// escape
|
||||
if (e.keyCode === 27) {
|
||||
hideEditor();
|
||||
}
|
||||
}
|
||||
|
||||
function hideEditor(forceSave) {
|
||||
|
||||
$(document).off('mousedown.tag-editor');
|
||||
$(input).off('keydown.tag-editor').hide();
|
||||
|
||||
if (forceSave && value !== input.val()) {
|
||||
item[field.targetName] = input.val();
|
||||
saveAndSelect(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (input === null) {
|
||||
input = $('<input class="tag-editor" type="text">').appendTo(element);
|
||||
}
|
||||
|
||||
input.val(value)
|
||||
.width(element.width() - 6)
|
||||
.show().focus()
|
||||
.position({
|
||||
my: 'left top',
|
||||
at: 'left+3 top+3',
|
||||
of: element
|
||||
});
|
||||
|
||||
$(input).on('keydown.tag-editor', onKeyDown);
|
||||
$(document).on('mousedown.tag-editor', function (e) {
|
||||
if (!input.is(e.target)) {
|
||||
hideEditor(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveAndSelect(record) {
|
||||
var ds = scope._dataSource;
|
||||
var data = _.extend({}, record, {
|
||||
version: record.version || record.$version
|
||||
});
|
||||
ds.save(data).success(function (rec) {
|
||||
scope.select(rec);
|
||||
});
|
||||
}
|
||||
|
||||
scope.onTagEdit = onTagEdit;
|
||||
};
|
||||
|
||||
$scope.canSelect = function() {
|
||||
var canSelect = $scope.attr('canSelect');
|
||||
if (canSelect !== undefined) return canSelect;
|
||||
if ($scope.selectEnable !== undefined) return $scope.selectEnable;
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.canNew = function() {
|
||||
return $scope.attr('canNew') !== false;
|
||||
};
|
||||
|
||||
$scope.canEdit = function() {
|
||||
return !$scope.isReadonly();
|
||||
};
|
||||
|
||||
$scope.canView = function() {
|
||||
return $scope.attr('canView') !== false;
|
||||
};
|
||||
|
||||
$scope.canRemove = function() {
|
||||
return $scope.attr('canRemove') !== false;
|
||||
};
|
||||
|
||||
$scope.select = function(value) {
|
||||
|
||||
};
|
||||
|
||||
$scope.onNew = function() {
|
||||
$scope.showEditor(null);
|
||||
};
|
||||
|
||||
$scope.onEdit = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onSelect = function() {
|
||||
$scope.showSelector();
|
||||
};
|
||||
|
||||
$scope.onRemove = function() {
|
||||
|
||||
};
|
||||
|
||||
var hasPermission = $scope.hasPermission;
|
||||
$scope.hasPermission = function(perm) {
|
||||
if (hasPermission && !hasPermission.apply($scope, arguments)) {
|
||||
return false;
|
||||
}
|
||||
if (!field.perms) return true;
|
||||
var perms = field.perms;
|
||||
var permitted = perms[perm];
|
||||
if (!permitted) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
||||
1323
sophal/js/form/form.relational.multiple.js
Normal file
1323
sophal/js/form/form.relational.multiple.js
Normal file
File diff suppressed because it is too large
Load Diff
400
sophal/js/form/form.relational.nested.js
Normal file
400
sophal/js/form/form.relational.nested.js
Normal file
@ -0,0 +1,400 @@
|
||||
/*
|
||||
* 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 NestedForm = {
|
||||
scope: true,
|
||||
controller: [ '$scope', '$element', function($scope, $element) {
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
};
|
||||
|
||||
$scope.$$forceWatch = false;
|
||||
$scope.$$forceCounter = false;
|
||||
|
||||
$scope.$setForceWatch = function () {
|
||||
$scope.$$forceWatch = true;
|
||||
$scope.$$forceCounter = true;
|
||||
};
|
||||
|
||||
$scope.registerNested($scope);
|
||||
$scope.show();
|
||||
}],
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
},
|
||||
template: '<div ui-view-form x-handler="this"></div>'
|
||||
};
|
||||
|
||||
ui.EmbeddedEditorCtrl = EmbeddedEditorCtrl;
|
||||
ui.EmbeddedEditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
||||
|
||||
function EmbeddedEditorCtrl($scope, $element, DataSource, ViewService) {
|
||||
|
||||
var params = angular.copy($scope._viewParams);
|
||||
|
||||
params.views = _.compact([params.summaryView || params.summaryViewDefault]);
|
||||
$scope._viewParams = params;
|
||||
|
||||
ui.ViewCtrl($scope, DataSource, ViewService);
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
$scope.visible = false;
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
var originalEdit = $scope.edit;
|
||||
|
||||
function doEdit(record) {
|
||||
if (record && record.id > 0 && !record.$fetched) {
|
||||
$scope.doRead(record.id).success(function(record){
|
||||
originalEdit(record);
|
||||
});
|
||||
} else {
|
||||
originalEdit(record);
|
||||
}
|
||||
}
|
||||
|
||||
function doClose() {
|
||||
if ($scope.isDetailView) {
|
||||
$scope.edit($scope.getSelectedRecord());
|
||||
return;
|
||||
}
|
||||
$scope.edit(null);
|
||||
$scope.waitForActions(function () {
|
||||
$scope.visible = false;
|
||||
$element.hide();
|
||||
$element.data('$rel').show();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.edit = function(record) {
|
||||
doEdit(record);
|
||||
$scope.setEditable(!$scope.$parent.$$readonly);
|
||||
};
|
||||
|
||||
$scope.onClose = function() {
|
||||
$scope.onClear();
|
||||
doClose();
|
||||
};
|
||||
|
||||
$scope.onOK = function() {
|
||||
if (!$scope.isValid()) {
|
||||
return;
|
||||
}
|
||||
var record = $scope.record;
|
||||
if (record) record.$fetched = true;
|
||||
|
||||
var event = $scope.$broadcast('on:before-save', record);
|
||||
if (event.defaultPrevented) {
|
||||
if (event.error) {
|
||||
return axelor.dialogs.error(event.error);
|
||||
}
|
||||
}
|
||||
$scope.waitForActions(function () {
|
||||
$scope.select($scope.record);
|
||||
$scope.waitForActions(doClose);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onAdd = function() {
|
||||
if (!$scope.isValid() || !$scope.record) {
|
||||
return;
|
||||
}
|
||||
|
||||
var record = $scope.record;
|
||||
record.id = null;
|
||||
record.version = null;
|
||||
record.$version = null;
|
||||
|
||||
$scope.onClear();
|
||||
|
||||
function doSelect(rec) {
|
||||
if (rec) {
|
||||
$scope.select(rec);
|
||||
}
|
||||
return doEdit(rec);
|
||||
}
|
||||
|
||||
if (!$scope.editorCanSave) {
|
||||
return doSelect(record);
|
||||
}
|
||||
|
||||
$scope.onSave().then(function (rec) {
|
||||
doSelect(rec);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onClear = function() {
|
||||
if ($scope.$parent.selection) {
|
||||
$scope.$parent.selection.length = 0;
|
||||
}
|
||||
doEdit(null);
|
||||
};
|
||||
|
||||
$scope.canUpdate = function () {
|
||||
return $scope.record && $scope.record.id;
|
||||
};
|
||||
|
||||
function loadSelected() {
|
||||
var record = $scope.getSelectedRecord();
|
||||
if ($scope.isDetailView) {
|
||||
$scope.edit(record);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on('grid:changed', function(event) {
|
||||
loadSelected();
|
||||
});
|
||||
|
||||
$scope.$on('on:edit', function(event, record) {
|
||||
if ($scope.$parent.record === record) {
|
||||
$scope.waitForActions(loadSelected);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$parent.$watch('isReadonly()', function nestedReadonlyWatch(readonly, old) {
|
||||
if (readonly === old) return;
|
||||
$scope.setEditable(!readonly);
|
||||
});
|
||||
|
||||
$scope.show();
|
||||
}
|
||||
|
||||
var EmbeddedEditor = {
|
||||
restrict: 'EA',
|
||||
css: 'nested-editor',
|
||||
scope: true,
|
||||
controller: EmbeddedEditorCtrl,
|
||||
link: function (scope, element, attrs) {
|
||||
setTimeout(function () {
|
||||
var prev = element.prev();
|
||||
if (prev.is("[ui-slick-grid]")) {
|
||||
element.zIndex(prev.zIndex() + 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
template:
|
||||
'<fieldset class="form-item-group bordered-box" ui-show="visible">'+
|
||||
'<div ui-view-form x-handler="this"></div>'+
|
||||
'<div class="btn-toolbar pull-right">'+
|
||||
'<button type="button" class="btn btn btn-info" ng-click="onClose()" ng-show="isReadonly()"><span x-translate>Back</span></button> '+
|
||||
'<button type="button" class="btn btn-primary" ng-click="onOK()" ng-show="!isReadonly() && canUpdate()"><span x-translate>OK</span></button>'+
|
||||
'<button type="button" class="btn btn-primary" ng-click="onAdd()" ng-show="!isReadonly() && !canUpdate()"><span x-translate>Add</span></button> '+
|
||||
'<button type="button" class="btn btn-danger" ng-click="onClose()" ng-show="!isReadonly()"><span x-translate>Cancel</span></button> '+
|
||||
'</div>'+
|
||||
'</fieldset>'
|
||||
};
|
||||
|
||||
ui.NestedEditorCtrl = NestedEditorCtrl;
|
||||
ui.NestedEditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
||||
|
||||
function NestedEditorCtrl($scope, $element, DataSource, ViewService) {
|
||||
|
||||
var params = angular.copy($scope._viewParams);
|
||||
|
||||
params.views = _.compact([params.summaryView || params.summaryViewDefault]);
|
||||
$scope._viewParams = params;
|
||||
|
||||
ui.ManyToOneCtrl.call(this, $scope, $element, DataSource, ViewService);
|
||||
|
||||
$scope.nested = null;
|
||||
$scope.registerNested = function(scope) {
|
||||
$scope.nested = scope;
|
||||
|
||||
$scope.$watch("isReadonly()", function nestedReadonlyWatch(readonly) {
|
||||
scope.setEditable(!readonly);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var NestedEditor = {
|
||||
restrict: 'EA',
|
||||
css: 'nested-editor',
|
||||
require: '?ngModel',
|
||||
scope: true,
|
||||
controller: NestedEditorCtrl,
|
||||
link: function(scope, element, attrs, model) {
|
||||
|
||||
function setValidity(nested, valid) {
|
||||
model.$setValidity('valid', nested.isValid());
|
||||
if (scope.setValidity) {
|
||||
scope.setValidity('valid', nested.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
var configure = _.once(function (nested) {
|
||||
|
||||
//FIX: select on M2O doesn't apply to nested editor
|
||||
var unwatchId = scope.$watch(attrs.ngModel + '.id', function nestedRecordIdWatch(id, old){
|
||||
if (id === old) {
|
||||
return;
|
||||
}
|
||||
unwatchId();
|
||||
unwatchId = null;
|
||||
scope.$applyAsync();
|
||||
});
|
||||
|
||||
var unwatchValid = nested.$watch('form.$valid', function nestedValidWatch(valid, old){
|
||||
if (valid === old) {
|
||||
return;
|
||||
}
|
||||
unwatchValid();
|
||||
unwatchValid = null;
|
||||
setValidity(nested, valid);
|
||||
});
|
||||
|
||||
scope.$on("on:check-nested-values", function (e, value) {
|
||||
if (nested && value) {
|
||||
var val = scope.getValue() || {};
|
||||
if (val.$updatedValues === value) {
|
||||
_.extend(nested.record, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var parentAttrs = scope.$parent.field || {};
|
||||
if (parentAttrs.forceWatch) {
|
||||
nested.$$forceWatch = true;
|
||||
}
|
||||
});
|
||||
|
||||
var unwatch = null;
|
||||
var original = null;
|
||||
|
||||
function nestedEdit(record, fireOnLoad) {
|
||||
|
||||
var nested = scope.nested;
|
||||
var counter = 0;
|
||||
|
||||
if (!nested) return;
|
||||
if (unwatch) unwatch();
|
||||
|
||||
original = angular.copy(record);
|
||||
|
||||
unwatch = nested.$watch('record', function nestedRecordWatch(rec, old) {
|
||||
|
||||
if (counter++ === 0 && !nested.$$forceCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ds = nested._dataSource;
|
||||
var name = scope.field.name;
|
||||
|
||||
// don't process default values
|
||||
if (ds.equals(rec, nested.defaultValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isEmpty(rec)) rec = null;
|
||||
if (_.isEmpty(old)) old = null;
|
||||
if (rec == old) {
|
||||
return;
|
||||
}
|
||||
if (rec) {
|
||||
rec.$dirty = !(rec.id > 0 && ds.equals(rec, original));
|
||||
}
|
||||
|
||||
model.$setViewValue(rec);
|
||||
setValidity(nested, nested.isValid());
|
||||
}, true);
|
||||
|
||||
return nested.edit(record, fireOnLoad);
|
||||
}
|
||||
|
||||
scope.ngModel = model;
|
||||
scope.visible = false;
|
||||
|
||||
scope.onClear = function() {
|
||||
scope.$parent.setValue(null, true);
|
||||
scope.$parent.$broadcast('on:new');
|
||||
};
|
||||
|
||||
scope.onClose = function() {
|
||||
scope.$parent._isNestedOpen = false;
|
||||
scope.visible = false;
|
||||
element.hide();
|
||||
};
|
||||
|
||||
scope.canClose = function() {
|
||||
return scope.canToggle() && scope.canSelect();
|
||||
};
|
||||
|
||||
attrs.$observe('title', function(title){
|
||||
scope.title = title;
|
||||
});
|
||||
|
||||
model.$render = function() {
|
||||
var nested = scope.nested,
|
||||
promise = nested._viewPromise,
|
||||
oldValue = model.$viewValue;
|
||||
|
||||
function doRender() {
|
||||
var value = model.$viewValue;
|
||||
if (oldValue !== value) { // prevent unnecessary onLoad
|
||||
return;
|
||||
}
|
||||
if (!value || !value.id || value.$dirty) {
|
||||
return nestedEdit(value, false);
|
||||
}
|
||||
if (value.$fetched && (nested.record||{}).$fetched) return;
|
||||
return nested.doRead(value.id).success(function(record){
|
||||
record.$fetched = true;
|
||||
value.$fetched = true;
|
||||
return nestedEdit(_.extend({}, value, record));
|
||||
});
|
||||
}
|
||||
|
||||
if (nested == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
promise.then(function() {
|
||||
configure(nested);
|
||||
nestedEdit(model.$viewValue, false);
|
||||
scope.waitForActions(doRender, 100);
|
||||
});
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<fieldset class="form-item-group bordered-box" ui-show="visible">'+
|
||||
'<legend>'+
|
||||
'<span ng-bind-html="title"></span> '+
|
||||
'<span class="legend-toolbar" style="display: none;" ng-show="!isReadonly()">'+
|
||||
'<a href="" tabindex="-1" ng-click="onClear()" title="{{\'Clear\' | t}}" ng-show="canShowIcon(\'clear\')"><i class="fa fa-ban"></i></a> '+
|
||||
'<a href="" tabindex="-1" ng-click="onSelect()" title="{{\'Select\' | t}}" ng-show="canShowIcon(\'select\')"><i class="fa fa-search"></i></a> '+
|
||||
'<a href="" tabindex="-1" ng-click="onClose()" title="{{\'Close\' | t}}" ng-show="canClose()"><i class="fa fa-times-circle"></i></a>'+
|
||||
'</span>'+
|
||||
'</legend>'+
|
||||
'<div ui-nested-form></div>'+
|
||||
'</fieldset>'
|
||||
};
|
||||
|
||||
ui.formDirective('uiNestedEditor', NestedEditor);
|
||||
ui.formDirective('uiEmbeddedEditor', EmbeddedEditor);
|
||||
ui.formDirective('uiNestedForm', NestedForm);
|
||||
|
||||
})();
|
||||
1212
sophal/js/form/form.relational.single.js
Normal file
1212
sophal/js/form/form.relational.single.js
Normal file
File diff suppressed because it is too large
Load Diff
483
sophal/js/form/form.widget.js
Normal file
483
sophal/js/form/form.widget.js
Normal file
@ -0,0 +1,483 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
/**
|
||||
* The Form widget.
|
||||
*
|
||||
*/
|
||||
ui.formWidget('Form', {
|
||||
|
||||
priority: 100,
|
||||
|
||||
css: "dynamic-form",
|
||||
|
||||
scope: false,
|
||||
|
||||
compile: function(element, attrs) {
|
||||
|
||||
element.hide();
|
||||
element.find('[x-field],[data-field]').each(function(){
|
||||
|
||||
var elem = $(this),
|
||||
name = elem.attr('x-field') || elem.attr('data-field');
|
||||
|
||||
if (name && elem.attr('ui-button') === undefined) {
|
||||
if (!elem.attr('ng-model')) {
|
||||
elem.attr('ng-model', 'record.' + name);
|
||||
}
|
||||
if (!elem.attr('ng-required')) {
|
||||
// always attache a required validator to make
|
||||
// dynamic `required` attribute change effective
|
||||
elem.attr('ng-required', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ui.formCompile.apply(this, arguments);
|
||||
},
|
||||
|
||||
link: function(scope, element, attrs, controller) {
|
||||
|
||||
element.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
scope.$watch('record', function formRecordWatch(rec, old) {
|
||||
if (element.is(':visible')) {
|
||||
return;
|
||||
}
|
||||
scope.ajaxStop(function() {
|
||||
element.show();
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This directive is used filter $watch on scopes of inactive tabs.
|
||||
*
|
||||
*/
|
||||
ui.directive('uiTabGate', function() {
|
||||
|
||||
return {
|
||||
|
||||
compile: function compile(tElement, tAttrs) {
|
||||
|
||||
return {
|
||||
pre: function preLink(scope, element, attrs) {
|
||||
scope.$watchChecker(function(current) {
|
||||
if (current.$$popupStack.length) return true;
|
||||
if (current.tabSelected === undefined) {
|
||||
return !scope.tab || scope.tab.selected === undefined || scope.tab.selected;
|
||||
}
|
||||
return current.tabSelected;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This directive is used to filter $watch on scopes of hidden forms.
|
||||
*
|
||||
*/
|
||||
ui.directive('uiFormGate', function() {
|
||||
|
||||
return {
|
||||
compile: function compile(tElement, tAttrs) {
|
||||
|
||||
return {
|
||||
pre: function preLink(scope, element, attrs) {
|
||||
var parent = null;
|
||||
scope.$watchChecker(function(current) {
|
||||
if (scope.tabSelected === false) {
|
||||
return false;
|
||||
}
|
||||
if (parent === null) {
|
||||
parent = element.parents('[ui-show]:first');
|
||||
}
|
||||
// hack for hidden nested editors (#2173)
|
||||
if (scope.$$forceWatch) {
|
||||
return true;
|
||||
}
|
||||
return !(parent.hasClass('ui-hide') || parent.hasClass('ui-hide'));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This directive is used to filter $watch on scopes based on some condition.
|
||||
*
|
||||
*/
|
||||
ui.directive('uiWatchIf', ['$parse', function($parse) {
|
||||
|
||||
return {
|
||||
compile: function compile(tElement, tAttrs) {
|
||||
return {
|
||||
pre: function preLink(scope, element, attrs) {
|
||||
var value = false,
|
||||
expression = $parse(attrs.uiWatchIf);
|
||||
|
||||
scope.$watchChecker(function (current) {
|
||||
if (current === scope) {
|
||||
return value = expression(scope);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
function toBoolean(value) {
|
||||
if (value && value.length !== 0) {
|
||||
var v = angular.lowercase("" + value);
|
||||
value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
|
||||
} else {
|
||||
value = false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This directive is used to speedup uiFormGate.
|
||||
*/
|
||||
ui.directive('uiShow', function() {
|
||||
|
||||
return {
|
||||
scope: true, // create new scope to always watch the expression
|
||||
link: function link(scope, element, attrs) {
|
||||
scope.$$shouldWatch = true;
|
||||
scope.$watch(attrs.uiShow, function uiShowWatchAction(value){
|
||||
var val = toBoolean(value);
|
||||
element.css({ display: val ? '' : 'none', opacity: 0 }).toggleClass('ui-hide', !val);
|
||||
if (val) {
|
||||
element.animate({ opacity: 1 }, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This directive is used by view-pane to attach/detach element from DOM tree
|
||||
*/
|
||||
ui.directive('uiAttach', function () {
|
||||
return function (scope, element, attrs) {
|
||||
var parent = null;
|
||||
var detachTimer = null;
|
||||
var uiAttachWatch = function uiAttachWatch(attach) {
|
||||
var result = toBoolean(attach);
|
||||
if (result) {
|
||||
if (parent) {
|
||||
if (detachTimer) {
|
||||
clearTimeout(detachTimer);
|
||||
detachTimer = null;
|
||||
} else {
|
||||
element.appendTo(parent);
|
||||
}
|
||||
parent = null;
|
||||
scope.$broadcast('dom:attach');
|
||||
}
|
||||
} else {
|
||||
parent = element.parent();
|
||||
scope.$broadcast('dom:detach');
|
||||
detachTimer = setTimeout(function () {
|
||||
detachTimer = null;
|
||||
element.detach();
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
uiAttachWatch.uiAttachWatch = true;
|
||||
|
||||
scope.$watch(attrs.uiAttach, uiAttachWatch, true);
|
||||
scope.$on('$destroy', function () {
|
||||
if (detachTimer) {
|
||||
clearTimeout(detachTimer);
|
||||
detachTimer = null;
|
||||
}
|
||||
if (parent) {
|
||||
parent = null;
|
||||
element.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This directive can be used by widget to restore scroll when element is re-attached to DOM tree.
|
||||
*/
|
||||
ui.directive('uiAttachScroll', function () {
|
||||
return function (scope, element, attrs) {
|
||||
setTimeout(function () {
|
||||
var elem = element;
|
||||
var scrollTop = 0;
|
||||
|
||||
if (attrs.uiAttachScroll) {
|
||||
elem = element.find(attrs.uiAttachScroll);
|
||||
}
|
||||
|
||||
elem.on('scroll', function () {
|
||||
scrollTop = this.scrollTop;
|
||||
});
|
||||
|
||||
function resetScroll() {
|
||||
elem.scrollTop(scrollTop);
|
||||
}
|
||||
|
||||
scope.$on('dom:attach', resetScroll);
|
||||
scope.$on('tab:select', resetScroll);
|
||||
}, 300);
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiWidgetStates', ['$parse', '$interpolate', function($parse, $interpolate) {
|
||||
|
||||
function isValid(scope, name) {
|
||||
if (!name) return scope.isValid();
|
||||
var ctrl = scope.form;
|
||||
if (ctrl) {
|
||||
ctrl = ctrl[name];
|
||||
}
|
||||
if (ctrl) {
|
||||
return ctrl.$valid;
|
||||
}
|
||||
}
|
||||
|
||||
function withContext(scope, record) {
|
||||
var values = _.extend({}, scope._context, scope._jsonContext, record);
|
||||
return _.extend(values, {
|
||||
$user: axelor.config['user.login'],
|
||||
$group: axelor.config['user.group'],
|
||||
$userId: axelor.config['user.id'],
|
||||
});
|
||||
}
|
||||
|
||||
function handleCondition(scope, field, attr, condition, negative) {
|
||||
|
||||
if (!condition || _.isBoolean(condition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.$on("on:record-change", function(e, rec, force) {
|
||||
if (field && field.jsonField) {
|
||||
handle(scope.record);
|
||||
} else if (rec === scope.record || force) {
|
||||
handle(rec);
|
||||
}
|
||||
});
|
||||
scope.$on("on:grid-selection-change", function(e, context) {
|
||||
if (field && field.jsonField) return;
|
||||
if (!scope._isDetailsForm) {
|
||||
handle(context);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch("isReadonly()", watcher);
|
||||
scope.$watch("isRequired()", watcher);
|
||||
scope.$watch("isValid()", watcher);
|
||||
|
||||
var expr = $parse(condition);
|
||||
|
||||
function watcher(current, old) {
|
||||
var rec = scope.record;
|
||||
if (rec === undefined && current === old) return;
|
||||
if (rec === undefined && scope.getContext) {
|
||||
rec = scope.getContext();
|
||||
}
|
||||
handle(rec);
|
||||
}
|
||||
|
||||
function handle(rec) {
|
||||
var value;
|
||||
try {
|
||||
value = !!axelor.$eval(scope, expr, withContext(scope, rec));
|
||||
} catch (e) {
|
||||
console.error('FAILED:', condition, e);
|
||||
}
|
||||
// defer attr change to allow field init, see RM-14998
|
||||
scope.$applyAsync(function () {
|
||||
scope.attr(attr, negative ? !value : value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleHilites(scope, field) {
|
||||
if (!field || _.isEmpty(field.hilites)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hilites = field.hilites || [];
|
||||
var exprs = _.map(_.pluck(hilites, 'condition'), function (s) { return $parse(s); });
|
||||
|
||||
function handle(rec) {
|
||||
for (var i = 0; i < hilites.length; i++) {
|
||||
var hilite = hilites[i];
|
||||
var expr = exprs[i];
|
||||
var value = false;
|
||||
try {
|
||||
value = axelor.$eval(scope, expr, withContext(scope, rec));
|
||||
} catch (e) {
|
||||
console.error('FAILED:', hilite, e);
|
||||
}
|
||||
if (value) {
|
||||
return scope.attr('highlight', {
|
||||
hilite: hilite,
|
||||
passed: value
|
||||
});
|
||||
}
|
||||
}
|
||||
return scope.attr('highlight', {});
|
||||
}
|
||||
|
||||
scope.$on("on:record-change", function(e, rec) {
|
||||
if (rec === scope.record) {
|
||||
handle(rec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleBind(scope, field) {
|
||||
|
||||
if (!field.bind || !field.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var expr = $interpolate(field.bind);
|
||||
var last = null;
|
||||
|
||||
function handle(rec) {
|
||||
var value;
|
||||
try {
|
||||
value = expr(withContext(scope, rec));
|
||||
if (value.length === 0) {
|
||||
value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('FAILED:', field.bind, e);
|
||||
}
|
||||
|
||||
if (scope.setValue && scope.record && last !== value) {
|
||||
scope.setValue(last = value);
|
||||
}
|
||||
}
|
||||
|
||||
scope.$on("on:record-change", function(e, rec) {
|
||||
if (field && field.jsonField) {
|
||||
handle(scope.record);
|
||||
} else if (rec && rec === scope.record) {
|
||||
handle(rec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleValueExpr(scope, field) {
|
||||
|
||||
if (!field.valueExpr || !field.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var expr = $parse(field.valueExpr);
|
||||
|
||||
function handle(rec) {
|
||||
var value;
|
||||
try {
|
||||
value = axelor.$eval(scope, expr, withContext(scope, rec));
|
||||
if (value && value.length === 0) {
|
||||
value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('FAILED:', field.valueExpr, e);
|
||||
}
|
||||
|
||||
if (scope.setValue && scope.record) {
|
||||
scope.setValue(value, false);
|
||||
}
|
||||
}
|
||||
|
||||
scope.$on("on:record-change", function(e, rec) {
|
||||
scope.$timeout(function () {
|
||||
if (field && field.jsonField) {
|
||||
handle(scope.record);
|
||||
} else if (rec && rec === scope.record) {
|
||||
handle(rec);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleFor(scope, field, attr, conditional, negative) {
|
||||
if (field[conditional]) {
|
||||
handleCondition(scope, field, attr, field[conditional], negative);
|
||||
}
|
||||
}
|
||||
|
||||
function handleForField(scope) {
|
||||
var field = scope.field;
|
||||
if (!field) return;
|
||||
handleFor(scope, field, "valid", "validIf");
|
||||
handleFor(scope, field, "hidden", "hideIf");
|
||||
handleFor(scope, field, "hidden", "showIf", true);
|
||||
handleFor(scope, field, "readonly", "readonlyIf");
|
||||
handleFor(scope, field, "required", "requiredIf");
|
||||
handleFor(scope, field, "collapse", "collapseIf");
|
||||
handleFor(scope, field, "canNew", "canNew");
|
||||
handleFor(scope, field, "canView", "canView");
|
||||
handleFor(scope, field, "canEdit", "canEdit");
|
||||
handleFor(scope, field, "canRemove", "canRemove");
|
||||
handleFor(scope, field, "canSelect", "canSelect");
|
||||
handleHilites(scope, field);
|
||||
handleBind(scope, field);
|
||||
handleValueExpr(scope, field);
|
||||
}
|
||||
|
||||
function handleForView(scope) {
|
||||
var field = scope.schema;
|
||||
if (!field) return;
|
||||
handleFor(scope, field, "canNew", "canNew");
|
||||
handleFor(scope, field, "canEdit", "canEdit");
|
||||
handleFor(scope, field, "canSave", "canSave");
|
||||
handleFor(scope, field, "canCopy", "canCopy");
|
||||
handleFor(scope, field, "canDelete", "canDelete");
|
||||
handleFor(scope, field, "canArchive", "canArchive");
|
||||
handleFor(scope, field, "canAttach", "canAttach");
|
||||
}
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
scope.$evalAsync(function() {
|
||||
if (element.is('[ui-form]')) {
|
||||
return handleForView(scope);
|
||||
}
|
||||
handleForField(scope);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
240
sophal/js/lib/dialogs.js
Normal file
240
sophal/js/lib/dialogs.js
Normal file
@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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 dialogs = {
|
||||
|
||||
config: {
|
||||
yesNo: false
|
||||
},
|
||||
|
||||
say: function(str) {
|
||||
return this.box(str, {
|
||||
title: _t('Information')
|
||||
});
|
||||
},
|
||||
|
||||
warn: function(str, callback) {
|
||||
return this.box(str, {
|
||||
title: _t('Warning'),
|
||||
onClose: callback
|
||||
});
|
||||
},
|
||||
|
||||
error: function(str, callback) {
|
||||
return this.box(str, {
|
||||
title: _t('Error'),
|
||||
onClose: callback
|
||||
});
|
||||
},
|
||||
|
||||
confirm: function(str, callback, options) {
|
||||
var element = null,
|
||||
opts = null,
|
||||
cb = angular.noop,
|
||||
doCall = true;
|
||||
|
||||
for (var i = 1; i < 3; i++) {
|
||||
var arg = arguments[i];
|
||||
if (_.isFunction(arg)) cb = arg;
|
||||
if (_.isObject(arg)) opts = arg;
|
||||
}
|
||||
|
||||
opts = _.extend({
|
||||
title: _t('Question')
|
||||
}, this.config, opts);
|
||||
|
||||
var titleOK = opts.yesNo ? _t('Yes') : _t('OK');
|
||||
var titleCancel = opts.yesNo ? _t('No') : _t('Cancel');
|
||||
|
||||
element = this.box(str, {
|
||||
title: opts.title,
|
||||
onClose: function() {
|
||||
if (doCall) cb(false);
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: titleCancel,
|
||||
'class': 'btn',
|
||||
click: function() {
|
||||
cb(false);
|
||||
doCall = false;
|
||||
element.dialog('close');
|
||||
}
|
||||
},
|
||||
{
|
||||
text: titleOK,
|
||||
'class': 'btn btn-primary',
|
||||
click: function() {
|
||||
cb(true);
|
||||
doCall = false;
|
||||
element.dialog('close');
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
box: function(str, options) {
|
||||
|
||||
var opts = $.extend({}, options);
|
||||
var title = opts.title || _t('Information');
|
||||
var onClose = opts.onClose || $.noop;
|
||||
var onOpen = opts.onOpen || $.noop;
|
||||
var buttons = opts.buttons || [
|
||||
{
|
||||
'text' : _t('OK'),
|
||||
'class' : 'btn btn-primary',
|
||||
'click' : function() {
|
||||
element.dialog('close');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
var element = $('<div class="message-box" style="padding: 15px;"></div>').attr('title', title).html(str);
|
||||
var dialog = element.dialog({
|
||||
dialogClass: 'ui-dialog-responsive ui-dialog-small ui-dialog-dragged',
|
||||
resizable: false,
|
||||
draggable: true,
|
||||
autoOpen: false,
|
||||
closeOnEscape: true,
|
||||
modal: true,
|
||||
zIndex: 1100,
|
||||
open: function(e) {
|
||||
onOpen(e);
|
||||
},
|
||||
close: function(e) {
|
||||
onClose(e);
|
||||
element.dialog('destroy');
|
||||
element.remove();
|
||||
},
|
||||
show: {
|
||||
effect: 'fade',
|
||||
duration: 300
|
||||
},
|
||||
buttons: buttons
|
||||
});
|
||||
|
||||
dialog.dialog('open');
|
||||
|
||||
return dialog;
|
||||
}
|
||||
};
|
||||
|
||||
// patch ui.dialog to maintain overlay opacity
|
||||
['open', 'close', 'moveToTop'].forEach(function (name) {
|
||||
var func = $.ui.dialog.prototype[name];
|
||||
$.ui.dialog.prototype[name] = function () {
|
||||
func.apply(this, arguments);
|
||||
var all = $('body > .ui-widget-overlay').css('opacity', 0);
|
||||
var last = name === 'close' ? all.last() : $(this.overlay.$el);
|
||||
last.css('opacity', 0.3);
|
||||
};
|
||||
});
|
||||
|
||||
var elemNotifyStack = null;
|
||||
var elemNotifyText = '<div class="alert alert-block fade in">'+
|
||||
' <button type="button" class="close" data-dismiss="alert">×</button>'+
|
||||
' <h4 class="alert-heading">#title#</h4>'+
|
||||
' <p>#message#</p>'+
|
||||
'</div>';
|
||||
var elemNotifyText2 = '<div class="alert alert-block fade in">'+
|
||||
' <button type="button" class="close" data-dismiss="alert">×</button>'+
|
||||
' <strong>#title#</strong> #message#'+
|
||||
'</div>';
|
||||
|
||||
function doNotify(message, options) {
|
||||
if (elemNotifyStack === null) {
|
||||
elemNotifyStack = $('<div class="notify-stack"></div>')
|
||||
.css('position', 'fixed')
|
||||
.css('bottom', 0)
|
||||
.css('right', 10)
|
||||
.zIndex(9999999)
|
||||
.appendTo("body");
|
||||
}
|
||||
|
||||
var opts = _.extend({
|
||||
timeout: 5000
|
||||
}, options);
|
||||
var tmpl, elem;
|
||||
|
||||
tmpl = opts.alt ? elemNotifyText2 : elemNotifyText;
|
||||
tmpl = tmpl.replace("#title#", opts.title || '').replace("#message#", message);
|
||||
tmpl = axelor.sanitize(tmpl);
|
||||
|
||||
elem = $(tmpl)
|
||||
.css('margin-bottom', 7)
|
||||
.appendTo(elemNotifyStack);
|
||||
|
||||
if (opts.css) {
|
||||
elem.addClass(opts.css);
|
||||
}
|
||||
|
||||
_.delay(function () {
|
||||
if (elem) {
|
||||
elem.alert("close");
|
||||
elem = null;
|
||||
}
|
||||
}, opts.timeout);
|
||||
|
||||
elem.alert();
|
||||
}
|
||||
|
||||
var notify = {
|
||||
|
||||
info: function(message, options) {
|
||||
var opts = _.extend({
|
||||
title: _t('Information'),
|
||||
css: 'alert-info'
|
||||
}, options);
|
||||
return doNotify(message, opts);
|
||||
},
|
||||
|
||||
alert: function(message, options) {
|
||||
var opts = _.extend({
|
||||
title: _t('Alert')
|
||||
}, options);
|
||||
return doNotify(message, opts);
|
||||
},
|
||||
|
||||
success: function(message, options) {
|
||||
var opts = _.extend({
|
||||
title: _t('Success'),
|
||||
css: 'alert-primary'
|
||||
}, options);
|
||||
return doNotify(message, opts);
|
||||
},
|
||||
|
||||
error: function(message, options) {
|
||||
var opts = _.extend({
|
||||
title: _t('Error'),
|
||||
css: 'alert-error'
|
||||
}, options);
|
||||
return doNotify(message, opts);
|
||||
}
|
||||
};
|
||||
|
||||
this.axelor = this.axelor || {};
|
||||
this.axelor.dialogs = dialogs;
|
||||
this.axelor.notify = notify;
|
||||
|
||||
}).call(this);
|
||||
38
sophal/js/lib/i18n.js
Normal file
38
sophal/js/lib/i18n.js
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 bundle = (window._t || {}).bundle || {};
|
||||
|
||||
function gettext(key) {
|
||||
var message = bundle[key] || bundle[(key||'').trim()] || key;
|
||||
if (message && arguments.length > 1) {
|
||||
for(var i = 1 ; i < arguments.length ; i++) {
|
||||
var placeholder = new RegExp('\\{' + (i-1) + '\\}', 'g');
|
||||
var value = arguments[i];
|
||||
message = message.replace(placeholder, value);
|
||||
}
|
||||
}
|
||||
return axelor.sanitize(message);
|
||||
}
|
||||
|
||||
this._t = gettext;
|
||||
|
||||
}).call(this);
|
||||
209
sophal/js/lib/tabs.js
Normal file
209
sophal/js/lib/tabs.js
Normal file
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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 BSTabs = function(element) {
|
||||
this.element = $(element);
|
||||
this._setup();
|
||||
};
|
||||
|
||||
BSTabs.prototype = {
|
||||
|
||||
constructor: BSTabs,
|
||||
|
||||
_setup: function() {
|
||||
|
||||
this.$elemStrip = this.element.find('.nav-tabs-strip:first');
|
||||
this.$elemLeftScroller = this.element.find('.nav-tabs-scroll-l:first');
|
||||
this.$elemRightScroller = this.element.find('.nav-tabs-scroll-r:first');
|
||||
this.$elemMenu = this.element.find('.nav-tabs-menu:first');
|
||||
|
||||
this.$elemTabs = this.element.find('.nav-tabs:first').addClass('nav-tabs-scrollable');
|
||||
|
||||
if (this.$elemMenu.length) {
|
||||
this.$elemRightScroller.css('right', 16);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.$elemLeftScroller.click(function(){
|
||||
self._scrollLeft();
|
||||
return false;
|
||||
});
|
||||
this.$elemRightScroller.click(function(){
|
||||
self._scrollRight();
|
||||
return false;
|
||||
});
|
||||
|
||||
var _onResize = _.debounce(function () { self._adjustScroll(); }, 300);
|
||||
var _onAdjustSize = _.debounce(function () { self._adjustScroll(); });
|
||||
var _onAdjust = function(event) {
|
||||
event.stopPropagation();
|
||||
setTimeout(function (){
|
||||
self._adjustScroll();
|
||||
});
|
||||
};
|
||||
|
||||
$(window).on('resize', _onResize);
|
||||
$(document).on('adjust:size', _onAdjustSize);
|
||||
|
||||
this.element.on('adjust:tabs', _onAdjust);
|
||||
|
||||
this.$elemTabs.on("click", " > li > a", function(event){
|
||||
self._adjustTab($(this).parent(), true);
|
||||
});
|
||||
|
||||
this.element.on('$destroy', function () {
|
||||
$(window).off('resize', _onResize);
|
||||
$(document).off('adjust:size', _onAdjustSize);
|
||||
});
|
||||
},
|
||||
|
||||
_getTabsWidth: function() {
|
||||
var widthTabs = 0;
|
||||
this.$elemTabs.find('> li:visible').each(function(){
|
||||
widthTabs += $(this).outerWidth(true);
|
||||
});
|
||||
return widthTabs;
|
||||
},
|
||||
|
||||
_scrollLeft: function() {
|
||||
if (this.$elemLeftScroller.hasClass('disabled'))
|
||||
return;
|
||||
|
||||
var x = this.$elemTabs.position().left;
|
||||
var scrollTo = Math.min(0, x + 100);
|
||||
|
||||
this._scrollTabs(scrollTo, true);
|
||||
},
|
||||
|
||||
_scrollRight: function() {
|
||||
if (this.$elemRightScroller.hasClass('disabled'))
|
||||
return;
|
||||
|
||||
var x = this.$elemTabs.position().left;
|
||||
var w = this._getTabsWidth();
|
||||
|
||||
var mx = - (w - this.$elemStrip.width());
|
||||
|
||||
var scrollTo = Math.max(mx, x - 100);
|
||||
|
||||
this._scrollTabs(scrollTo, true);
|
||||
},
|
||||
|
||||
_scrollTabs: function(scrollTo, animate) {
|
||||
if (scrollTo === this._lastScrollTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastScrollTo = scrollTo;
|
||||
|
||||
if (animate) {
|
||||
var self = this;
|
||||
return this.$elemTabs.animate({
|
||||
'left': scrollTo
|
||||
}, 300, function() {
|
||||
self._activateScrollers();
|
||||
});
|
||||
}
|
||||
this.$elemTabs.css('left', scrollTo);
|
||||
this._activateScrollers();
|
||||
},
|
||||
|
||||
_activateScrollers: function() {
|
||||
|
||||
if (this.$elemTabs.position().left < 0) {
|
||||
this.$elemLeftScroller.removeClass('disabled');
|
||||
} else {
|
||||
this.$elemLeftScroller.addClass('disabled');
|
||||
}
|
||||
|
||||
if (this._getTabsWidth() + this.$elemTabs.position().left > this.$elemStrip.width() + 1) {
|
||||
this.$elemRightScroller.removeClass('disabled');
|
||||
} else {
|
||||
this.$elemRightScroller.addClass('disabled');
|
||||
}
|
||||
},
|
||||
|
||||
_adjustTab: function(tab, animate) {
|
||||
|
||||
if (!$(tab).length) return;
|
||||
|
||||
var w = this.$elemStrip.innerWidth();
|
||||
var scrollTo = this.$elemTabs.position().left;
|
||||
|
||||
var left = $(tab).position().left + scrollTo;
|
||||
var right = left + $(tab).outerWidth(true);
|
||||
|
||||
if (left < 0) {
|
||||
scrollTo -= left;
|
||||
} else if (right > w){
|
||||
scrollTo -= right - w;
|
||||
}
|
||||
|
||||
this._scrollTabs(scrollTo, animate);
|
||||
},
|
||||
|
||||
_adjustScroll: function() {
|
||||
|
||||
var widthStrip = this.$elemStrip.width();
|
||||
var widthTabs = this._getTabsWidth();
|
||||
|
||||
this.element.toggleClass("nav-tabs-overflow", widthStrip < widthTabs);
|
||||
|
||||
var scrollTo = 0;
|
||||
|
||||
if (widthStrip >= widthTabs) {
|
||||
this.$elemLeftScroller.hide();
|
||||
this.$elemRightScroller.hide();
|
||||
this.$elemMenu.hide();
|
||||
this.$elemStrip.css('margin', '0');
|
||||
} else {
|
||||
this.$elemLeftScroller.show();
|
||||
this.$elemRightScroller.show();
|
||||
this.$elemMenu.show();
|
||||
this.$elemStrip.css('margin', this.$elemMenu.length ? '0 32px 0 16px' : '0 16px');
|
||||
|
||||
var left = this.$elemTabs.position().left;
|
||||
var right = widthTabs + left;
|
||||
if (right < widthStrip) {
|
||||
scrollTo = left + (widthStrip - right);
|
||||
} else {
|
||||
var tab = this.$elemTabs.find('> li.active');
|
||||
if (tab) {
|
||||
return this._adjustTab(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._scrollTabs(scrollTo);
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.bsTabs = function () {
|
||||
return this.each(function () {
|
||||
var $this = $(this),
|
||||
data = $this.data('bsTabs');
|
||||
if (!data) $this.data('bsTabs', (data = new BSTabs(this)));
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.bsTabs.Constructor = BSTabs;
|
||||
|
||||
})();
|
||||
95
sophal/js/lib/utils.js
Normal file
95
sophal/js/lib/utils.js
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
// integrate underscore.string with underscore
|
||||
_.mixin(_.str.exports());
|
||||
|
||||
var util = this.util ? this.util : this.util = {};
|
||||
|
||||
/**
|
||||
* Based on the `util.inherits` of `Node.js` with some additional features
|
||||
*
|
||||
* 1. The super prototype is attached to the base prototype as `super_`.
|
||||
* 2. The base prototype can be provided as third argument.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* function Hello(message) {
|
||||
* this.message = message;
|
||||
* }
|
||||
*
|
||||
* Hello.prototype.say = function(what) {
|
||||
* console.log(what || this.message);
|
||||
* }
|
||||
*
|
||||
* function HelloWorld(message) {
|
||||
* this.super_.constructor.apply(this, arguments);
|
||||
* }
|
||||
*
|
||||
* util.inherits(HelloWorld, Hello, {
|
||||
*
|
||||
* say: function(what) {
|
||||
* // do something
|
||||
* this.super_.say.apply(this, arguments);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param ctor the base constructor
|
||||
* @param superCtor the super constructor
|
||||
* @param proto the prototype of the base constructor (optional)
|
||||
*
|
||||
* @returns the base constructor
|
||||
*/
|
||||
util.inherits = function(ctor, superCtor, /* optional */ proto) {
|
||||
|
||||
var props = {
|
||||
constructor: {
|
||||
value: ctor,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
};
|
||||
|
||||
Object.getOwnPropertyNames(proto||{}).forEach(function(name) {
|
||||
props[name] = Object.getOwnPropertyDescriptor(proto, name);
|
||||
});
|
||||
|
||||
ctor.super_ = superCtor;
|
||||
ctor.prototype = Object.create(superCtor.prototype, props);
|
||||
ctor.prototype.super_ = superCtor.prototype;
|
||||
|
||||
return ctor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut to `util.inherits`.
|
||||
*
|
||||
* @param superCtor the super constructor
|
||||
* @param proto the prototype for this contructor
|
||||
*
|
||||
* @returns this constructor itself
|
||||
*/
|
||||
Function.prototype.inherits = function(superCtor, /* optional */ proto) {
|
||||
return util.inherits(this, superCtor, proto);
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
867
sophal/js/view/view.base.js
Normal file
867
sophal/js/view/view.base.js
Normal file
@ -0,0 +1,867 @@
|
||||
/*
|
||||
* 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.ViewCtrl = ViewCtrl;
|
||||
ui.ViewCtrl.$inject = ['$scope', 'DataSource', 'ViewService'];
|
||||
|
||||
function ViewCtrl($scope, DataSource, ViewService) {
|
||||
|
||||
$scope._viewParams = $scope._viewParams || $scope.selectedTab;
|
||||
if (!$scope._viewParams) {
|
||||
throw "View parameters are not provided.";
|
||||
}
|
||||
|
||||
var params = $scope._viewParams;
|
||||
|
||||
$scope._views = ViewService.accept(params);
|
||||
$scope._viewType = params.viewType;
|
||||
|
||||
if ($scope.$parent && $scope.$parent._model) {
|
||||
$scope._parentModel = $scope.$parent._model;
|
||||
}
|
||||
|
||||
$scope._model = params.model;
|
||||
$scope._fields = {};
|
||||
|
||||
$scope._dataSource = null;
|
||||
$scope._domain = params.domain;
|
||||
$scope._context = params.context;
|
||||
|
||||
if (params.model) {
|
||||
$scope._dataSource = DataSource.create(params.model, params);
|
||||
}
|
||||
|
||||
$scope._defer = function() {
|
||||
return ViewService.defer();
|
||||
};
|
||||
|
||||
$scope.loadView = function(viewType, viewName) {
|
||||
var view = $scope._views[viewType] || {
|
||||
type: viewType,
|
||||
name: viewName
|
||||
};
|
||||
var ctx = $scope._context;
|
||||
if ($scope.getContext) {
|
||||
ctx = $scope.getContext();
|
||||
}
|
||||
return ViewService.getMetaDef($scope._model, view, ctx);
|
||||
};
|
||||
|
||||
$scope.loadFields = function() {
|
||||
return ViewService.getFields($scope._model);
|
||||
};
|
||||
|
||||
$scope.updateRoute = function() {
|
||||
this.$emit("on:update-route");
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
throw "Not Implemented.";
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
throw "Not Implemented.";
|
||||
};
|
||||
|
||||
var switchedTo = null;
|
||||
|
||||
$scope.switchTo = function(viewType, /* optional */ callback) {
|
||||
|
||||
var view = $scope._views[viewType];
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = view.deferred.promise;
|
||||
promise.then(function(viewScope){
|
||||
|
||||
if (!viewScope || switchedTo === viewType) {
|
||||
return;
|
||||
}
|
||||
|
||||
switchedTo = viewType;
|
||||
|
||||
$scope._viewTypeLast = $scope._viewType === 'form' ? $scope._viewTypeLast : $scope._viewType;
|
||||
$scope._viewType = viewType;
|
||||
$scope._viewParams.viewType = viewType; //XXX: remove
|
||||
$scope._viewParams.$viewScope = viewScope;
|
||||
|
||||
viewScope.show();
|
||||
|
||||
if (viewScope.updateRoute) {
|
||||
viewScope.updateRoute();
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(viewScope);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!params.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hide toolbar button titles
|
||||
$scope.tbTitleHide = !axelor.config['view.toolbar.titles'];
|
||||
|
||||
function switchAndEdit(id, readonly) {
|
||||
$scope.switchTo('form', function(scope) {
|
||||
scope._viewPromise.then(function() {
|
||||
scope.doRead(id).success(function(record) {
|
||||
scope.edit(record);
|
||||
scope.setEditable(!readonly);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// show single or default record if specified
|
||||
var context = params.context || {};
|
||||
if (context._showSingle || context._showRecord) {
|
||||
var ds = $scope._dataSource;
|
||||
var forceEdit = (params.params||{}).forceEdit === true;
|
||||
|
||||
if (context._showRecord > 0) {
|
||||
params.viewType = "form";
|
||||
return $scope.switchTo('form');
|
||||
}
|
||||
|
||||
return ds.search({
|
||||
offset: 0,
|
||||
limit: 2,
|
||||
fields: ["id"]
|
||||
}).success(function(records, page){
|
||||
if (page.total === 1 && records.length === 1) {
|
||||
return switchAndEdit(records[0].id, !forceEdit);
|
||||
}
|
||||
return $scope.switchTo($scope._viewType || 'grid');
|
||||
});
|
||||
}
|
||||
|
||||
// switch to the the current viewType
|
||||
$scope.switchTo($scope._viewType || 'grid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Base controller for DataSource views. This controller should not be used
|
||||
* directly but actual controller should inherit from it.
|
||||
*
|
||||
*/
|
||||
ui.DSViewCtrl = function DSViewCtrl(type, $scope, $element) {
|
||||
|
||||
if (!type) {
|
||||
throw "No view type provided.";
|
||||
}
|
||||
if (!$scope._dataSource) {
|
||||
throw "DataSource is not provided.";
|
||||
}
|
||||
|
||||
$scope._viewResolver = $scope._defer();
|
||||
$scope._viewPromise = $scope._viewResolver.promise;
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
var view = $scope._views[type] || {};
|
||||
var viewPromise = null;
|
||||
var hiddenButtons = {};
|
||||
|
||||
var params = $scope._viewParams;
|
||||
|
||||
if (params.params && params.params.limit) {
|
||||
if (ds && ds._page) {
|
||||
ds._page.limit = +(params.params.limit) || ds._page.limit;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.fields = {};
|
||||
$scope.fields_related = {};
|
||||
$scope.schema = null;
|
||||
|
||||
$scope.show = function() {
|
||||
if (!viewPromise) {
|
||||
viewPromise = $scope.loadView(type, view.name);
|
||||
viewPromise.then(function(meta){
|
||||
var schema = meta.view;
|
||||
var fields = meta.fields || params.fields;
|
||||
var toolbar = [];
|
||||
_.each(schema.toolbar, function(button){
|
||||
button.custom = true;
|
||||
if (/^(new|edit|save|delete|copy|cancel|back|refresh|search|export|log|files)$/.test(button.name)) {
|
||||
hiddenButtons[button.name] = button;
|
||||
button.custom = false;
|
||||
}
|
||||
toolbar.push(button);
|
||||
});
|
||||
var forceTitle = params.forceTitle;
|
||||
if (forceTitle === undefined) {
|
||||
forceTitle = (params.params||{}).forceTitle;
|
||||
}
|
||||
if (!forceTitle && schema.title) {
|
||||
$scope.viewTitle = schema.title;
|
||||
}
|
||||
$scope.fields = fields;
|
||||
$scope.fields_related = meta.related;
|
||||
$scope.schema = schema;
|
||||
$scope.toolbar = toolbar;
|
||||
$scope.menubar = schema.menubar;
|
||||
|
||||
$scope.toolbarAsMenu = _.isEmpty(toolbar) ? null : [{
|
||||
icon: 'fa-wrench',
|
||||
isButton: true,
|
||||
items: _.map(toolbar, function (item) {
|
||||
return _.extend({}, item, {
|
||||
name: item.name,
|
||||
action: item.onClick,
|
||||
title: item.title || item.autoTitle || item.name
|
||||
});
|
||||
})
|
||||
}];
|
||||
|
||||
// watch on view.loaded to improve performance
|
||||
schema.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.onShow(viewPromise);
|
||||
};
|
||||
|
||||
$scope.onShow = function(promise) {
|
||||
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return ds && ds.canNext();
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return ds && ds.canPrev();
|
||||
};
|
||||
|
||||
$scope.getPageSize = function() {
|
||||
var page = ds && ds._page;
|
||||
if (page) {
|
||||
return page.limit;
|
||||
}
|
||||
return 40;
|
||||
};
|
||||
|
||||
$scope.setPageSize = function(value) {
|
||||
var page = ds && ds._page,
|
||||
limit = Math.max(0, +value) || 40;
|
||||
if (page && page.limit != limit) {
|
||||
page.limit = limit;
|
||||
$scope.onRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
var can = (function (scope) {
|
||||
var fn = null;
|
||||
var perms = {
|
||||
'new': 'create',
|
||||
'copy': 'create',
|
||||
'edit': 'write',
|
||||
'save': 'write',
|
||||
'delete': 'remove',
|
||||
'archive': 'remove',
|
||||
'export': 'export'
|
||||
};
|
||||
var actions = {
|
||||
'new': 'canNew',
|
||||
'edit': 'canEdit',
|
||||
'save': 'canSave',
|
||||
'copy': 'canCopy',
|
||||
'delete': 'canDelete',
|
||||
'archive': 'canArchive',
|
||||
'attach': 'canAttach'
|
||||
};
|
||||
|
||||
function attr(which) {
|
||||
if (fn === null && _.isFunction(scope.attr)) {
|
||||
fn = scope.attr;
|
||||
}
|
||||
return !fn || fn(which) !== false;
|
||||
}
|
||||
|
||||
function perm(which) {
|
||||
return which === undefined || scope.hasPermission(which);
|
||||
}
|
||||
|
||||
return function can(what) {
|
||||
return attr(actions[what]) && perm(perms[what]);
|
||||
};
|
||||
})($scope);
|
||||
|
||||
$scope.hasButton = function(name) {
|
||||
if (!can(name)) {
|
||||
return false;
|
||||
}
|
||||
if (_(hiddenButtons).has(name)) {
|
||||
var button = hiddenButtons[name];
|
||||
if (button.isHidden) {
|
||||
return !button.isHidden();
|
||||
}
|
||||
return !button.hidden;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.hasPermission = function(perm) {
|
||||
var view = $scope.schema;
|
||||
var defaultValue = arguments.length === 2 ? arguments[1] : true;
|
||||
if (!view || !view.perms) return defaultValue;
|
||||
var perms = view.perms;
|
||||
var permitted = perms[perm];
|
||||
if (permitted === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
return _.toBoolean(permitted);
|
||||
};
|
||||
|
||||
$scope.isPermitted = function(perm, record, callback) {
|
||||
var ds = this._dataSource;
|
||||
ds.isPermitted(perm, record).success(function(res){
|
||||
var errors = res.errors;
|
||||
if (errors) {
|
||||
return axelor.dialogs.error(errors.read);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.canShowToolbar = function() {
|
||||
var params = ($scope._viewParams || {}).params;
|
||||
if (params && params['show-toolbar'] === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.hasHelp = function() {
|
||||
var view = $scope.schema;
|
||||
return view ? view.helpLink : false;
|
||||
};
|
||||
|
||||
$scope.onShowHelp = function() {
|
||||
if ($scope.hasHelp()) {
|
||||
window.open($scope.schema.helpLink);
|
||||
}
|
||||
};
|
||||
|
||||
if (view.deferred) {
|
||||
view.deferred.resolve($scope);
|
||||
}
|
||||
|
||||
$scope.$on('on:tab-reload', function(e, tab) {
|
||||
if ($scope === e.targetScope && $scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ui.directive('uiViewPane', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
controller: ['$scope', '$attrs', 'DataSource', 'ViewService', function ($scope, $attrs, DataSource, ViewService) {
|
||||
|
||||
var params = $scope.$eval($attrs.uiViewPane);
|
||||
|
||||
$scope._viewParams = params;
|
||||
ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.viewList = [];
|
||||
$scope.viewType = null;
|
||||
|
||||
var switchTo = $scope.switchTo;
|
||||
$scope.switchTo = function (type, callback) {
|
||||
var view = $scope._views[type];
|
||||
if (view && $scope.viewList.indexOf(type) === -1) {
|
||||
$scope.viewList.push(type);
|
||||
}
|
||||
var viewScope = !$scope._isPopup && $scope.selectedTab && $scope.selectedTab.$viewScope;
|
||||
if (viewScope && viewScope.viewType === 'form' && viewScope.viewType !== type) {
|
||||
viewScope.$$resetForm();
|
||||
}
|
||||
$scope.viewType = type;
|
||||
return switchTo(type, callback);
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTab.viewType', function viewTypeWatch(type) {
|
||||
var params = $scope._viewParams;
|
||||
if (params && params.$viewScope !== ($scope.selectedTab || {}).$viewScope) {
|
||||
return;
|
||||
}
|
||||
if ($scope.viewType !== type && type) {
|
||||
$scope.switchTo(type);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.viewTemplate = function (type) {
|
||||
var tname = "ui-template:" + type;
|
||||
var template = type;
|
||||
if (params.params && params.params[tname]) {
|
||||
template = params.params[tname];
|
||||
}
|
||||
return 'partials/views/' + template + '.html';
|
||||
};
|
||||
|
||||
var type = params.viewType || params.type;
|
||||
$scope.keepAttached = $scope._isPopup || (params.params||{}).popup || type === 'html';
|
||||
$scope.switchTo(type);
|
||||
}],
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
},
|
||||
template:
|
||||
"<div class='view-pane' ui-attach='keepAttached || tab.selected'>" +
|
||||
"<div class='view-container'" +
|
||||
" ng-repeat='type in viewList'" +
|
||||
" ui-show='type === viewType'" +
|
||||
" ui-attach-scroll ui-attach='keepAttached || type == viewType'" +
|
||||
" ng-include='viewTemplate(type)'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewPopup', function() {
|
||||
|
||||
return {
|
||||
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
|
||||
var params = $scope.$eval($attrs.uiViewPopup);
|
||||
|
||||
$scope.tab = params;
|
||||
$scope._isPopup = true;
|
||||
|
||||
$scope.onHotKey = function (e, action) {
|
||||
return false;
|
||||
};
|
||||
|
||||
var canClose = false;
|
||||
|
||||
$scope.onOK = function () {
|
||||
$scope.closeTab($scope.tab, function() {
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onBeforeClose = function(e) {
|
||||
if (canClose) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$scope.onOK();
|
||||
};
|
||||
|
||||
$scope.onPopupClose = function () {
|
||||
var tab = $scope.tab,
|
||||
params = tab.params || {},
|
||||
parent = tab.$popupParent;
|
||||
|
||||
while (parent && parent.$$destroyed && parent.tab) {
|
||||
parent = parent.tab.$popupParent;
|
||||
}
|
||||
if (parent && parent.reload && params.popup === "reload") {
|
||||
parent.reload();
|
||||
}
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
|
||||
$scope.onPopupOK = function () {
|
||||
var viewScope = $scope._viewParams.$viewScope;
|
||||
if (!viewScope.onSave || (!viewScope.isDirty() && viewScope.id)) {
|
||||
return $scope.onOK();
|
||||
}
|
||||
return viewScope.onSave({ fireOnLoad: false }).then(function(record, page) {
|
||||
viewScope.edit(record);
|
||||
viewScope.$timeout($scope.onOK.bind($scope));
|
||||
});
|
||||
};
|
||||
|
||||
params = $scope.tab.params || {};
|
||||
if (params['popup-save'] === false) {
|
||||
$scope.onPopupOK = false;
|
||||
}
|
||||
}],
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.$watch('viewTitle', function viewTitleWatch(title) {
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
scope.waitForActions(function () {
|
||||
if (scope._viewParams.viewType === 'html') {
|
||||
scope.viewTitle = scope.tabTitle(scope._viewParams);
|
||||
scope._doShow();
|
||||
return;
|
||||
}
|
||||
|
||||
var unwatch = scope.$watch("_viewParams.$viewScope.schema.loaded", function viewLoadedWatch(loaded) {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
var viewScope = scope._viewParams.$viewScope;
|
||||
var viewPromise = viewScope._viewPromise;
|
||||
scope.viewTitle = scope.tabTitle(scope._viewParams);
|
||||
scope.$broadcast('grid:adjust-size', viewScope);
|
||||
scope._doShow(viewPromise);
|
||||
});
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-resizable="true" x-on-close="onPopupClose" x-on-ok="onPopupOK" x-on-before-close="onBeforeClose">' +
|
||||
'<div ui-view-pane="tab"></div>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiRecordPager', function(){
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var elText = element.find('.record-pager-text').show(),
|
||||
elChanger = element.find('.record-pager-change').hide(),
|
||||
elInput = elChanger.find('input');
|
||||
|
||||
scope.showText = attrs.uiRecordPager !== "no-text";
|
||||
|
||||
function updatePageSize() {
|
||||
var size = +(elInput.val()) || 0;
|
||||
if (scope.setPageSize && size > 0) {
|
||||
scope.setPageSize(size);
|
||||
}
|
||||
elText.add(elChanger).toggle();
|
||||
}
|
||||
|
||||
elText.click(function(e) {
|
||||
elText.add(elChanger).toggle();
|
||||
elInput.zIndex(elInput.parent().zIndex() + 1).focus().select();
|
||||
});
|
||||
|
||||
elInput.on('click', function () {
|
||||
elInput.zIndex(elInput.parent().zIndex() + 1).focus().select();
|
||||
});
|
||||
|
||||
elChanger.on('click', 'button', function() {
|
||||
updatePageSize();
|
||||
});
|
||||
|
||||
elChanger.keyup(function(e) {
|
||||
if(e.keyCode == 13) { // ENTER
|
||||
updatePageSize();
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
template:
|
||||
'<div class="record-pager hidden-phone">'+
|
||||
'<span ng-show="showText">'+
|
||||
'<span class="record-pager-text">{{pagerText()}}</span>'+
|
||||
'<span class="input-append record-pager-change">'+
|
||||
'<input type="text" style="width: 30px;" value="{{getPageSize()}}">'+
|
||||
'<button type="button" class="btn add-on"><i class="fa fa-check"></i></button>'+
|
||||
'</span>'+
|
||||
'</span>'+
|
||||
'<div class="btn-group">'+
|
||||
'<button class="btn" ng-disabled="!canPrev()" ng-click="onPrev()"><i class="fa fa-chevron-left"></i></button>'+
|
||||
'<button class="btn" ng-disabled="!canNext()" ng-click="onNext()"><i class="fa fa-chevron-right"></i></button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewCustomize', ['NavService', function(NavService) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.canShow = function () {
|
||||
if (!axelor.config['user.technical']) {
|
||||
return false;
|
||||
}
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && (view.viewId || view.modelId);
|
||||
};
|
||||
|
||||
scope.hasViewID = function () {
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && view.viewId;
|
||||
};
|
||||
|
||||
scope.hasModelID = function () {
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && view.modelId;
|
||||
};
|
||||
|
||||
scope.hasActionID = function () {
|
||||
return (scope.selectedTab || {}).actionId;
|
||||
};
|
||||
|
||||
scope.onShowView = function () {
|
||||
var id = scope.hasViewID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaView", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
scope.waitForActions(function () {
|
||||
var vs = (scope.selectedTab || {}).$viewScope;
|
||||
if (vs && vs.setEditable) {
|
||||
vs.setEditable();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.onShowModel = function () {
|
||||
var id = scope.hasModelID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaModel", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
};
|
||||
|
||||
scope.onShowAction = function () {
|
||||
var id = scope.hasActionID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaAction", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<ul ng-show='canShow()' class='nav menu-bar view-customize hidden-phone'>" +
|
||||
"<li class='dropdown menu'>" +
|
||||
"<a class='dropdown-toggle btn' data-toggle='dropdown' title='{{ \"Customize...\" | t}}'>" +
|
||||
"<i class='fa fa-wrench'></i>" +
|
||||
"</a>" +
|
||||
"<ul class='dropdown-menu pull-right'>" +
|
||||
"<li><a ng-click='onShowView()' ng-show='hasViewID()'>View...</a></li>" +
|
||||
"<li><a ng-click='onShowModel()' ng-show='hasModelID()'>Model...</a></li>" +
|
||||
"<li><a ng-click='onShowAction()' ng-show='hasActionID()'>Action...</a></li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"</ul>"
|
||||
};
|
||||
}]);
|
||||
|
||||
function viewSwitcher(scope, element, attrs) {
|
||||
|
||||
var params = (scope._viewParams || scope.tab);
|
||||
var viewTypes = _.pluck(params.views, 'type');
|
||||
|
||||
if ((params.viewType || params.type) === 'dashboard') {
|
||||
element.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
element.find("[x-view-type]").click(function(e) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
var type = $(this).attr("x-view-type");
|
||||
var vs = params.$viewScope || (scope.selectedTab || {}).$viewScope;
|
||||
var ds = vs._dataSource;
|
||||
var page = ds && ds._page;
|
||||
|
||||
if (type === "form" && page) {
|
||||
if (page.index === -1) page.index = 0;
|
||||
}
|
||||
|
||||
if ((scope.selectedTab || {}).viewType === 'grid') {
|
||||
var items = vs.getItems() || [];
|
||||
var index = _.first(vs.selection || []);
|
||||
if (index === undefined && items.length === 0 && vs.schema.canNew === false) {
|
||||
return;
|
||||
}
|
||||
if (index !== undefined) page.index = index;
|
||||
}
|
||||
|
||||
vs.switchTo(type);
|
||||
vs.$applyAsync();
|
||||
}).each(function() {
|
||||
var type = $(this).attr("x-view-type");
|
||||
if (viewTypes.indexOf(type) === -1) {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
|
||||
var watchExpr = scope._viewParams ? '_viewType' : 'tab.viewType';
|
||||
scope.$watch(watchExpr, function viewTypeWatch(type) {
|
||||
element.find("[x-view-type]").attr("disabled", false);
|
||||
element.find("[x-view-type][x-view-type=" + type + "]").attr("disabled", true);
|
||||
});
|
||||
}
|
||||
|
||||
ui.directive('uiViewSwitcher', function(){
|
||||
return {
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
element.parents('.view-container:first').addClass('has-toolbar');
|
||||
viewSwitcher(scope, element, attrs);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="view-switcher pull-right hidden-phone">'+
|
||||
'<div class="btn-group">'+
|
||||
'<button class="btn" x-view-type="grid"><i class="fa fa-list"></i></button>'+
|
||||
'<button class="btn" x-view-type="cards"><i class="fa fa-th-large"></i></button>'+
|
||||
'<button class="btn" x-view-type="kanban"><i class="fa fa-columns"></i></button>'+
|
||||
'<button class="btn" x-view-type="calendar"><i class="fa fa-calendar"></i></button>'+
|
||||
'<button class="btn" x-view-type="gantt"><i class="fa fa-calendar"></i></button>'+
|
||||
'<button class="btn" x-view-type="chart"><i class="fa fa-bar-chart-o"></i></button>'+
|
||||
'<button class="btn" x-view-type="form" ><i class="fa fa-file-text-o"></i></button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewSwitcherMenu', function(){
|
||||
return {
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
viewSwitcher(scope, element, attrs);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<span class='view-switch-menu dropdown pull-right'>" +
|
||||
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-ellipsis-v'></i></a>" +
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li><a href='' x-view-type='grid' x-translate>Grid</a></li>" +
|
||||
"<li><a href='' x-view-type='cards' x-translate>Cards</a></li>" +
|
||||
"<li><a href='' x-view-type='kanban' x-translate>Kanban</a></li>" +
|
||||
"<li><a href='' x-view-type='calendar' x-translate>Calendar</a></li>" +
|
||||
"<li><a href='' x-view-type='gantt' x-translate>Gantt</a></li>" +
|
||||
"<li><a href='' x-view-type='chart' x-translate>Chart</a></li>" +
|
||||
"<li><a href='' x-view-type='form' x-translate>Form</a></li>" +
|
||||
"</ul>" +
|
||||
"</span>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiHotKeys', function() {
|
||||
|
||||
var keys = {
|
||||
45: 'new', // insert
|
||||
69: 'edit', // e
|
||||
83: 'save', // s
|
||||
68: 'delete', // d
|
||||
82: 'refresh', // r
|
||||
70: 'search', // f
|
||||
71: 'select', // g
|
||||
74: 'prev', // j
|
||||
75: 'next', // n
|
||||
|
||||
77: 'focus-menu', // m
|
||||
120: 'toggle-menu', // F9
|
||||
|
||||
81: 'close' // q
|
||||
};
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
|
||||
var loginWindow = $("#loginWindow");
|
||||
|
||||
$(document).on('keydown.axelor-keys', function (e) {
|
||||
|
||||
if (loginWindow.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// disable backspace as back button
|
||||
if (e.which === 8 && e.target === document.body) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
var action = keys[e.which];
|
||||
|
||||
if (action === "toggle-menu") {
|
||||
$('#offcanvas-toggle a').click();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.altKey || e.shiftKey || !e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "focus-menu") {
|
||||
var activeMenu = $('.sidebar .nav-tree li.active');
|
||||
if (activeMenu.length === 0) {
|
||||
activeMenu = $('.sidebar .nav-tree li:first');
|
||||
}
|
||||
|
||||
var navTree = activeMenu.parents('[nav-tree]:first');
|
||||
if (navTree.length) {
|
||||
navTree.navtree('selectItem', activeMenu);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var tab = scope.selectedTab,
|
||||
dlg = $('[ui-editor-popup]:visible:last,[ui-view-popup]:visible:last,[ui-dms-popup]:visible:last').first(),
|
||||
vs = tab ? tab.$viewScope : null;
|
||||
|
||||
if (dlg.length) {
|
||||
vs = dlg.scope();
|
||||
}
|
||||
|
||||
if (!vs || !keys.hasOwnProperty(e.which)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "close") {
|
||||
scope.closeTab(tab, function() {
|
||||
scope.$applyAsync();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action === "search") {
|
||||
var filterBox = $('.filter-box .search-query:visible');
|
||||
if (filterBox.length) {
|
||||
filterBox.focus().select();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isFunction(vs.onHotKey)) {
|
||||
return vs.onHotKey(e, action);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$(document).off('keydown.axelor-keys');
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
769
sophal/js/view/view.calendar.js
Normal file
769
sophal/js/view/view.calendar.js
Normal file
@ -0,0 +1,769 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
/* global d3: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.controller('CalendarViewCtrl', CalendarViewCtrl);
|
||||
|
||||
CalendarViewCtrl.$inject = ['$scope', '$element'];
|
||||
function CalendarViewCtrl($scope, $element) {
|
||||
|
||||
ui.DSViewCtrl('calendar', $scope, $element);
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
|
||||
var view = {};
|
||||
var colors = {};
|
||||
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.refresh();
|
||||
}
|
||||
|
||||
viewPromise.then(function(){
|
||||
|
||||
var schema = $scope.schema;
|
||||
|
||||
initialized = true;
|
||||
|
||||
view = {
|
||||
start: schema.eventStart,
|
||||
stop: schema.eventStop,
|
||||
length: parseInt(schema.eventLength) || 0,
|
||||
color: schema.colorBy,
|
||||
title: schema.items[0].name
|
||||
};
|
||||
|
||||
$scope._viewResolver.resolve(schema, $element);
|
||||
$scope.updateRoute();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isAgenda = function() {
|
||||
var field = this.fields[view.start];
|
||||
return field && field.type === "datetime";
|
||||
};
|
||||
|
||||
var d3_colors = d3.scale.category10().range().concat(
|
||||
d3.scale.category20().range());
|
||||
|
||||
function nextColor(n) {
|
||||
if (n === undefined || n < 0 || n >= d3_colors.length) {
|
||||
n = _.random(0, d3_colors.length);
|
||||
}
|
||||
var c = d3.rgb(d3_colors[n]);
|
||||
return {
|
||||
bg: "" + c,
|
||||
fg: "" + c.brighter(99),
|
||||
bc: "" + c.darker(0.9)
|
||||
};
|
||||
}
|
||||
|
||||
$scope.fetchItems = function(start, end, callback) {
|
||||
var fields = _.pluck(this.fields, 'name');
|
||||
var criteria = {
|
||||
operator: "and",
|
||||
criteria: [{
|
||||
fieldName: view.start,
|
||||
operator: ">=",
|
||||
value: start
|
||||
}, {
|
||||
fieldName: view.start,
|
||||
operator: "<=",
|
||||
value: end
|
||||
}]
|
||||
};
|
||||
|
||||
// make sure to include items whose end date falls in current range
|
||||
if (view.stop) {
|
||||
criteria = {
|
||||
operator: "or",
|
||||
criteria: [criteria, {
|
||||
operator: "and",
|
||||
criteria: [{
|
||||
fieldName: view.stop,
|
||||
operator: ">=",
|
||||
value: start
|
||||
}, {
|
||||
fieldName: view.stop,
|
||||
operator: "<=",
|
||||
value: end
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// consider stored filter
|
||||
if (ds._filter) {
|
||||
if (ds._filter.criteria) {
|
||||
criteria = {
|
||||
operator: "and",
|
||||
criteria: [criteria].concat(ds._filter.criteria)
|
||||
};
|
||||
}
|
||||
if (_.size(ds._filter._domains) > 0) {
|
||||
criteria._domains = ds._filter._domains;
|
||||
}
|
||||
}
|
||||
|
||||
var opts = {
|
||||
fields: fields,
|
||||
filter: criteria,
|
||||
domain: this._domain,
|
||||
context: this._context,
|
||||
store: false,
|
||||
limit: -1
|
||||
};
|
||||
|
||||
ds.search(opts).success(function(records) {
|
||||
var items = _.clone(records);
|
||||
items.sort(function (x, y) { return x.id - y.id; });
|
||||
updateColors(items, true);
|
||||
callback(records);
|
||||
});
|
||||
};
|
||||
|
||||
function updateColors(records, reset) {
|
||||
var colorBy = view.color;
|
||||
var colorField = $scope.fields[colorBy];
|
||||
|
||||
if (!colorField) {
|
||||
return colors;
|
||||
}
|
||||
if (reset) {
|
||||
colors = {};
|
||||
}
|
||||
|
||||
_.each(records, function(record) {
|
||||
var item = record[colorBy];
|
||||
if (item === null || item === undefined) {
|
||||
return;
|
||||
}
|
||||
var key = $scope.getColorKey(record, item);
|
||||
var title = colorField.targetName ? item[colorField.targetName] : item;
|
||||
if (colorField.selectionList) {
|
||||
var select = _.find(colorField.selectionList, function (select) {
|
||||
return ("" + select.value) === ("" + title);
|
||||
});
|
||||
if (select) {
|
||||
title = select.title;
|
||||
}
|
||||
}
|
||||
if (!colors[key]) {
|
||||
colors[key] = {
|
||||
item: item,
|
||||
title: title || _t('Unknown'),
|
||||
color: nextColor(_.size(colors))
|
||||
};
|
||||
}
|
||||
record.$colorKey = key;
|
||||
});
|
||||
return colors;
|
||||
}
|
||||
|
||||
$scope.getColors = function() {
|
||||
return colors;
|
||||
};
|
||||
|
||||
$scope.getColor = function(record) {
|
||||
var key = this.getColorKey(record);
|
||||
if (key && !colors[key]) {
|
||||
updateColors([record], false);
|
||||
}
|
||||
if (colors[key]) {
|
||||
return colors[key].color;
|
||||
}
|
||||
return nextColor(0);
|
||||
};
|
||||
|
||||
$scope.getColorKey = function(record, key) {
|
||||
if (key) {
|
||||
return "" + (key.id || key);
|
||||
}
|
||||
if (record) {
|
||||
return this.getColorKey(null, record[view.color]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.getEventInfo = function(record) {
|
||||
var info = {},
|
||||
value;
|
||||
|
||||
value = record[view.start];
|
||||
info.start = value ? moment(value) : moment();
|
||||
|
||||
value = record[view.stop];
|
||||
info.end = value ? moment(value) : moment(info.start).add(view.length || 1, "hours");
|
||||
|
||||
var title = this.fields[view.title];
|
||||
var titleText = null;
|
||||
|
||||
if (title) {
|
||||
value = record[title.name];
|
||||
if (title.targetName) {
|
||||
value = value[title.targetName];
|
||||
} else if (title.selectionList) {
|
||||
var select = _.find(title.selectionList, function (select) {
|
||||
return ("" + select.value) === ("" + value);
|
||||
});
|
||||
if (select) {
|
||||
titleText = select.title;
|
||||
}
|
||||
}
|
||||
titleText = titleText || value || _t('Unknown');
|
||||
}
|
||||
|
||||
info.title = ("" + titleText);
|
||||
info.allDay = isAllDay(info);
|
||||
info.className = info.allDay ? "calendar-event-allDay" : "calendar-event-day";
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
function isAllDay(event) {
|
||||
if($scope.fields[view.start] && $scope.fields[view.start].type === 'date') {
|
||||
return true;
|
||||
}
|
||||
var start = moment(event.start);
|
||||
var end = moment(event.end);
|
||||
if (start.format("HH:mm") !== "00:00") {
|
||||
return false;
|
||||
}
|
||||
return !event.end || end.format("HH:mm") === "00:00";
|
||||
}
|
||||
|
||||
$scope.onEventChange = function(event, delta) {
|
||||
|
||||
var record = _.clone(event.record);
|
||||
|
||||
var start = event.start;
|
||||
var end = event.end;
|
||||
|
||||
if (isAllDay(event)) {
|
||||
start = start.clone().startOf("day").local();
|
||||
end = (end || start).clone().startOf("day").local();
|
||||
}
|
||||
|
||||
record[view.start] = start;
|
||||
record[view.stop] = end;
|
||||
|
||||
$scope.record = record;
|
||||
|
||||
function reset() {
|
||||
$scope.record = null;
|
||||
}
|
||||
|
||||
function doSave() {
|
||||
return ds.save(record).success(function(res){
|
||||
return $scope.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
var handler = $scope.onChangeHandler;
|
||||
if (handler) {
|
||||
var promise = handler.onChange().then(function () {
|
||||
return doSave();
|
||||
});
|
||||
promise.success = function(fn) {
|
||||
promise.then(fn);
|
||||
return promise;
|
||||
};
|
||||
promise.error = function (fn) {
|
||||
promise.then(null, fn);
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
return doSave();
|
||||
};
|
||||
|
||||
$scope.removeEvent = function(event, callback) {
|
||||
ds.remove(event.record).success(callback);
|
||||
};
|
||||
|
||||
$scope.select = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
var args = [],
|
||||
query = {};
|
||||
|
||||
return {
|
||||
mode: 'calendar',
|
||||
args: args,
|
||||
query: query
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
|
||||
if (opts.mode === "calendar") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "calendar") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('axelor.ui').directive('uiViewCalendar', ['ViewService', 'ActionService', function(ViewService, ActionService) {
|
||||
|
||||
function link(scope, element, attrs, controller) {
|
||||
|
||||
var main = element.children('.calendar-main');
|
||||
var mini = element.find('.calendar-mini');
|
||||
var legend = element.find('.calendar-legend');
|
||||
|
||||
var ctx = (scope._viewParams.context||{});
|
||||
var params = (scope._viewParams.params||{});
|
||||
|
||||
var schema = scope.schema;
|
||||
var mode = ctx.calendarMode || params.calendarMode || schema.mode || "month";
|
||||
var date = ctx.calendarDate || params.calendarDate;
|
||||
|
||||
var editable = schema.editable === undefined ? true : schema.editable;
|
||||
var calRange = {};
|
||||
|
||||
var RecordManager = (function () {
|
||||
|
||||
var records = [],
|
||||
current = [];
|
||||
|
||||
function add(record) {
|
||||
if (!record || _.isArray(record)) {
|
||||
records = record || [];
|
||||
return filter();
|
||||
}
|
||||
var found = _.findWhere(records, {
|
||||
id: record.id
|
||||
});
|
||||
if (!found) {
|
||||
found = record;
|
||||
records.push(record);
|
||||
}
|
||||
if (found !== record) {
|
||||
_.extend(found, record);
|
||||
}
|
||||
return filter();
|
||||
}
|
||||
|
||||
function remove(record) {
|
||||
records = _.filter(records, function (item) {
|
||||
return record.id !== item.id;
|
||||
});
|
||||
return filter();
|
||||
}
|
||||
|
||||
function filter() {
|
||||
|
||||
var selected = [];
|
||||
|
||||
_.each(scope.getColors(), function (color) {
|
||||
if (color.checked) {
|
||||
selected.push(scope.getColorKey(null, color.item));
|
||||
}
|
||||
});
|
||||
|
||||
main.fullCalendar('removeEventSource', current);
|
||||
current = records;
|
||||
|
||||
if (selected.length) {
|
||||
current = _.filter(records, function(record) {
|
||||
return _.contains(selected, record.$colorKey);
|
||||
});
|
||||
}
|
||||
|
||||
main.fullCalendar('addEventSource', current);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
function events(start, end, timezone, callback) {
|
||||
calRange.start = start;
|
||||
calRange.end = end;
|
||||
scope._viewPromise.then(function(){
|
||||
scope.fetchItems(start, end, function(items) {
|
||||
callback([]);
|
||||
add(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
add: add,
|
||||
remove: remove,
|
||||
filter: filter,
|
||||
events: events
|
||||
};
|
||||
}());
|
||||
|
||||
if (date) {
|
||||
date = moment(date).toDate();
|
||||
}
|
||||
|
||||
mini.datepicker({
|
||||
showOtherMonths: true,
|
||||
selectOtherMonths: true,
|
||||
onSelect: function(dateStr) {
|
||||
main.fullCalendar('gotoDate', mini.datepicker('getDate'));
|
||||
}
|
||||
});
|
||||
|
||||
if (date) {
|
||||
mini.datepicker('setDate', date);
|
||||
}
|
||||
|
||||
var lang = axelor.config["user.lang"] || 'en';
|
||||
|
||||
var options = {
|
||||
|
||||
header: false,
|
||||
|
||||
timeFormat: 'h(:mm)t',
|
||||
|
||||
axisFormat: 'h(:mm)t',
|
||||
|
||||
timezone: 'local',
|
||||
|
||||
lang: lang,
|
||||
|
||||
editable: editable,
|
||||
|
||||
selectable: editable,
|
||||
|
||||
selectHelper: editable,
|
||||
|
||||
select: function(start, end) {
|
||||
var event = {
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
|
||||
// all day
|
||||
if (!start.hasTime() && !end.hasTime()) {
|
||||
event.start = start.clone().startOf("day").local();
|
||||
event.end = end.clone().startOf("day").local();
|
||||
}
|
||||
|
||||
scope.$applyAsync(function(){
|
||||
scope.showEditor(event);
|
||||
});
|
||||
main.fullCalendar('unselect');
|
||||
},
|
||||
|
||||
defaultDate: date,
|
||||
|
||||
events: RecordManager,
|
||||
|
||||
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
|
||||
hideBubble();
|
||||
scope.onEventChange(event, delta).error(function(){
|
||||
revertFunc();
|
||||
});
|
||||
},
|
||||
|
||||
eventResize: function( event, delta, revertFunc, jsEvent, ui, view ) {
|
||||
scope.onEventChange(event, delta).error(function(){
|
||||
revertFunc();
|
||||
});
|
||||
},
|
||||
|
||||
eventClick: function(event, jsEvent, view) {
|
||||
showBubble(event, jsEvent.target);
|
||||
},
|
||||
|
||||
eventDataTransform: function(record) {
|
||||
return updateEvent(null, record);
|
||||
},
|
||||
|
||||
viewDisplay: function(view) {
|
||||
hideBubble();
|
||||
mini.datepicker('setDate', main.fullCalendar('getDate'));
|
||||
},
|
||||
|
||||
allDayText: _t('All Day')
|
||||
};
|
||||
|
||||
if (lang.indexOf('fr') === 0) {
|
||||
_.extend(options, {
|
||||
timeFormat: 'H:mm',
|
||||
axisFormat: 'H:mm',
|
||||
firstDay: 1,
|
||||
|
||||
views: {
|
||||
week: {
|
||||
titleFormat: 'D MMM YYYY',
|
||||
columnFormat: 'ddd DD/MM'
|
||||
},
|
||||
|
||||
day: {
|
||||
titleFormat: 'D MMM YYYY'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main.fullCalendar(options);
|
||||
|
||||
var editor = null;
|
||||
var bubble = null;
|
||||
|
||||
function hideBubble() {
|
||||
if (bubble) {
|
||||
bubble.popover('destroy');
|
||||
bubble = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showBubble(event, elem) {
|
||||
hideBubble();
|
||||
bubble = $(elem).popover({
|
||||
html: true,
|
||||
title: "<b>" + event.title + "</b>",
|
||||
placement: "top",
|
||||
container: 'body',
|
||||
content: function() {
|
||||
var html = $("<div></div>").addClass("calendar-bubble-content");
|
||||
var start = event.start;
|
||||
var end = event.end && event.allDay ? moment(event.end).add(-1, 'second') : event.end;
|
||||
var singleDay = (event.allDay || !scope.isAgenda()) && (!end || moment(start).isSame(end, 'day'));
|
||||
var dateFormat = !scope.isAgenda() || event.allDay ? "ddd D MMM" : "ddd D MMM HH:mm";
|
||||
|
||||
$("<span>").text(moment(start).format(dateFormat)).appendTo(html);
|
||||
|
||||
if (schema.eventStop && end && !singleDay) {
|
||||
$("<span> - </span>").appendTo(html);
|
||||
$("<span>").text(moment(end).format(dateFormat)).appendTo(html);
|
||||
}
|
||||
|
||||
$("<hr>").appendTo(html);
|
||||
|
||||
if (scope.isEditable()) {
|
||||
$('<a href="javascript: void(0)" style="margin-right: 5px;"></a>').text(_t("Delete"))
|
||||
.appendTo(html)
|
||||
.click(function(e){
|
||||
hideBubble();
|
||||
scope.$applyAsync(function(){
|
||||
scope.removeEvent(event, function() {
|
||||
RecordManager.remove(event.record);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('<a class="pull-right" href="javascript: void(0)"></a>')
|
||||
.append(_t("Edit event")).append("<strong> »</strong>")
|
||||
.appendTo(html)
|
||||
.click(function(e){
|
||||
hideBubble();
|
||||
scope.$applyAsync(function(){
|
||||
scope.showEditor(event);
|
||||
});
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
|
||||
bubble.popover('show');
|
||||
}
|
||||
|
||||
$("body").on("mousedown", function(e){
|
||||
var elem = $(e.target || e.srcElement);
|
||||
if (!bubble || bubble.is(elem) || bubble.has(elem).length) {
|
||||
return;
|
||||
}
|
||||
if (!elem.parents().is(".popover")) {
|
||||
hideBubble();
|
||||
}
|
||||
});
|
||||
|
||||
function updateEvent(event, record) {
|
||||
if (!event || !event.id) {
|
||||
var color = scope.getColor(record);
|
||||
event = {
|
||||
id: record.id,
|
||||
record: record,
|
||||
backgroundColor: color.bg,
|
||||
borderColor: color.bc,
|
||||
textColor: color.fg
|
||||
};
|
||||
} else {
|
||||
_.extend(event.record, record);
|
||||
}
|
||||
|
||||
event = _.extend(event, scope.getEventInfo(record));
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
scope.editorCanSave = true;
|
||||
|
||||
scope.showEditor = function(event) {
|
||||
|
||||
var view = this.schema;
|
||||
var record = _.extend({}, event.record);
|
||||
|
||||
record[view.eventStart] = event.start;
|
||||
record[view.eventStop] = event.end;
|
||||
|
||||
if (!editor) {
|
||||
editor = ViewService.compile('<div ui-editor-popup></div>')(scope.$new());
|
||||
editor.data('$target', element);
|
||||
}
|
||||
|
||||
var popup = editor.isolateScope();
|
||||
|
||||
popup.show(record, function(result) {
|
||||
RecordManager.add(result);
|
||||
});
|
||||
popup.waitForActions(function() {
|
||||
if (!record || !record.id) {
|
||||
popup.$broadcast("on:new");
|
||||
} else {
|
||||
popup.setEditable(scope.isEditable());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.isEditable = function() {
|
||||
return editable;
|
||||
};
|
||||
|
||||
scope.refresh = function(record) {
|
||||
if (calRange.start && calRange.end) {
|
||||
return RecordManager.events(calRange.start, calRange.end, options.timezone, function () {});
|
||||
}
|
||||
return main.fullCalendar("refetchEvents");
|
||||
};
|
||||
|
||||
scope.filterEvents = function() {
|
||||
RecordManager.filter();
|
||||
};
|
||||
|
||||
scope.pagerText = function() {
|
||||
return main.fullCalendar("getView").title;
|
||||
};
|
||||
|
||||
scope.isMode = function(name) {
|
||||
return mode === name;
|
||||
};
|
||||
|
||||
scope.onMode = function(name) {
|
||||
mode = name;
|
||||
if (name === "week") {
|
||||
name = scope.isAgenda() ? "agendaWeek" : "basicWeek";
|
||||
}
|
||||
if (name === "day") {
|
||||
name = scope.isAgenda() ? "agendaDay" : "basicDay";
|
||||
}
|
||||
main.fullCalendar("changeView", name);
|
||||
};
|
||||
|
||||
scope.onRefresh = function () {
|
||||
scope.refresh();
|
||||
};
|
||||
|
||||
scope.onNext = function() {
|
||||
main.fullCalendar('next');
|
||||
};
|
||||
|
||||
scope.onPrev = function() {
|
||||
main.fullCalendar('prev');
|
||||
};
|
||||
|
||||
scope.onToday = function() {
|
||||
main.fullCalendar('today');
|
||||
};
|
||||
|
||||
scope.onChangeHandler = null;
|
||||
if (schema.onChange) {
|
||||
scope.onChangeHandler = ActionService.handler(scope, element, {
|
||||
action: schema.onChange
|
||||
});
|
||||
}
|
||||
|
||||
function adjustSize() {
|
||||
if (main.is(':hidden')) {
|
||||
return;
|
||||
}
|
||||
hideBubble();
|
||||
main.fullCalendar('render');
|
||||
main.fullCalendar('option', 'height', element.height());
|
||||
legend.css("max-height", legend.parent().height() - mini.height()
|
||||
- (parseInt(legend.css('marginTop')) || 0)
|
||||
- (parseInt(legend.css('marginBottom')) || 0));
|
||||
}
|
||||
|
||||
scope.$onAdjust(adjustSize, 100);
|
||||
|
||||
scope.$callWhen(function () {
|
||||
return main.is(':visible');
|
||||
}, function() {
|
||||
element.parents('.view-container:first').css('overflow', 'inherit');
|
||||
scope.onMode(mode);
|
||||
adjustSize();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return {
|
||||
link: function(scope, element, attrs, controller) {
|
||||
scope._viewPromise.then(function(){
|
||||
link(scope, element, attrs, controller);
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div>'+
|
||||
'<div class="calendar-main" ui-attach-scroll=".fc-scroller"></div>'+
|
||||
'<div class="calendar-side">'+
|
||||
'<div class="calendar-mini"></div>'+
|
||||
'<form class="form calendar-legend">'+
|
||||
'<label class="checkbox" ng-repeat="color in getColors()" style="color: {{color.color.bc}}">'+
|
||||
'<input type="checkbox" ng-click="filterEvents()" ng-model="color.checked"> {{color.title}}</label>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
991
sophal/js/view/view.chart.js
Normal file
991
sophal/js/view/view.chart.js
Normal file
@ -0,0 +1,991 @@
|
||||
/*
|
||||
* 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(){
|
||||
|
||||
/* global d3: true, nv: true, D3Funnel: true, RadarChart: true, GaugeChart: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.ChartCtrl = ChartCtrl;
|
||||
ui.ChartCtrl.$inject = ['$scope', '$element', '$http', 'ActionService'];
|
||||
|
||||
function ChartCtrl($scope, $element, $http, ActionService) {
|
||||
|
||||
var views = $scope._views;
|
||||
var view = $scope.view = views.chart;
|
||||
|
||||
var viewChart = null;
|
||||
var searchScope = null;
|
||||
var clickHandler = null;
|
||||
var actionHandler = null;
|
||||
|
||||
var loading = false;
|
||||
var unwatch = null;
|
||||
|
||||
function refresh() {
|
||||
|
||||
if (viewChart && searchScope && $scope.searchFields && !searchScope.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var context = $scope._context || {};
|
||||
if ($scope.getContext) {
|
||||
context = _.extend({}, $scope.getContext(), context);
|
||||
}
|
||||
|
||||
if (searchScope) {
|
||||
context = _.extend({}, context, searchScope.getContext());
|
||||
}
|
||||
|
||||
context = _.extend({}, context, { _domainAction: $scope._viewAction });
|
||||
loading = true;
|
||||
|
||||
var params = {
|
||||
data: context
|
||||
};
|
||||
|
||||
if (viewChart) {
|
||||
params.fields = ['dataset'];
|
||||
}
|
||||
|
||||
return $http.post('ws/meta/chart/' + view.name, params).then(function(response) {
|
||||
var res = response.data;
|
||||
var data = res.data;
|
||||
var isInitial = viewChart === null;
|
||||
|
||||
if (viewChart === null) {
|
||||
viewChart = data;
|
||||
if (data.config && data.config.onClick) {
|
||||
clickHandler = ActionService.handler($scope, $element, {
|
||||
action: data.config.onClick
|
||||
});
|
||||
}
|
||||
if (data.config && data.config.onAction) {
|
||||
actionHandler = ActionService.handler($scope, $element, {
|
||||
action: data.config.onAction
|
||||
});
|
||||
}
|
||||
if (data.config && data.config.onActionTitle) {
|
||||
$scope.actionTitle = data.config.onActionTitle;
|
||||
}
|
||||
} else {
|
||||
data = _.extend({}, viewChart, data);
|
||||
}
|
||||
|
||||
if ($scope.searchFields === undefined && data.search) {
|
||||
$scope.searchFields = data.search;
|
||||
$scope.searchInit = data.onInit;
|
||||
$scope.usingSQL = data.usingSQL;
|
||||
} else {
|
||||
$scope.render(data);
|
||||
if (isInitial) {
|
||||
refresh(); // force loading data
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
}, function () {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.setSearchScope = function (formScope) {
|
||||
searchScope = formScope;
|
||||
};
|
||||
|
||||
$scope.hasAction = function () {
|
||||
return !!actionHandler;
|
||||
};
|
||||
|
||||
$scope.handleAction = function (data) {
|
||||
if (actionHandler) {
|
||||
actionHandler._getContext = function () {
|
||||
return _.extend({}, { _data: data }, {
|
||||
_model: $scope._model || 'com.axelor.meta.db.MetaView',
|
||||
_chart: view.name
|
||||
});
|
||||
};
|
||||
actionHandler.handle();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleClick = function (e) {
|
||||
if (clickHandler) {
|
||||
clickHandler._getContext = function () {
|
||||
return _.extend({}, e.data.raw, {
|
||||
_model: $scope._model || 'com.axelor.meta.db.MetaView',
|
||||
_chart: view.name
|
||||
});
|
||||
};
|
||||
clickHandler.handle();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onRefresh = function(force) {
|
||||
if (unwatch || loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in case of onInit
|
||||
if ($scope.searchInit && !(searchScope||{}).record && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch = $scope.$watch(function chartRefreshWatch() {
|
||||
if ($element.is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.render = function(data) {
|
||||
|
||||
};
|
||||
|
||||
// refresh to load chart
|
||||
$scope.onRefresh();
|
||||
}
|
||||
|
||||
ChartFormCtrl.$inject = ['$scope', '$element', 'ViewService', 'DataSource'];
|
||||
function ChartFormCtrl($scope, $element, ViewService, DataSource) {
|
||||
|
||||
$scope._dataSource = DataSource.create('com.axelor.meta.db.MetaView');
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
$scope.setEditable();
|
||||
$scope.setSearchScope($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;
|
||||
}
|
||||
|
||||
var unwatch = $scope.$watch('searchFields', function chartSearchFieldsWatch(fields) {
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
|
||||
var meta = { fields: fixFields(fields) };
|
||||
var view = {
|
||||
type: 'form',
|
||||
items: [{
|
||||
type: 'panel',
|
||||
noframe: true,
|
||||
items: _.map(meta.fields, function (item) {
|
||||
var props = _.extend({}, item, {
|
||||
showTitle: false,
|
||||
placeholder: item.title || item.autoTitle
|
||||
});
|
||||
if (item.multiple && (item.target || item.selection)) {
|
||||
item.widget = item.target ? "TagSelect" : "MultiSelect";
|
||||
}
|
||||
return props;
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
ViewService.process(meta, view);
|
||||
|
||||
view.onLoad = $scope.searchInit;
|
||||
|
||||
$scope.fields = meta.fields;
|
||||
$scope.schema = view;
|
||||
$scope.schema.loaded = true;
|
||||
|
||||
var interval;
|
||||
|
||||
function reload() {
|
||||
$scope.$parent.onRefresh();
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
|
||||
function delayedReload() {
|
||||
clearTimeout(interval);
|
||||
interval = setTimeout(reload, 500);
|
||||
}
|
||||
|
||||
function onNewOrEdit() {
|
||||
if ($scope.$events.onLoad) {
|
||||
$scope.$events.onLoad().then(delayedReload);
|
||||
}
|
||||
}
|
||||
|
||||
var __getContext = $scope.getContext;
|
||||
$scope.getContext = function () {
|
||||
var ctx = __getContext.call(this);
|
||||
_.each(meta.fields, function (item) {
|
||||
if (item.multiple && (item.target || item.selection)) {
|
||||
var value = ctx[item.name];
|
||||
if (_.isArray(value)) value = _.pluck(value, "id");
|
||||
if (_.isString(value)) value = value.split(/\s*,\s*/g);
|
||||
ctx[item.name] = value;
|
||||
} else if (item.target && $scope.usingSQL) {
|
||||
var value = ctx[item.name];
|
||||
if (value) {
|
||||
ctx[item.name] = value.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
return ctx;
|
||||
};
|
||||
|
||||
$scope.$on('on:new', onNewOrEdit);
|
||||
$scope.$on('on:edit', onNewOrEdit);
|
||||
|
||||
$scope.$watch('record', function chartSearchRecordWatch(record) {
|
||||
if (interval === undefined) {
|
||||
interval = null;
|
||||
return;
|
||||
}
|
||||
if ($scope.isValid()) delayedReload();
|
||||
}, true);
|
||||
|
||||
$scope.$watch('$events.onLoad', function chartOnLoadWatch(handler) {
|
||||
if (handler) {
|
||||
handler().then(delayedReload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function $conv(value, type) {
|
||||
if (!value && type === 'text') return 'N/A';
|
||||
if (!value) return 0;
|
||||
if (_.isNumber(value)) return value;
|
||||
if (/^(-)?\d+(\.\d+)?$/.test(value)) {
|
||||
return +value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function applyXY(chart, data) {
|
||||
var type = data.xType;
|
||||
chart.y(function (d) { return d.y; });
|
||||
if (type == "date") {
|
||||
return chart.x(function (d) { return moment(d.x).toDate(); });
|
||||
}
|
||||
return chart.x(function (d) { return d.x; });
|
||||
}
|
||||
|
||||
var themes = {
|
||||
|
||||
// default
|
||||
d3: d3.scale.category10().range(),
|
||||
|
||||
// material
|
||||
material: [
|
||||
'#f44336', // Red
|
||||
'#E91E63', // Pink
|
||||
'#9c27b0', // Purple
|
||||
'#673ab7', // Deep Purple
|
||||
'#3f51b5', // Indigo
|
||||
'#2196F3', // Blue
|
||||
'#03a9f4', // Light Blue
|
||||
'#00bcd4', // Cyan
|
||||
'#009688', // Teal
|
||||
'#4caf50', // Green
|
||||
'#8bc34a', // Light Green
|
||||
'#cddc39', // Lime
|
||||
'#ffeb3b', // Yellow
|
||||
'#ffc107', // Amber
|
||||
'#ff9800', // Orange
|
||||
'#ff5722', // Deep Orange
|
||||
'#795548', // Brown
|
||||
'#9e9e9e', // Grey
|
||||
'#607d8b', // Blue Grey
|
||||
],
|
||||
|
||||
// chart.js
|
||||
chartjs: [
|
||||
'#ff6384', '#ff9f40', '#ffcd56', '#4bc0c0',
|
||||
'#36a2eb', '#9966ff', '#c9cbcf',
|
||||
],
|
||||
|
||||
// echart - roma
|
||||
roma: [
|
||||
'#E01F54','#001852','#f5e8c8','#b8d2c7','#c6b38e',
|
||||
'#a4d8c2','#f3d999','#d3758f','#dcc392','#2e4783',
|
||||
'#82b6e9','#ff6347','#a092f1','#0a915d','#eaf889',
|
||||
'#6699FF','#ff6666','#3cb371','#d5b158','#38b6b6',
|
||||
],
|
||||
|
||||
// echart - macarons
|
||||
macarons: [
|
||||
'#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80',
|
||||
'#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa',
|
||||
'#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050',
|
||||
'#59678c','#c9ab00','#7eb00a','#6f5553','#c14089',
|
||||
]
|
||||
};
|
||||
|
||||
function colors(names, shades, type) {
|
||||
var given = themes[names] ? themes[names] : names;
|
||||
given = given || themes.material;
|
||||
given = _.isArray(given) ? given : given.split(',');
|
||||
if (given && shades > 1) {
|
||||
var n = Math.max(0, Math.min(+(shades) || 4, 4));
|
||||
return _.flatten(given.map(function (c) {
|
||||
return _.first(_.range(0, n + 1).map(d3.scale.linear().domain([0, n + 1]).range([c, 'white'])), n);
|
||||
}));
|
||||
}
|
||||
return given;
|
||||
}
|
||||
|
||||
var CHARTS = {};
|
||||
|
||||
function PlusData(series, data) {
|
||||
var result = _.chain(data.dataset)
|
||||
.groupBy(data.xAxis)
|
||||
.map(function (group, name) {
|
||||
var value = 0;
|
||||
_.each(group, function (item) {
|
||||
value += $conv(item[series.key]);
|
||||
});
|
||||
var raw = {};
|
||||
if (group[0]) {
|
||||
raw[data.xAxis] = name;
|
||||
raw[series.key] = value;
|
||||
raw[data.xAxis + 'Id'] = group[0][data.xAxis + 'Id'];
|
||||
}
|
||||
return {
|
||||
x: name === 'null' ? 'N/A' : name,
|
||||
y: value,
|
||||
raw: raw
|
||||
};
|
||||
}).value();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function PlotData(series, data) {
|
||||
var ticks = _.chain(data.dataset).pluck(data.xAxis).unique().map(function (v) { return $conv(v, data.xType); }).value();
|
||||
var groupBy = series.groupBy;
|
||||
var datum = [];
|
||||
|
||||
_.chain(data.dataset).groupBy(groupBy)
|
||||
.map(function (group, groupName) {
|
||||
var name = groupBy ? groupName : null;
|
||||
var values = _.map(group, function (item) {
|
||||
var x = $conv(item[data.xAxis], data.xType) || 0;
|
||||
var y = $conv(item[series.key] || name || 0);
|
||||
return { x: x, y: y, raw: item };
|
||||
});
|
||||
|
||||
var my = _.pluck(values, 'x');
|
||||
var missing = _.difference(ticks, my);
|
||||
if (ticks.length === missing.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(missing, function(x) {
|
||||
values.push({ x: x, y: 0 });
|
||||
});
|
||||
|
||||
values = _.sortBy(values, 'x');
|
||||
|
||||
datum.push({
|
||||
key: name || series.title,
|
||||
type: series.type,
|
||||
values: values
|
||||
});
|
||||
});
|
||||
|
||||
return datum;
|
||||
}
|
||||
|
||||
function PieChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlusData(series, data);
|
||||
var config = data.config || {};
|
||||
|
||||
var chart = nv.models.pieChart()
|
||||
.showLabels(false)
|
||||
.height(null)
|
||||
.width(null)
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
if (series.type === "donut") {
|
||||
chart.donut(true)
|
||||
.donutRatio(0.40);
|
||||
}
|
||||
|
||||
if (_.toBoolean(config.percent)) {
|
||||
chart.showLabels(true)
|
||||
.labelType("percent")
|
||||
.labelThreshold(0.05);
|
||||
}
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(1200).call(chart);
|
||||
|
||||
chart.pie.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.pie = PieChart;
|
||||
CHARTS.donut = PieChart;
|
||||
|
||||
function DBarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlusData(series, data);
|
||||
|
||||
datum = [{
|
||||
key: data.title,
|
||||
values: datum
|
||||
}];
|
||||
|
||||
var chart = nv.models.discreteBarChart()
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; })
|
||||
.staggerLabels(true)
|
||||
.showValues(true);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.discretebar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function BarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.multiBarChart()
|
||||
.reduceXTicks(false);
|
||||
|
||||
chart.multibar.hideable(true);
|
||||
chart.stacked(data.stacked);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function HBarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.multiBarHorizontalChart();
|
||||
|
||||
chart.stacked(data.stacked);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function FunnelChart(scope, element, data) {
|
||||
|
||||
if(!data.dataset){
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = new D3Funnel(element[0]);
|
||||
var w = element.width();
|
||||
var h = element.height();
|
||||
var config = _.extend({}, data.config);
|
||||
var props = {
|
||||
fillType: 'gradient',
|
||||
hoverEffects: true,
|
||||
dynamicArea: true,
|
||||
animation: 200};
|
||||
|
||||
if(config.width){
|
||||
props.width = w*config.width/100;
|
||||
}
|
||||
if(config.height){
|
||||
props.height = h*config.height/100;
|
||||
}
|
||||
|
||||
var series = _.first(data.series) || {};
|
||||
var opts = [];
|
||||
_.each(data.dataset, function(dat){
|
||||
opts.push([dat[data.xAxis],($conv(dat[series.key])||0)]);
|
||||
});
|
||||
chart.draw(opts, props);
|
||||
|
||||
chart.update = function(){};
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.bar = BarChart;
|
||||
CHARTS.dbar = DBarChart;
|
||||
CHARTS.hbar = HBarChart;
|
||||
CHARTS.funnel = FunnelChart;
|
||||
|
||||
function LineChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.lineChart()
|
||||
.showLegend(true)
|
||||
.showYAxis(true)
|
||||
.showXAxis(true);
|
||||
|
||||
applyXY(chart, data);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function AreaChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.stackedAreaChart();
|
||||
|
||||
applyXY(chart, data);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.line = LineChart;
|
||||
CHARTS.area = AreaChart;
|
||||
|
||||
function RadarCharter(scope, element, data) {
|
||||
|
||||
var result = _.map(data.dataset, function(item) {
|
||||
return _.map(data.series, function(s) {
|
||||
var title = s.title || s.key,
|
||||
value = item[s.key];
|
||||
return {
|
||||
axis: title,
|
||||
value: $conv(value) || 0
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
var id = _.uniqueId('_radarChart'),
|
||||
parent = element.parent();
|
||||
|
||||
parent.attr('id', id)
|
||||
.addClass('radar-chart')
|
||||
.empty();
|
||||
|
||||
var size = Math.min(parent.innerWidth(), parent.innerHeight());
|
||||
|
||||
RadarChart.draw('#'+id, result, {
|
||||
w: size,
|
||||
h: size
|
||||
});
|
||||
|
||||
parent.children('svg')
|
||||
.css('width', 'auto')
|
||||
.css('margin', 'auto')
|
||||
.css('margin-top', 10);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function GaugeCharter(scope, element, data) {
|
||||
|
||||
var config = data.config,
|
||||
min = +(config.min) || 0,
|
||||
max = +(config.max) || 100,
|
||||
value = 0;
|
||||
|
||||
var item = _.first(data.dataset),
|
||||
series = _.first(data.series),
|
||||
key = series.key || data.xAxis;
|
||||
|
||||
if (item) {
|
||||
value = item[key] || value;
|
||||
}
|
||||
|
||||
var w = element.width();
|
||||
var h = element.height();
|
||||
|
||||
var parent = element.hide().parent();
|
||||
|
||||
parent.children('svg').remove();
|
||||
parent.append(element);
|
||||
|
||||
var chart = GaugeChart(parent[0], {
|
||||
size: 300,
|
||||
clipWidth: 300,
|
||||
clipHeight: h,
|
||||
ringWidth: 60,
|
||||
minValue: min,
|
||||
maxValue: max,
|
||||
transitionMs: 4000
|
||||
});
|
||||
|
||||
chart.render();
|
||||
chart.update(value);
|
||||
|
||||
parent.children('svg:last')
|
||||
.css('display', 'block')
|
||||
.css('width', 'auto')
|
||||
.css('margin', 'auto')
|
||||
.css('margin-top', 0);
|
||||
}
|
||||
|
||||
function TextChart(scope, element, data) {
|
||||
|
||||
var config = _.extend({
|
||||
strong: true,
|
||||
shadow: false,
|
||||
fontSize: 22
|
||||
}, data.config);
|
||||
|
||||
var values = _.first(data.dataset) || {};
|
||||
var series = _.first(data.series) || {};
|
||||
|
||||
var value = values[series.key];
|
||||
|
||||
if (config.format) {
|
||||
value = _t(config.format, value);
|
||||
}
|
||||
|
||||
var svg = d3.select(element.empty()[0]);
|
||||
var text = svg.append("svg:text")
|
||||
.attr("x", "50%")
|
||||
.attr("y", "50%")
|
||||
.attr("dy", ".3em")
|
||||
.attr("text-anchor", "middle")
|
||||
.text(value);
|
||||
|
||||
if (config.color) text.attr("fill", config.color);
|
||||
if (config.fontSize) text.style("font-size", config.fontSize);
|
||||
if (_.toBoolean(config.strong)) text.style("font-weight", "bold");
|
||||
if (_.toBoolean(config.shadow)) text.style("text-shadow", "0 1px 2px rgba(0, 0, 0, .5)");
|
||||
}
|
||||
|
||||
CHARTS.text = TextChart;
|
||||
CHARTS.radar = RadarCharter;
|
||||
CHARTS.gauge = GaugeCharter;
|
||||
|
||||
function Chart(scope, element, data) {
|
||||
|
||||
var type = null;
|
||||
var config = data.config || {};
|
||||
|
||||
for(var i = 0 ; i < data.series.length ; i++) {
|
||||
type = data.series[i].type;
|
||||
if (type === "bar" && !data.series[i].groupBy) type = "dbar";
|
||||
if (type === "pie" || type === "dbar" || type === "radar" || type === "gauge") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "pie" && data.series.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== "radar" && data.series.length > 1) {
|
||||
type = "multi";
|
||||
}
|
||||
|
||||
// clean up last instance
|
||||
(function () {
|
||||
var chart = element.off('adjustSize').empty().data('chart');
|
||||
if (chart && chart.tooltip && chart.tooltip.id) {
|
||||
d3.select('#' + chart.tooltip.id()).remove();
|
||||
}
|
||||
})();
|
||||
|
||||
nv.addGraph(function generate() {
|
||||
|
||||
var noData = _t('No records found.');
|
||||
if (data.dataset && data.dataset.stacktrace) {
|
||||
noData = data.dataset.message;
|
||||
data.dataset = [];
|
||||
}
|
||||
|
||||
var maker = CHARTS[type] || CHARTS.bar || function () {};
|
||||
var chart = maker(scope, element, data);
|
||||
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// series scale attribute
|
||||
var series = _.first(data.series);
|
||||
var scale = series && series.scale;
|
||||
|
||||
// format as integer if no scale is specified
|
||||
// and data has integer series values
|
||||
if (!isInteger(scale) && hasIntegerValues(data)) {
|
||||
scale = 0;
|
||||
}
|
||||
|
||||
if (isInteger(scale)) {
|
||||
var format = '.' + scale + 'f';
|
||||
chart.yAxis && chart.yAxis.tickFormat(d3.format(format));
|
||||
chart.valueFormat && chart.valueFormat(d3.format(format));
|
||||
}
|
||||
|
||||
if (chart.color) {
|
||||
chart.color(colors(config.colors, config.shades, type));
|
||||
}
|
||||
|
||||
if (chart.noData) {
|
||||
chart.noData(noData);
|
||||
}
|
||||
if(chart.controlLabels) {
|
||||
chart.controlLabels({
|
||||
grouped: _t('Grouped'),
|
||||
stacked: _t('Stacked'),
|
||||
stream: _t('Stream'),
|
||||
expanded: _t('Expanded'),
|
||||
stack_percent: _t('Stack %')
|
||||
});
|
||||
}
|
||||
|
||||
var tickFormats = {
|
||||
"date" : function (d) {
|
||||
var f = config.xFormat;
|
||||
return moment(d).format(f || 'YYYY-MM-DD');
|
||||
},
|
||||
"month" : function(d) {
|
||||
var v = "" + d;
|
||||
var f = config.xFormat;
|
||||
if (v.indexOf(".") > -1) return "";
|
||||
if (_.isString(d) && /^(\d+)$/.test(d)) {
|
||||
d = parseInt(d);
|
||||
}
|
||||
if (_.isNumber(d)) {
|
||||
return moment([moment().year(), d - 1, 1]).format(f || "MMM");
|
||||
}
|
||||
if (_.isString(d) && d.indexOf('-') > 0) {
|
||||
return moment(d).format(f || 'MMM, YYYY');
|
||||
}
|
||||
return d;
|
||||
},
|
||||
"year" : function(d) {
|
||||
return moment([moment().year(), d - 1, 1]).format("YYYY");
|
||||
},
|
||||
"number": d3.format(',f'),
|
||||
"decimal": d3.format(',.1f'),
|
||||
"text": function(d) { return d; }
|
||||
};
|
||||
|
||||
var tickFormat = tickFormats[data.xType];
|
||||
if (chart.xAxis && tickFormat) {
|
||||
chart.xAxis
|
||||
.rotateLabels(-45)
|
||||
.tickFormat(tickFormat);
|
||||
}
|
||||
|
||||
if (chart.yAxis && data.yTitle) {
|
||||
chart.yAxis.axisLabel(data.yTitle);
|
||||
}
|
||||
|
||||
var margin = data.xType === 'date' ? { 'bottom': 65 } : null;
|
||||
['top', 'left', 'bottom', 'right'].forEach(function (side) {
|
||||
var key = 'margin-' + side;
|
||||
var val = parseInt(config[key]);
|
||||
if (val) {
|
||||
(margin||(margin={}))[side] = val;
|
||||
}
|
||||
});
|
||||
if (chart.margin && margin) {
|
||||
chart.margin(margin);
|
||||
}
|
||||
|
||||
var lastWidth = 0;
|
||||
var lastHeight = 0;
|
||||
|
||||
function adjust() {
|
||||
|
||||
if (!element[0] || element.parent().is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = element[0].getBoundingClientRect();
|
||||
var w = rect.width,
|
||||
h = rect.height;
|
||||
|
||||
if (w === lastWidth && h === lastHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastWidth = w;
|
||||
lastHeight = h;
|
||||
|
||||
chart.update();
|
||||
}
|
||||
|
||||
element.data('chart', chart);
|
||||
scope.$onAdjust(adjust, 100);
|
||||
setTimeout(chart.update, 10);
|
||||
|
||||
return chart;
|
||||
});
|
||||
}
|
||||
|
||||
function hasIntegerValues(data) {
|
||||
var series = _.first(data.series);
|
||||
var dataset = _.first(data.dataset);
|
||||
return series && dataset && isInteger(dataset[series.key]);
|
||||
}
|
||||
|
||||
function isInteger(n) {
|
||||
return (n ^ 0) === n;
|
||||
}
|
||||
|
||||
var directiveFn = function(){
|
||||
return {
|
||||
controller: ChartCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var svg = element.children('svg');
|
||||
var form = element.children('.chart-controls');
|
||||
|
||||
function doExport(data) {
|
||||
var dataset = data.dataset || [];
|
||||
var header = [];
|
||||
_.each(dataset, function (item) {
|
||||
header = _.unique(_.flatten([header, _.keys(item)]));
|
||||
|
||||
});
|
||||
|
||||
var content = "data:text/csv;charset=utf-8," + header.join(';') + '\n';
|
||||
|
||||
dataset.forEach(function (item) {
|
||||
var row = header.map(function (key) {
|
||||
var val = item[key];
|
||||
if (val === undefined || val === null) {
|
||||
val = '';
|
||||
}
|
||||
return '"' + (''+val).replace(/"/g, '""') + '"';
|
||||
});
|
||||
content += row.join(';') + '\n';
|
||||
});
|
||||
|
||||
var name = (data.title || 'export').toLowerCase();
|
||||
ui.download(encodeURI(content), _.underscored(name) + '.csv');
|
||||
}
|
||||
|
||||
scope.render = function(data) {
|
||||
if (element.is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
svg.height(element.height() - form.height()).width('100%');
|
||||
if (!scope.dashlet || !scope.dashlet.title) {
|
||||
scope.title = data.title;
|
||||
}
|
||||
Chart(scope, svg, data);
|
||||
var canExport = data && _.isArray(data.dataset);
|
||||
scope.canExport = function () {
|
||||
return canExport;
|
||||
};
|
||||
scope.onExport = function () {
|
||||
doExport(data);
|
||||
};
|
||||
scope.onAction = function () {
|
||||
scope.handleAction(data && data.dataset);
|
||||
};
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
function onNewOrEdit() {
|
||||
if (scope.searchInit && scope.searchFields) {
|
||||
return;
|
||||
}
|
||||
scope.onRefresh(true);
|
||||
}
|
||||
|
||||
scope.$on('on:new', onNewOrEdit);
|
||||
scope.$on('on:edit', onNewOrEdit);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="chart-container" style="background-color: white; ">'+
|
||||
'<div ui-chart-form></div>'+
|
||||
'<svg></svg>'+
|
||||
'</div>'
|
||||
};
|
||||
};
|
||||
|
||||
ui.directive('uiChartForm', function () {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
controller: ChartFormCtrl,
|
||||
link: function (scope, element, attrs, ctrls) {
|
||||
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='chart-controls'>" +
|
||||
"<div ui-view-form x-handler='this'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
ui.directive('uiViewChart', directiveFn);
|
||||
ui.directive('uiPortletChart', directiveFn);
|
||||
|
||||
})();
|
||||
258
sophal/js/view/view.custom.js
Normal file
258
sophal/js/view/view.custom.js
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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");
|
||||
|
||||
CustomViewCtrl.$inject = ['$scope', '$http', 'DataSource', 'ViewService'];
|
||||
function CustomViewCtrl($scope, $http, DataSource, ViewService) {
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
|
||||
var view = $scope._views.custom || {};
|
||||
var viewPromise = null;
|
||||
|
||||
$scope.show = function() {
|
||||
if (!viewPromise) {
|
||||
viewPromise = $scope.loadView('custom', view.name);
|
||||
viewPromise.then(function(meta) {
|
||||
var schema = meta.view;
|
||||
$scope.schema = schema;
|
||||
$scope.schema.loaded = true;
|
||||
});
|
||||
}
|
||||
$scope.onShow(viewPromise);
|
||||
};
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
// it will be refreshed by dashlet
|
||||
if ($scope.dashlet) {
|
||||
return;
|
||||
}
|
||||
viewPromise.then(function () {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'custom'
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
if (opts.mode === "custom") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "custom") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getContext = function () {
|
||||
var context = $scope._context || {};
|
||||
if ($scope.$parent.getContext) {
|
||||
context = _.extend({}, $scope.$parent.getContext(), context);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
$scope.onRefresh = function() {
|
||||
var context = $scope.getContext();
|
||||
var params = {
|
||||
data: context
|
||||
};
|
||||
return $http.post('ws/meta/custom/' + view.name, params).then(function(response) {
|
||||
var res = response.data;
|
||||
$scope.data = (res.data||{}).dataset;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var customDirective = ["$compile", function ($compile) {
|
||||
return {
|
||||
controller: CustomViewCtrl,
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
|
||||
var evalScope = axelor.$evalScope(scope);
|
||||
|
||||
function render(template) {
|
||||
var elem = $('<span>' + axelor.sanitize(template.trim()) + '</span>');
|
||||
if (elem.children().length === 1) {
|
||||
elem = elem.children().first();
|
||||
}
|
||||
if (scope.schema && scope.schema.css) {
|
||||
element.parents(".dashlet:first").addClass(scope.schema.css);
|
||||
}
|
||||
|
||||
elem = $compile(elem)(evalScope);
|
||||
element.append(elem);
|
||||
}
|
||||
|
||||
var unwatch = scope.$watch('schema.template', function customTemplateWatch(template) {
|
||||
if (template) {
|
||||
unwatch();
|
||||
render(template);
|
||||
}
|
||||
});
|
||||
|
||||
scope.showToggle = false;
|
||||
|
||||
scope.$watch('data', function customDataWatch(data) {
|
||||
evalScope.data = data;
|
||||
evalScope.first = _.first(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
ui.directive('uiCustomView', customDirective);
|
||||
ui.directive('uiPortletCustom', customDirective);
|
||||
|
||||
// helper directives
|
||||
ui.directive('reportBox', function() {
|
||||
return {
|
||||
scope: {
|
||||
value: '=',
|
||||
label: '@',
|
||||
percent: '=',
|
||||
up: '=',
|
||||
tag: '=',
|
||||
tagCss: '='
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
setTimeout(function () {
|
||||
element.parents('.dashlet:first')
|
||||
.addClass("report-box");
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='report-box'>" +
|
||||
"<h1>{{value}}</h1>" +
|
||||
"<small>{{label}}</small>" +
|
||||
"<div class='font-bold text-info pull-right' ng-show='percent'>" +
|
||||
"<span>{{percent}}</span> <i class='fa fa-level-up'></i>" +
|
||||
"</div>" +
|
||||
"<div class='report-tags' ng-if='tag'><span class='label' ng-class='tagCss'>{{tag}}</span></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('reportTable', function() {
|
||||
return {
|
||||
scope: {
|
||||
data: '=',
|
||||
columns: '@',
|
||||
sums: '@'
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var cols = [];
|
||||
var sums = (scope.sums||'').split(',');
|
||||
var fields = {};
|
||||
var schema = scope.$parent.$parent.schema;
|
||||
|
||||
function makeColumns(names) {
|
||||
cols = [];
|
||||
fields = {};
|
||||
_.each(names, function (name) {
|
||||
var field = _.findWhere(schema.items, { name: name }) || {};
|
||||
var col = _.extend({}, field, field.widgetAttrs, {
|
||||
name: name,
|
||||
title: _.humanize(name)
|
||||
});
|
||||
fields[name] = col;
|
||||
cols.push(col);
|
||||
});
|
||||
scope.cols = cols;
|
||||
}
|
||||
|
||||
if (scope.columns) {
|
||||
makeColumns((scope.columns||'').split(','));
|
||||
} else {
|
||||
var unwatch = scope.$watch('data', function reportDataWatch(data) {
|
||||
if (data) {
|
||||
unwatch();
|
||||
var first = _.first(data) || {};
|
||||
var names = _.keys(first).filter(function (name) { return name !== '$$hashKey'; });
|
||||
makeColumns(names.sort());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.sums = sums;
|
||||
|
||||
scope.format = function(value, name) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
var field = fields[name];
|
||||
if (field && field.scale) {
|
||||
var val = +(value);
|
||||
if (_.isNumber(val)) {
|
||||
return val.toFixed(field.scale);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.sum = function (name) {
|
||||
if (sums.indexOf(name) === -1) {
|
||||
return "";
|
||||
}
|
||||
var res = 0.0;
|
||||
_.each(scope.data, function (row) {
|
||||
var val = +(row[name]) || 0;
|
||||
res += val;
|
||||
});
|
||||
return scope.format(res, name);
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
element.parents('.dashlet:first')
|
||||
.addClass("report-table");
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<table class='table table-striped'>" +
|
||||
"<thead>" +
|
||||
"<tr>" +
|
||||
"<th ng-repeat='col in cols'>{{col.title}}</th>" +
|
||||
"</tr>" +
|
||||
"</thead>" +
|
||||
"<tbody>" +
|
||||
"<tr ng-repeat='row in data'>" +
|
||||
"<td ng-repeat='col in cols'>{{format(row[col.name], col.name)}}</td>" +
|
||||
"</tr>" +
|
||||
"</tbody>" +
|
||||
"<tfoot ng-if='sums.length'>" +
|
||||
"<tr>" +
|
||||
"<td ng-repeat='col in cols'>{{sum(col.name)}}</td>" +
|
||||
"</tr>" +
|
||||
"</tfoot>" +
|
||||
"</table>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
335
sophal/js/view/view.dashboard.js
Normal file
335
sophal/js/view/view.dashboard.js
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
DashboardCtrl.$inject = ['$scope', '$element'];
|
||||
function DashboardCtrl($scope, $element) {
|
||||
|
||||
var view = $scope._views.dashboard;
|
||||
if (view.items) {
|
||||
$scope.$timeout(function () {
|
||||
$scope.parse(view);
|
||||
});
|
||||
} else {
|
||||
$scope.loadView('dashboard', view.name).success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function(){
|
||||
if (view.deferred)
|
||||
view.deferred.resolve($scope);
|
||||
});
|
||||
|
||||
$scope.show = function(promise) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'dashboard',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
var items = angular.copy(schema.items || []);
|
||||
var row = [];
|
||||
|
||||
items.forEach(function (item, i) {
|
||||
var span = item.colSpan || 6;
|
||||
|
||||
item.$index = i;
|
||||
item.spanCss = {};
|
||||
item.spanCss['dashlet-cs' + span] = true;
|
||||
|
||||
row.push(item);
|
||||
});
|
||||
|
||||
$scope.schema = schema;
|
||||
$scope.row = row;
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewDashboard', ['ViewService', function(ViewService) {
|
||||
|
||||
return {
|
||||
controller: DashboardCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
scope.sortableOptions = {
|
||||
handle: ".dashlet-header",
|
||||
cancel: ".dashlet-buttons",
|
||||
items: ".dashlet",
|
||||
tolerance: "pointer",
|
||||
activate: function(e, ui) {
|
||||
var height = ui.helper.height();
|
||||
ui.placeholder.height(height);
|
||||
},
|
||||
deactivate: function(event, ui) {
|
||||
axelor.$adjustSize();
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
var schema = scope.schema;
|
||||
var items = _.map(scope.row, function (item) {
|
||||
return schema.items[item.$index];
|
||||
});
|
||||
|
||||
if (angular.equals(schema.items, items)) {
|
||||
return;
|
||||
}
|
||||
|
||||
schema.items = items;
|
||||
return ViewService.save(schema);
|
||||
}
|
||||
};
|
||||
|
||||
var unwatch = scope.$watch("schema", function dashboardSchemaWatch(schema) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
if (schema.css) {
|
||||
element.addClass(schema.css);
|
||||
}
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template:
|
||||
"<div ui-sortable='sortableOptions' ng-model='row'>" +
|
||||
"<div class='dashlet' ng-class='dashlet.spanCss' ng-repeat='dashlet in row' ui-view-dashlet></div>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
DashletCtrl.$inject = ['$scope', '$element', 'MenuService', 'DataSource', 'ViewService'];
|
||||
function DashletCtrl($scope, $element, MenuService, DataSource, ViewService) {
|
||||
|
||||
var self = this;
|
||||
var init = _.once(function init() {
|
||||
|
||||
ui.ViewCtrl.call(self, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.show = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.$on('on:attrs-change:refresh', function(e) {
|
||||
e.preventDefault();
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('on:tab-reload', function(e) {
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.initDashlet = function(dashlet, options) {
|
||||
|
||||
var action = dashlet.action;
|
||||
if (!action) {
|
||||
return init();
|
||||
}
|
||||
|
||||
MenuService.action(action, options).success(function(result){
|
||||
if (_.isEmpty(result.data)) {
|
||||
return;
|
||||
}
|
||||
var view = result.data[0].view;
|
||||
|
||||
$scope._viewParams = view;
|
||||
$scope._viewAction = action;
|
||||
|
||||
init();
|
||||
|
||||
$scope.title = dashlet.title || view.title;
|
||||
if ($scope.attr) {
|
||||
$scope.title = $scope.attr('title') || $scope.title;
|
||||
}
|
||||
$scope.parseDashlet(dashlet, view);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewDashlet', ['$compile', function($compile){
|
||||
return {
|
||||
scope: true,
|
||||
controller: DashletCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var lazy = true;
|
||||
(function () {
|
||||
var counter = 0;
|
||||
return function checkLoading() {
|
||||
if (counter < 10 && element.parent().is(":hidden")) {
|
||||
counter++;
|
||||
return setTimeout(checkLoading, 100);
|
||||
}
|
||||
|
||||
lazy = !element.parent().is(".dashlet-row");
|
||||
|
||||
scope.waitForActions(function () {
|
||||
var unwatch = scope.$watch(function dashletInitWatch() {
|
||||
var dashlet = scope.dashlet;
|
||||
if (!dashlet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.parent().is(":hidden")) {
|
||||
lazy = true;
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
|
||||
var ctx;
|
||||
if (scope.getContext) {
|
||||
ctx = scope.getContext();
|
||||
}
|
||||
scope.initDashlet(dashlet, {
|
||||
context: ctx
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
})()();
|
||||
|
||||
scope.parseDashlet = _.once(function(dashlet, view) {
|
||||
var body = element.find('.dashlet-body:first');
|
||||
var header = element.find('.dashlet-header:first');
|
||||
var template = $('<div ui-portlet-' + view.viewType + '></div>');
|
||||
|
||||
scope.noFilter = !dashlet.canSearch;
|
||||
|
||||
template = $compile(template)(scope);
|
||||
body.append(template);
|
||||
|
||||
if (dashlet.height) {
|
||||
setTimeout(function() {
|
||||
body.css("height", Math.max(0, dashlet.height - header.outerHeight()));
|
||||
});
|
||||
}
|
||||
if (dashlet.css) {
|
||||
element.addClass(dashlet.css);
|
||||
}
|
||||
if (view && view.viewType) {
|
||||
element.addClass(view.viewType);
|
||||
}
|
||||
|
||||
element.removeClass('hidden');
|
||||
|
||||
scope.show();
|
||||
|
||||
// if lazy, load data
|
||||
if (scope.onRefresh && lazy) {
|
||||
scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
scope.showPager = false;
|
||||
scope.showRefresh = true;
|
||||
scope.showToggle = true;
|
||||
|
||||
scope.collapsed = false;
|
||||
scope.collapsedIcon = "fa-chevron-up";
|
||||
scope.onDashletToggle = function(event) {
|
||||
var body = element.children('.dashlet-body');
|
||||
var action = scope.collapsed ? "show" : "hide";
|
||||
scope.collapsed = !scope.collapsed;
|
||||
scope.collapsedIcon = scope.collapsed ? "fa-chevron-down" : "fa-chevron-up";
|
||||
element.removeClass("collapsed");
|
||||
body[action]("blind", 200, function () {
|
||||
element.toggleClass("collapsed", !!scope.collapsed);
|
||||
if (body.css('display') !== 'none' && action === 'hide') {
|
||||
body.hide();
|
||||
}
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
};
|
||||
|
||||
scope.doNext = function() {
|
||||
if (this.canNext()) this.onNext();
|
||||
};
|
||||
|
||||
scope.doPrev = function() {
|
||||
if (this.canPrev()) this.onPrev();
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='dashlet hidden'>" +
|
||||
"<div class='dashlet-header'>" +
|
||||
"<ul class='dashlet-buttons pull-right' ng-if='showRefresh || canExport() || hasAction()'>" +
|
||||
"<li class='dropdown'>" +
|
||||
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-gear'></i></a>" +
|
||||
"<ul class='dropdown-menu pull-right'>" +
|
||||
"<li ng-if='showRefresh'>" +
|
||||
"<a href='' ng-click='onRefresh()' x-translate>Refresh</a>" +
|
||||
"</li>" +
|
||||
"<li ng-if='canExport()'>" +
|
||||
"<a href='' ng-click='onExport()' x-translate>Export</a>" +
|
||||
"</li>" +
|
||||
"<li ng-if='hasAction()' class='divider'></li>" +
|
||||
"<li ng-if='hasAction()'>" +
|
||||
"<a href='' ng-click='onAction()'>{{ (actionTitle || _t('Action')) }}</a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"<li ng-if='showToggle'><a href='' ng-click='onDashletToggle()'><i class='fa' ng-class='collapsedIcon'></i></a></li>" +
|
||||
"</ul>" +
|
||||
"<div class='dashlet-pager pull-right' ng-if='showPager'>" +
|
||||
"<span class='dashlet-pager-text'>{{pagerText()}}</span>" +
|
||||
"<a href='' ng-click='doPrev()' ng-class='{disabled: !canPrev()}'><i class='fa fa-step-backward'></i></a>" +
|
||||
"<a href='' ng-click='doNext()' ng-class='{disabled: !canNext()}'><i class='fa fa-step-forward'></i></a>" +
|
||||
"</div>" +
|
||||
"<div class='dashlet-title'>{{title}}</div>" +
|
||||
"</div>" +
|
||||
"<div class='dashlet-body'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1812
sophal/js/view/view.dms.js
Normal file
1812
sophal/js/view/view.dms.js
Normal file
File diff suppressed because it is too large
Load Diff
1507
sophal/js/view/view.form.js
Normal file
1507
sophal/js/view/view.form.js
Normal file
File diff suppressed because it is too large
Load Diff
835
sophal/js/view/view.gantt.js
Normal file
835
sophal/js/view/view.gantt.js
Normal file
@ -0,0 +1,835 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
// localization
|
||||
|
||||
(function () {
|
||||
|
||||
/* global gantt: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
$(function () {
|
||||
$('<script>')
|
||||
.attr('type', 'text/javascript')
|
||||
.attr('src', 'https://export.dhtmlx.com/gantt/api.js').appendTo('head');
|
||||
});
|
||||
|
||||
var regional = {
|
||||
month_full: [
|
||||
_t('January'),
|
||||
_t('February'),
|
||||
_t('March'),
|
||||
_t('April'),
|
||||
_t('May'),
|
||||
_t('June'),
|
||||
_t('July'),
|
||||
_t('August'),
|
||||
_t('September'),
|
||||
_t('October'),
|
||||
_t('November'),
|
||||
_t('December')],
|
||||
month_short: [
|
||||
_t('Jan'),
|
||||
_t('Feb'),
|
||||
_t('Mar'),
|
||||
_t('Apr'),
|
||||
_t('May'),
|
||||
_t('Jun'),
|
||||
_t('Jul'),
|
||||
_t('Aug'),
|
||||
_t('Sep'),
|
||||
_t('Oct'),
|
||||
_t('Nov'),
|
||||
_t('Dec')],
|
||||
day_full: [
|
||||
_t('Sunday'),
|
||||
_t('Monday'),
|
||||
_t('Tuesday'),
|
||||
_t('Wednesday'),
|
||||
_t('Thursday'),
|
||||
_t('Friday'),
|
||||
_t('Saturday')],
|
||||
day_short : [_t('Sun'), _t('Mon'), _t('Tue'), _t('Wed'), _t('Thu'), _t('Fri'), _t('Sat')]
|
||||
};
|
||||
|
||||
gantt.locale = {
|
||||
date: regional,
|
||||
labels:{
|
||||
new_task: _t("New task"),
|
||||
icon_save: _t("Save"),
|
||||
icon_cancel: _t("Cancel"),
|
||||
icon_details: _t("Details"),
|
||||
icon_edit: _t("Edit"),
|
||||
icon_delete: _t("Delete"),
|
||||
confirm_closing:"",// Your changes will be lost, are your sure ?
|
||||
confirm_deleting: _t("Task will be deleted permanently, are you sure?"),
|
||||
section_description: _t("Description"),
|
||||
section_time: _t("Time period"),
|
||||
section_type: _t("Type"),
|
||||
|
||||
/* grid columns */
|
||||
|
||||
column_text : _t("Task name"),
|
||||
column_start_date : _t("Start time"),
|
||||
column_duration : _t("Duration"),
|
||||
column_add : "",
|
||||
|
||||
/* link confirmation */
|
||||
link: _t("Link"),
|
||||
confirm_link_deleting: _t("will be deleted"),
|
||||
link_start: " " + _t("(start)"),
|
||||
link_end: " " + _t("(end)"),
|
||||
|
||||
type_task: _t("Task"),
|
||||
type_project: _t("Project"),
|
||||
type_milestone: _t("Milestone"),
|
||||
|
||||
minutes: _t("Minutes"),
|
||||
hours: _t("Hours"),
|
||||
days: _t("Days"),
|
||||
weeks: _t("Week"),
|
||||
months: _t("Months"),
|
||||
years: _t("Years")
|
||||
}
|
||||
};
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
ui.controller('GanttViewCtrl', GanttViewCtrl);
|
||||
|
||||
GanttViewCtrl.$inject = ['$scope', '$element'];
|
||||
|
||||
function GanttViewCtrl($scope, $element) {
|
||||
|
||||
ui.DSViewCtrl('gantt', $scope, $element);
|
||||
var ds = $scope._dataSource;
|
||||
var view = $scope._views.gantt;
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.refresh();
|
||||
}
|
||||
|
||||
viewPromise.then(function(){
|
||||
var schema = $scope.schema;
|
||||
initialized = true;
|
||||
$scope._viewResolver.resolve(schema, $element);
|
||||
$scope.updateRoute();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.select = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.fetchItems = function(callback) {
|
||||
|
||||
var schema = $scope.schema;
|
||||
|
||||
var searchFields = _.pluck(this.fields, "name");
|
||||
searchFields.push(schema.taskStart);
|
||||
|
||||
var optionalFields = [schema.taskProgress,
|
||||
schema.taskEnd,
|
||||
schema.taskDuration,
|
||||
schema.taskParent,
|
||||
schema.taskSequence,
|
||||
schema.taskProgress,
|
||||
schema.finishToStart,
|
||||
schema.startToStart,
|
||||
schema.finishToFinish,
|
||||
schema.startToFinish,
|
||||
schema.taskUser
|
||||
];
|
||||
|
||||
_.each(optionalFields,function(optField){
|
||||
if(optField){
|
||||
searchFields.push(optField);
|
||||
}
|
||||
});
|
||||
|
||||
var opts = {
|
||||
fields: searchFields,
|
||||
filter: false,
|
||||
domain: this._domain,
|
||||
store: false
|
||||
};
|
||||
|
||||
ds.search(opts).success(function(records) {
|
||||
callback(records);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'gantt',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
if (opts.mode === "gantt") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "calendar") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.doSave = function(task, callback){
|
||||
var record = _.clone(task.record);
|
||||
return ds.save(record).success(function(res){
|
||||
callback(task, res);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doRemove = function(id, task){
|
||||
var record = _.clone(task.record);
|
||||
return ds.remove(record).success(function(res){
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
ui.directive('uiViewGantt', ['ViewService', 'ActionService', function(ViewService, ActionService) {
|
||||
|
||||
function link(scope, element, attrs, controller) {
|
||||
var main = element.children(".gantt-main");
|
||||
var schema = scope.schema;
|
||||
var fields = scope.fields;
|
||||
var fieldNames = _.pluck(schema.items, "name");
|
||||
var firstField = fields[fieldNames[0]];
|
||||
var mode = schema.mode || "week";
|
||||
var editor = null;
|
||||
ganttInit();
|
||||
|
||||
function byId(list, id) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].key == id)
|
||||
return list[i].label || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setScaleConfig(value){
|
||||
|
||||
switch (value) {
|
||||
case "day":
|
||||
gantt.config.scale_unit = "day";
|
||||
gantt.config.date_scale = "%d/%m/%Y";
|
||||
gantt.config.subscales = [{unit:"hour", step:1, date:"%H:%i"}];
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 50;
|
||||
break;
|
||||
case "week":
|
||||
var weekScaleTemplate = function(date){
|
||||
var dateToStr = gantt.date.date_to_str("%d/%m/%Y");
|
||||
var endDate = gantt.date.add(gantt.date.add(date, 1, "week"), -1, "day");
|
||||
return gantt.date.date_to_str("%W")(date) + "(" + dateToStr(date) + " - " + dateToStr(endDate) + ")";
|
||||
};
|
||||
gantt.config.scale_unit = "week";
|
||||
gantt.templates.date_scale = weekScaleTemplate;
|
||||
gantt.config.min_column_width = 50;
|
||||
gantt.config.subscales = [
|
||||
{unit:"day", step:1, date:"%D %d" }];
|
||||
break;
|
||||
case "month":
|
||||
gantt.config.scale_unit = "month";
|
||||
gantt.config.date_scale = "%F, %Y";
|
||||
gantt.config.subscales = [
|
||||
{unit:"week", step:1, date:"%W" }
|
||||
];
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 50;
|
||||
break;
|
||||
case "year":
|
||||
gantt.config.scale_unit = "year";
|
||||
gantt.config.date_scale = "%Y";
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 100;
|
||||
gantt.config.subscales = [
|
||||
{unit:"month", step:1, date:"%M" }
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getGanttColumns() {
|
||||
|
||||
var colHeader = '<div class="gantt_grid_head_cell gantt_grid_head_add" onclick="gantt.createTask()"></div>';
|
||||
|
||||
var colContent = function(task){
|
||||
return '<i class="fa gantt_button_grid gantt_grid_add fa-plus" onclick="gantt.createTask(null, '+task.id+')"></i>'+
|
||||
'<i class="fa gantt_button_grid gantt_grid_delete fa-times" onclick="gantt.confirm({ ' +
|
||||
'title: gantt.locale.labels.confirm_deleting_title,'+
|
||||
'text: gantt.locale.labels.confirm_deleting,'+
|
||||
'callback: function(res){ '+
|
||||
' if(res)'+
|
||||
' gantt.deleteTask('+task.id+');'+
|
||||
'}})"></i>';
|
||||
};
|
||||
|
||||
|
||||
var columns = [];
|
||||
|
||||
if (schema.taskUser) {
|
||||
columns.push({name: "users", label: fields[schema.taskUser].title, align: "center", template: function (item) {
|
||||
return byId(gantt.serverList("users"), item.user_id);
|
||||
}});
|
||||
}
|
||||
|
||||
var isTree = true;
|
||||
_.each(fieldNames, function(fname){
|
||||
var field = fields[fname];
|
||||
if (columns.length == 0) {
|
||||
columns.push({ name:"text", label:field.title, tree:isTree,
|
||||
template: function(item){
|
||||
if(moment(item[fname], moment.ISO_8601, true).isValid()) {
|
||||
return moment(item[fname], moment.ISO_8601, true).format("MM/DD/YYYY h:mm:ss");
|
||||
}
|
||||
return item.text;
|
||||
}});
|
||||
}
|
||||
else {
|
||||
columns.push({ name:field.name, label:field.title, tree:isTree,
|
||||
template: function(item){
|
||||
if (!item.label) {
|
||||
if(moment(item[fname], moment.ISO_8601, true).isValid()) {
|
||||
return moment(item[fname], moment.ISO_8601, true).format("MM/DD/YYYY h:mm:ss");
|
||||
}
|
||||
return item[fname];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
isTree = false;
|
||||
});
|
||||
columns.push({ name:"buttons", label:colHeader, width:30, template:colContent });
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function setChildTaskDisplay() {
|
||||
|
||||
function createBox(sizes, class_name){
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = [
|
||||
"height:" + sizes.height + "px",
|
||||
"line-height:" + sizes.height + "px",
|
||||
"width:" + sizes.width + "px",
|
||||
"top:" + sizes.top + 'px',
|
||||
"left:" + sizes.left + "px",
|
||||
"position:absolute"
|
||||
].join(";");
|
||||
box.className = class_name;
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
gantt.templates.grid_row_class = gantt.templates.task_class=function(start, end, task){
|
||||
var css = [];
|
||||
if(gantt.hasChild(task.id)){
|
||||
css.push("task-parent");
|
||||
}
|
||||
if (!task.$open && gantt.hasChild(task.id)) {
|
||||
css.push("task-collapsed");
|
||||
}
|
||||
|
||||
if (task.$virtual || task.type == gantt.config.types.project)
|
||||
css.push("summary-bar");
|
||||
|
||||
if(task.user_id){
|
||||
css.push("gantt_resource_task gantt_resource_" + task.user_id);
|
||||
}
|
||||
|
||||
return css.join(" ");
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function ganttInit(){
|
||||
gantt = main.dhx_gantt();
|
||||
setScaleConfig("week");
|
||||
gantt.templates.leftside_text = function(start, end, task){
|
||||
if (!task.progress){
|
||||
return "";
|
||||
}
|
||||
return "<span style='text-align:left;'>"+Math.round(task.progress*100)+ "% </span>";
|
||||
};
|
||||
gantt.config.step = 1;
|
||||
gantt.config.duration_unit = "hour";
|
||||
gantt.config.duration_step = 1;
|
||||
gantt.config.scale_height = 75;
|
||||
gantt.config.grid_width = 400;
|
||||
gantt.config.fit_tasks = true;
|
||||
gantt.config.columns = getGanttColumns();
|
||||
gantt._onTaskIdChange = null;
|
||||
gantt._onLinkIdChange = null;
|
||||
gantt.config.autosize = "x";
|
||||
gantt.config.grid_resize = true;
|
||||
gantt.config.order_branch = true;
|
||||
gantt.config.date_grid = "%d/%m/%Y %H %i";
|
||||
gantt.serverList("users", []);
|
||||
|
||||
gantt.eachSuccessor = function(callback, root){
|
||||
if(!this.isTaskExists(root))
|
||||
return;
|
||||
|
||||
// remember tasks we've already iterated in order to avoid infinite loops
|
||||
var traversedTasks = arguments[2] || {};
|
||||
if(traversedTasks[root])
|
||||
return;
|
||||
traversedTasks[root] = true;
|
||||
|
||||
var rootTask = this.getTask(root);
|
||||
var links = rootTask.$source;
|
||||
if(links){
|
||||
for(var i=0; i < links.length; i++){
|
||||
var link = this.getLink(links[i]);
|
||||
if(this.isTaskExists(link.target)){
|
||||
callback.call(this, this.getTask(link.target));
|
||||
|
||||
// iterate the whole branch, not only first-level dependencies
|
||||
this.eachSuccessor(callback, link.target, traversedTasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gantt.templates.task_class=function(start, end, task){
|
||||
if(task.$virtual)
|
||||
return "summary-bar";
|
||||
};
|
||||
ganttAttachEvents();
|
||||
setChildTaskDisplay();
|
||||
fetchRecords();
|
||||
}
|
||||
|
||||
function ganttAttachEvents(){
|
||||
|
||||
gantt.templates.rightside_text = function(start, end, task){
|
||||
return byId(gantt.serverList("users"), task.user_id);
|
||||
};
|
||||
|
||||
if (schema.taskUser) {
|
||||
gantt.attachEvent("onParse", function(){
|
||||
var styleId = "dynamicGanttStyles";
|
||||
var element = document.getElementById(styleId);
|
||||
if(!element){
|
||||
element = document.createElement("style");
|
||||
element.id = styleId;
|
||||
document.querySelector("head").appendChild(element);
|
||||
}
|
||||
var html = [".gantt_cell:nth-child(1) .gantt_tree_content{" +
|
||||
" border-radius: 16px;" +
|
||||
" width: 100%;" +
|
||||
" height: 70%;" +
|
||||
" margin: 5% 0;" +
|
||||
" line-height: 230%;}"];
|
||||
var resources = gantt.serverList("users");
|
||||
|
||||
resources.forEach(function(r){
|
||||
html.push(".gantt_task_line.gantt_resource_" + r.key + "{" +
|
||||
"background-color:"+r.backgroundColor+"; " +
|
||||
"color:"+r.textColor+";" +
|
||||
"}");
|
||||
html.push(".gantt_row.gantt_resource_" + r.key + " .gantt_cell:nth-child(1) .gantt_tree_content{" +
|
||||
"background-color:"+r.backgroundColor+"; " +
|
||||
"color:"+r.textColor+";" +
|
||||
"}");
|
||||
});
|
||||
element.innerHTML = html.join("");
|
||||
});
|
||||
}
|
||||
|
||||
gantt.attachEvent("onAfterTaskAdd", updateRecord);
|
||||
gantt.attachEvent("onAfterTaskUpdate", updateRecord);
|
||||
gantt.attachEvent("onAfterTaskDelete", scope.doRemove);
|
||||
|
||||
gantt.attachEvent("onAfterLinkAdd", updateLink);
|
||||
gantt.attachEvent("onAfterLinkUpdate", updateLink);
|
||||
gantt.attachEvent("onAfterLinkDelete", deleteLink);
|
||||
|
||||
gantt.attachEvent("onTaskCreated",function(task){
|
||||
scope.showEditor(task, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
gantt.attachEvent("onBeforeLightbox", function(id) {
|
||||
var task = gantt.getTask(id);
|
||||
scope.showEditor(task, false);
|
||||
return false;
|
||||
});
|
||||
|
||||
var diff = 0;
|
||||
|
||||
gantt.attachEvent("onBeforeTaskChanged", function(id, mode, originalTask){
|
||||
var modes = gantt.config.drag_mode;
|
||||
if(mode == modes.move ){
|
||||
var modifiedTask = gantt.getTask(id);
|
||||
diff = modifiedTask.start_date - originalTask.start_date;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
//rounds positions of the child items to scale
|
||||
gantt.attachEvent("onAfterTaskDrag", function(id, mode, e){
|
||||
var modes = gantt.config.drag_mode;
|
||||
if(mode == modes.move ){
|
||||
gantt.eachSuccessor(function(child){
|
||||
child.start_date = gantt.roundDate(new Date(child.start_date.valueOf() + diff));
|
||||
child.end_date = gantt.calculateEndDate(child.start_date, child.duration);
|
||||
gantt.updateTask(child.id);
|
||||
},id );
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function fetchRecords() {
|
||||
|
||||
scope.fetchItems(function(records) {
|
||||
var data = [];
|
||||
var links = [];
|
||||
_.each(records, function(rec) {
|
||||
addData(data, rec);
|
||||
addLinks(links, rec);
|
||||
});
|
||||
gantt.parse({ "data":data, "links":links });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateRecordItem(id,link,toRemove){
|
||||
|
||||
var linkMap = {
|
||||
"0":"finishToStart",
|
||||
"1":"startToStart",
|
||||
"2":"finishToFinish",
|
||||
"3":"startToFinish"
|
||||
};
|
||||
|
||||
var linkField = schema[linkMap[link.type]];
|
||||
var task = gantt.getTask(link.target);
|
||||
var record = task.record;
|
||||
|
||||
if(record && linkField){
|
||||
var endRecord = gantt.getTask(link.source).record;
|
||||
if(endRecord){
|
||||
var recordList = record[linkField];
|
||||
recordList = recordList.filter(function(item, idx) {
|
||||
return item.id != endRecord.id;
|
||||
});
|
||||
|
||||
if(!toRemove){
|
||||
recordList.push(endRecord);
|
||||
}
|
||||
record[linkField] = recordList;
|
||||
task.record = record;
|
||||
scope.doSave(task, updateTaskRecord);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateLink(id,link){
|
||||
updateRecordItem(id, link, false);
|
||||
}
|
||||
|
||||
function updateTaskRecord(task, rec){
|
||||
task.record = rec;
|
||||
}
|
||||
|
||||
function deleteLink(id, link){
|
||||
updateRecordItem(id, link, true);
|
||||
}
|
||||
|
||||
function updateRecord(id, item){
|
||||
|
||||
var record = item.record;
|
||||
if(!record){ record = {}; }
|
||||
|
||||
var duration = item.duration || 1;
|
||||
|
||||
record[schema.taskStart] = item.start_date.toJSON();
|
||||
record[firstField.name] = item.text;
|
||||
|
||||
if(schema.taskProgress){
|
||||
record[schema.taskProgress] = item.progress*100;
|
||||
}
|
||||
if(schema.taskSequence){
|
||||
record[schema.taskSequence] = item.order;
|
||||
}
|
||||
if(schema.taskDuration){
|
||||
record[schema.taskDuration] = duration;
|
||||
}
|
||||
if(schema.taskEnd){
|
||||
record[schema.taskEnd] = item.end_date.toJSON();
|
||||
}
|
||||
|
||||
if(schema.taskParent && item.parent && !record[schema.taskParent]){
|
||||
var parentTask = gantt.getTask(item.parent);
|
||||
var parentRecord = parentTask.record;
|
||||
if(parentRecord){
|
||||
record[schema.taskParent] = parentRecord;
|
||||
}
|
||||
}
|
||||
|
||||
return scope.doSave(item, updateTaskRecord);
|
||||
}
|
||||
|
||||
function addData(data, rec){
|
||||
|
||||
if(rec[schema.taskStart]){
|
||||
var dict = {
|
||||
id:rec.id,
|
||||
open:true,
|
||||
isNew:true
|
||||
};
|
||||
dict = updateData(dict, rec);
|
||||
dict.isNew = false;
|
||||
|
||||
if(dict.start_date){
|
||||
data.push(dict);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addLinkDict(links, targetRecordId, sourceRecords, linkType){
|
||||
|
||||
_.each(sourceRecords, function(sourceRec){
|
||||
links.push({
|
||||
id:targetRecordId+"-"+sourceRec.id,
|
||||
target:targetRecordId,
|
||||
source:sourceRec.id,
|
||||
type:linkType
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function addLinks(links,record){
|
||||
|
||||
var linkMap = {
|
||||
"finishToStart":"0",
|
||||
"startToStart":"1",
|
||||
"finishToFinish":"2",
|
||||
"startToFinish":"3"
|
||||
};
|
||||
|
||||
_.each(_.keys(linkMap), function(key) {
|
||||
if(schema[key]){
|
||||
addLinkDict(links, record.id, record[schema[key]], linkMap[key]);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateTask(task, rec){
|
||||
|
||||
task = updateData(task, rec);
|
||||
|
||||
if(!task.isNew){
|
||||
gantt.refreshTask(task.id);
|
||||
}
|
||||
task.isNew = false;
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
function updateData(task, rec){
|
||||
|
||||
task.record = rec;
|
||||
|
||||
var name = firstField.targetName ? rec[firstField.targetName] : rec[firstField.name];
|
||||
task.text = "";
|
||||
if(name){
|
||||
task.text = name;
|
||||
}
|
||||
_.each(fields,function(field){
|
||||
var val = rec[field.name];
|
||||
if(_.isObject(val) && field.targetName){
|
||||
val = val[field.targetName];
|
||||
}
|
||||
task[field.name] = val || "";
|
||||
});
|
||||
|
||||
var endDate = null;
|
||||
if(schema.taskEnd && rec[schema.taskEnd]){
|
||||
endDate = moment(rec[schema.taskEnd]);
|
||||
task.end_date = endDate.toDate();
|
||||
if(task.isNew){
|
||||
task.end_date = endDate.format("DD-MM-YYYY HH:mm:SS");
|
||||
}
|
||||
}
|
||||
|
||||
var startDate = moment(rec[schema.taskStart]);
|
||||
if(task.isNew){
|
||||
task.start_date = startDate.format("DD-MM-YYYY HH:mm:SS");
|
||||
}
|
||||
else{
|
||||
task.start_date = startDate.toDate();
|
||||
}
|
||||
|
||||
|
||||
if(schema.taskDuration && rec[schema.taskDuration]){
|
||||
task.duration = rec[schema.taskDuration];
|
||||
}
|
||||
else if(endDate){
|
||||
task.duration = gantt.calculateDuration(startDate.toDate(), endDate);
|
||||
}
|
||||
else{
|
||||
task.duration = "1";
|
||||
}
|
||||
|
||||
if(!endDate){
|
||||
task.end_date = gantt.calculateEndDate(startDate.toDate(), task.duration);
|
||||
}
|
||||
|
||||
if(schema.taskProgress){
|
||||
task.progress = rec[schema.taskProgress]/100;
|
||||
}
|
||||
|
||||
if(schema.taskParent){
|
||||
if(rec[schema.taskParent] && rec[schema.taskParent].id != task.id){
|
||||
task.parent = rec[schema.taskParent].id;
|
||||
}
|
||||
else{
|
||||
task.parent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(schema.taskSequence){
|
||||
task.sortorder = rec[schema.taskSequence];
|
||||
}
|
||||
|
||||
if(schema.taskUser && rec[schema.taskUser]) {
|
||||
task.user_id = rec[schema.taskUser].id;
|
||||
if(!byId(gantt.serverList("users"), task.user_id)) {
|
||||
gantt.serverList("users").push({key:task.user_id,
|
||||
label:rec[schema.taskUser][fields[schema.taskUser].targetName],
|
||||
backgroundColor: get_random_color(),
|
||||
textColor:"#FFF"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
}
|
||||
|
||||
function get_random_color() {
|
||||
function c() {
|
||||
var hex = Math.floor(Math.random()*256).toString(16);
|
||||
return ("0"+String(hex)).substr(-2); // pad with zero
|
||||
}
|
||||
return "#"+c()+c()+c();
|
||||
}
|
||||
|
||||
|
||||
scope.onMode = function(name) {
|
||||
setScaleConfig(name);
|
||||
mode = name;
|
||||
gantt.render();
|
||||
};
|
||||
|
||||
scope.isMode = function(name) {
|
||||
return mode === name;
|
||||
};
|
||||
|
||||
scope.onRefresh = function () {
|
||||
gantt.clearAll();
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
scope.onPrint = function () {
|
||||
gantt.exportToPDF({
|
||||
name: "Gantt.pdf",
|
||||
callback: function(result){
|
||||
window.open(result.url , '_self');
|
||||
}});
|
||||
};
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
gantt.clearAll();
|
||||
gantt.detachAllEvents();
|
||||
});
|
||||
|
||||
scope.showEditor = function(task, isNew) {
|
||||
var record = _.extend({}, task.record);
|
||||
if (!editor) {
|
||||
editor = ViewService.compile('<div ui-editor-popup></div>')(scope.$new());
|
||||
editor.data('$target', element);
|
||||
}
|
||||
|
||||
var popup = editor.isolateScope();
|
||||
popup.setEditable(true);
|
||||
|
||||
if(isNew && schema.taskParent && task.parent && !record[schema.taskParent]){
|
||||
var parentTask = gantt.getTask(task.parent);
|
||||
var parentRecord = parentTask.record;
|
||||
if(parentRecord){
|
||||
record[schema.taskParent] = parentRecord;
|
||||
}
|
||||
}
|
||||
|
||||
popup.show(record, function(result) {
|
||||
task.isNew = isNew;
|
||||
task = updateTask(task, result);
|
||||
if(isNew){
|
||||
gantt.addTask(task);
|
||||
}
|
||||
else {
|
||||
gantt.updateTask(task.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!record || !record.id) {
|
||||
popup.waitForActions(function() {
|
||||
popup.$broadcast("on:new");
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
link:function(scope, element, attrs, controller) {
|
||||
scope._viewPromise.then(function(){
|
||||
link(scope, element, attrs, controller);
|
||||
});
|
||||
},
|
||||
replace:true,
|
||||
template:
|
||||
'<div>'+
|
||||
'<div class="gantt-main"></div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1065
sophal/js/view/view.grid.js
Normal file
1065
sophal/js/view/view.grid.js
Normal file
File diff suppressed because it is too large
Load Diff
130
sophal/js/view/view.html.js
Normal file
130
sophal/js/view/view.html.js
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.HtmlViewCtrl = HtmlViewCtrl;
|
||||
ui.HtmlViewCtrl.$inject = ['$scope', '$element', '$sce', '$interpolate'];
|
||||
|
||||
function HtmlViewCtrl($scope, $element, $sce, $interpolate) {
|
||||
|
||||
var views = $scope._views;
|
||||
var stamp = -1;
|
||||
|
||||
$scope.view = views.html;
|
||||
|
||||
$scope.getContext = function () {
|
||||
var params = $scope._viewParams || {};
|
||||
var parent = $scope.$parent;
|
||||
return _.extend({}, params.context, parent.getContext ? parent.getContext() : {});
|
||||
};
|
||||
|
||||
$scope.getURL = function getURL() {
|
||||
var view = $scope.view;
|
||||
if (view) {
|
||||
var url = view.name || view.resource;
|
||||
if (stamp > 0) {
|
||||
var q = url.lastIndexOf('?');
|
||||
if (q > -1) {
|
||||
url += "&t" + stamp;
|
||||
} else {
|
||||
url += "?t" + stamp;
|
||||
}
|
||||
}
|
||||
if (url && url.indexOf('{{') > -1) {
|
||||
url = $interpolate(url)($scope.getContext());
|
||||
}
|
||||
return $sce.trustAsResourceUrl(url);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
if (stamp > -1) {
|
||||
stamp = new Date().getTime();
|
||||
} else {
|
||||
stamp = 0;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: "html"
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
if ($scope._viewParams) {
|
||||
$scope._viewParams.$viewScope = $scope;
|
||||
$scope.show();
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function() {
|
||||
if ($scope.view.deferred) {
|
||||
$scope.view.deferred.resolve($scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var directiveFn = function(){
|
||||
return {
|
||||
controller: HtmlViewCtrl,
|
||||
replace: true,
|
||||
link: function (scope, element) {
|
||||
setTimeout(function () {
|
||||
element.parents('[ui-attach]').each(function () {
|
||||
$(this).scope().keepAttached = true;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// XXX: chrome 76 issue? See RM-20400
|
||||
if (axelor.browser.chrome) {
|
||||
scope.$on('on:nav-click', function (e, tab) {
|
||||
if (tab.$viewScope !== scope) return;
|
||||
var iframe = element.find('iframe')[0];
|
||||
var embed = iframe.contentDocument.body.firstChild;
|
||||
if (embed && embed.id === 'plugin') {
|
||||
embed.height = '101%';
|
||||
setTimeout(function () {
|
||||
embed.height = '100%';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div class="iframe-container">'+
|
||||
'<iframe ng-src="{{getURL()}}" frameborder="0" scrolling="auto"></iframe>'+
|
||||
'</div>'
|
||||
};
|
||||
};
|
||||
|
||||
ui.directive('uiViewHtml', directiveFn);
|
||||
ui.directive('uiPortletHtml', directiveFn);
|
||||
|
||||
})();
|
||||
661
sophal/js/view/view.kanban.js
Normal file
661
sophal/js/view/view.kanban.js
Normal file
@ -0,0 +1,661 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
function BaseCardsCtrl(type, $scope, $element) {
|
||||
|
||||
ui.DSViewCtrl(type, $scope, $element);
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: type,
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function (viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.onRefresh();
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
|
||||
viewPromise.then(function (meta) {
|
||||
$scope.parse(meta.fields, meta.view);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
|
||||
};
|
||||
|
||||
$scope.onNew = function () {
|
||||
ds._page.index = -1;
|
||||
$scope.switchTo('form', function (formScope) {
|
||||
formScope.edit(null);
|
||||
formScope.setEditable();
|
||||
formScope.$broadcast("on:new");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
return $scope.filter({});
|
||||
};
|
||||
|
||||
function update(records) {
|
||||
$scope.records = records;
|
||||
}
|
||||
|
||||
$scope.handleEmpty = function () {
|
||||
};
|
||||
|
||||
$scope.filter = function(options) {
|
||||
var view = $scope.schema;
|
||||
var opts = {
|
||||
fields: _.pluck($scope.fields, 'name')
|
||||
};
|
||||
var handleEmpty = $scope.handleEmpty.bind($scope);
|
||||
|
||||
if (options.criteria || options._domains) {
|
||||
opts.filter = options;
|
||||
}
|
||||
if (options.archived !== undefined) {
|
||||
opts.archived = options.archived;
|
||||
}
|
||||
if (view.orderBy) {
|
||||
opts.sortBy = view.orderBy.split(',');
|
||||
}
|
||||
|
||||
var promise = ds.search(opts);
|
||||
promise.then(handleEmpty, handleEmpty);
|
||||
return promise.success(update).then(function () {
|
||||
$scope.handleEmpty();
|
||||
return ds.fixPage();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.pagerText = function() {
|
||||
var page = ds._page;
|
||||
if (page && page.from !== undefined) {
|
||||
if (page.total === 0) return null;
|
||||
return _t("{0} to {1} of {2}", page.from + 1, page.to, page.total);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onNext = function() {
|
||||
var fields = _.pluck($scope.fields, 'name');
|
||||
return ds.next(fields).success(update);
|
||||
};
|
||||
|
||||
$scope.onPrev = function() {
|
||||
var fields = _.pluck($scope.fields, 'name');
|
||||
return ds.prev(fields).success(update);
|
||||
};
|
||||
|
||||
$scope.getActionData = function(context) {
|
||||
return _.extend({
|
||||
_domain: ds._lastDomain,
|
||||
_domainContext: _.extend({}, ds._lastContext, context),
|
||||
_archived: ds._showArchived
|
||||
}, ds._filter);
|
||||
};
|
||||
}
|
||||
|
||||
ui.controller("CardsCtrl", ['$scope', '$element', function CardsCtrl($scope, $element) {
|
||||
|
||||
BaseCardsCtrl.call(this, 'cards', $scope, $element);
|
||||
|
||||
$scope.viewItems = {};
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
var viewItems = {};
|
||||
_.each(view.items, function (item) {
|
||||
if (item.name) {
|
||||
viewItems[item.name] = _.extend({}, item, fields[item.name], item.widgetAttrs);
|
||||
}
|
||||
});
|
||||
$scope.viewItems = viewItems;
|
||||
$scope.onRefresh();
|
||||
$scope.waitForActions(axelor.$adjustSize);
|
||||
};
|
||||
|
||||
$scope.onExport = function (full) {
|
||||
var fields = full ? [] : _.pluck($scope.viewItems, 'name');
|
||||
return $scope._dataSource.export_(fields).success(function(res) {
|
||||
var fileName = res.fileName;
|
||||
var filePath = 'ws/rest/' + $scope._model + '/export/' + fileName;
|
||||
ui.download(filePath, fileName);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.controller("KanbanCtrl", ['$scope', '$element', 'ActionService', function KanbanCtrl($scope, $element, ActionService) {
|
||||
|
||||
BaseCardsCtrl.call(this, 'kanban', $scope, $element);
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
var params = $scope._viewParams.params || {};
|
||||
var hideCols = (params['kanban-hide-columns'] || '').split(',');
|
||||
var columnBy = fields[view.columnBy] || {};
|
||||
var columns = _.filter(columnBy.selectionList, function (item) {
|
||||
return hideCols.indexOf(item.value) === -1;
|
||||
});
|
||||
|
||||
var first = _.first(columns);
|
||||
if (view.onNew) {
|
||||
first.canCreate = true;
|
||||
}
|
||||
|
||||
var sequenceBy = fields[view.sequenceBy] || {};
|
||||
if (["integer", "long"].indexOf(sequenceBy.type) === -1 || ["id", "version"].indexOf(sequenceBy.name) > -1) {
|
||||
throw new Error("Invalid sequenceBy field in view: " + view.name);
|
||||
}
|
||||
|
||||
$scope.sortableOptions.disabled = !view.draggable || !$scope.hasPermission('write');
|
||||
$scope.columns = columns;
|
||||
$scope.colWidth = params['kanban-column-width'];
|
||||
};
|
||||
|
||||
$scope.move = function (record, to, next, prev) {
|
||||
if(!record) {
|
||||
return;
|
||||
}
|
||||
var ds = $scope._dataSource._new($scope._model);
|
||||
var view = $scope.schema;
|
||||
|
||||
var rec = _.pick(record, "id", "version", view.sequenceBy);
|
||||
var prv = prev ? _.pick(prev, "id", "version", view.sequenceBy) : null;
|
||||
var nxt = next ? _.pick(next, "id", "version", view.sequenceBy) : null;
|
||||
|
||||
// update columnBy
|
||||
rec[view.columnBy] = to;
|
||||
|
||||
// update sequenceBy
|
||||
var all = _.compact([prv, rec, nxt]);
|
||||
var offset = _.min(_.pluck(all, view.sequenceBy)) || 0;
|
||||
|
||||
_.each(all, function (item, i) {
|
||||
item[view.sequenceBy] = offset + i;
|
||||
});
|
||||
|
||||
function doSave() {
|
||||
return ds.saveAll(all).success(function (records) {
|
||||
_.each(_.compact([prev, rec, next]), function (item) {
|
||||
_.extend(item, _.pick(ds.get(item.id), "version", view.sequenceBy));
|
||||
});
|
||||
_.extend(record, rec);
|
||||
}).error(function () {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
if (view.onMove) {
|
||||
var actScope = $scope.$new();
|
||||
actScope.record = rec;
|
||||
actScope.getContext = function () {
|
||||
return _.extend({}, $scope._context, rec);
|
||||
};
|
||||
return ActionService.handler(actScope, $(), { action: view.onMove }).handle().then(function () {
|
||||
return doSave();
|
||||
}, function (err) {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
return doSave();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
$scope.$broadcast("on:refresh");
|
||||
};
|
||||
|
||||
$scope.filter = function(searchFilter) {
|
||||
var options = {};
|
||||
if (searchFilter.criteria || searchFilter._domains) {
|
||||
options = {
|
||||
filter: searchFilter
|
||||
};
|
||||
if (searchFilter.archived !== undefined) {
|
||||
options.archived = searchFilter.archived;
|
||||
}
|
||||
$scope.$broadcast("on:filter", options);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sortableOptions = {
|
||||
connectWith: ".kanban-card-list",
|
||||
items: ".kanban-card",
|
||||
tolerance: "pointer",
|
||||
helper: "clone",
|
||||
stop: function (event, ui) {
|
||||
$scope.$broadcast('on:re-attach-click');
|
||||
var item = ui.item;
|
||||
var sortable = item.sortable;
|
||||
var source = sortable.source.scope();
|
||||
var target = (sortable.droptarget || $(this)).scope();
|
||||
|
||||
var next = item.next().scope();
|
||||
var prev = item.prev().scope();
|
||||
if (next) next = next.record;
|
||||
if (prev) prev = prev.record;
|
||||
|
||||
var index = sortable.dropindex;
|
||||
if (source === target && sortable.index === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.move(target.records[index], target.column.value, next, prev);
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiKanbanColumn', ["ActionService", function (ActionService) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var ds = scope._dataSource._new(scope._model);
|
||||
var view = scope.schema;
|
||||
var elemMore = element.children(".kanban-more");
|
||||
|
||||
ds._context = _.extend({}, scope._dataSource._context);
|
||||
ds._context[view.columnBy] = scope.column.value;
|
||||
ds._page.limit = view.limit || 20;
|
||||
|
||||
var domain = "self." + view.columnBy + " = :" + view.columnBy;
|
||||
ds._domain = scope._dataSource._domain ? scope._dataSource._domain + " AND " + domain : domain;
|
||||
|
||||
scope.records = [];
|
||||
|
||||
function handleEmpty() {
|
||||
element.toggleClass('empty', scope.isEmpty());
|
||||
}
|
||||
|
||||
function fetch(options) {
|
||||
var opts = _.extend({
|
||||
offset: 0,
|
||||
sortBy: [view.sequenceBy],
|
||||
fields: _.pluck(scope.fields, 'name')
|
||||
}, options);
|
||||
elemMore.hide();
|
||||
var promise = ds.search(opts);
|
||||
promise.success(function (records) {
|
||||
scope.records = scope.records.concat(records);
|
||||
elemMore.fadeIn('slow');
|
||||
});
|
||||
return promise.then(handleEmpty, handleEmpty);
|
||||
}
|
||||
|
||||
scope.$watch('records.length', handleEmpty);
|
||||
|
||||
scope.hasMore = function () {
|
||||
var page = ds._page;
|
||||
var next = page.from + page.limit;
|
||||
return next < page.total;
|
||||
};
|
||||
|
||||
scope.isEmpty = function () {
|
||||
return scope.records.length == 0;
|
||||
};
|
||||
|
||||
scope.onMore = function () {
|
||||
var page = ds._page;
|
||||
var next = scope.records.length;
|
||||
if (next < page.total) {
|
||||
return fetch({
|
||||
offset: next
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var onNew = null;
|
||||
|
||||
scope.getContext = function () {
|
||||
var ctx = _.extend({}, scope._context);
|
||||
ctx._value = scope.newItem;
|
||||
return ctx;
|
||||
};
|
||||
|
||||
scope.newItem = null;
|
||||
scope.onCreate = function () {
|
||||
|
||||
var rec = scope.record = {};
|
||||
var view = scope.schema;
|
||||
|
||||
rec[view.columnBy] = scope.column.value;
|
||||
|
||||
if (onNew === null) {
|
||||
onNew = ActionService.handler(scope, element, {
|
||||
action: view.onNew
|
||||
});
|
||||
}
|
||||
|
||||
var ds = scope._dataSource;
|
||||
var promise = onNew.handle();
|
||||
promise.then(function () {
|
||||
ds.save(scope.record).success(function (rec) {
|
||||
scope.newItem = null;
|
||||
scope.records.unshift(rec);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.onEdit = function (record, readonly) {
|
||||
scope.switchTo('form', function (formScope) {
|
||||
formScope.edit(record);
|
||||
formScope.setEditable(!readonly && scope.hasPermission('write') && formScope.canEdit());
|
||||
});
|
||||
};
|
||||
|
||||
scope.onDelete = function (record) {
|
||||
axelor.dialogs.confirm(_t("Do you really want to delete the selected record?"),
|
||||
function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
ds.removeAll([record]).success(function(records, page) {
|
||||
var index = scope.records.indexOf(record);
|
||||
scope.records.splice(index, 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.$on("on:refresh", function (e) {
|
||||
scope.newItem = null;
|
||||
scope.records.length = 0;
|
||||
fetch();
|
||||
});
|
||||
|
||||
scope.$on("on:filter", function (e, options) {
|
||||
scope.newItem = null;
|
||||
scope.records.length = 0;
|
||||
return fetch(options);
|
||||
});
|
||||
|
||||
element.on("click", ".kanban-card", function (e) {
|
||||
var elem = $(e.target);
|
||||
var selector = '[ng-click],[ui-action-click],button,a,.iswitch,.ibox,.kanban-card-menu';
|
||||
if (elem.is(selector) || element.find(selector).has(elem).length) {
|
||||
return;
|
||||
}
|
||||
var record = $(this).scope().record;
|
||||
scope.onEdit(record, true);
|
||||
scope.$applyAsync();
|
||||
});
|
||||
|
||||
if (scope.colWidth) {
|
||||
element.width(scope.colWidth);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
element.find('[ui-sortable]').sortable("option", "appendTo", element.parent());
|
||||
});
|
||||
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiCards', function () {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
var onRefresh = scope.onRefresh;
|
||||
scope.onRefresh = function () {
|
||||
scope.records = null;
|
||||
return onRefresh.apply(scope, arguments);
|
||||
};
|
||||
|
||||
scope.onEdit = function (record, readonly) {
|
||||
var ds = scope._dataSource;
|
||||
var page = ds._page;
|
||||
page.index = record ? ds._data.indexOf(record) : -1;
|
||||
scope.switchTo('form', function (formScope) {
|
||||
formScope.setEditable(!readonly && scope.hasPermission('write') && formScope.canEdit());
|
||||
});
|
||||
};
|
||||
|
||||
scope.onDelete = function (record) {
|
||||
axelor.dialogs.confirm(_t("Do you really want to delete the selected record?"),
|
||||
function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
var ds = scope._dataSource;
|
||||
ds.removeAll([record]).success(function() {
|
||||
scope.onRefresh();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.isEmpty = function () {
|
||||
return (scope.records||[]).length == 0;
|
||||
};
|
||||
|
||||
scope.handleEmpty = function () {
|
||||
element.toggleClass('empty', scope.isEmpty());
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiCard', ["$compile", function ($compile) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var body = element.find(".kanban-card-body");
|
||||
var record = scope.record;
|
||||
var evalScope = axelor.$evalScope(scope);
|
||||
|
||||
evalScope.record = record;
|
||||
evalScope.getContext = scope.getContext = function () {
|
||||
var ctx = _.extend({}, scope._context, scope.record);
|
||||
ctx._model = scope._model;
|
||||
return ctx;
|
||||
};
|
||||
|
||||
if (!record.$processed) {
|
||||
element.hide();
|
||||
}
|
||||
|
||||
function process(record) {
|
||||
if (record.$processed) {
|
||||
return record;
|
||||
}
|
||||
record.$processed = true;
|
||||
for (var name in record) {
|
||||
if (!record.hasOwnProperty(name) || name.indexOf('.') === -1) {
|
||||
continue;
|
||||
}
|
||||
var nested = record;
|
||||
var names = name.split('.');
|
||||
var head = _.first(names, names.length - 1);
|
||||
var last = _.last(names);
|
||||
var i, n;
|
||||
for (i = 0; i < head.length; i++) {
|
||||
n = head[i];
|
||||
nested = nested[n] || (nested[n] = {});
|
||||
}
|
||||
nested[last] = record[name];
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
evalScope.$watch("record", function cardRecordWatch(record) {
|
||||
_.extend(evalScope, process(record));
|
||||
}, true);
|
||||
|
||||
evalScope.$image = function (fieldName, imageName) {
|
||||
return ui.formatters.$image(scope, fieldName, imageName);
|
||||
};
|
||||
|
||||
evalScope.$fmt = function (fieldName) {
|
||||
return ui.formatters.$fmt(scope, fieldName, evalScope[fieldName]);
|
||||
};
|
||||
|
||||
var template = (scope.schema.template || "<span></span>").trim();
|
||||
if (template.indexOf('<') !== 0) {
|
||||
template = "<span>" + template + "</span>";
|
||||
}
|
||||
|
||||
scope.hilite = null;
|
||||
|
||||
$compile(template)(evalScope).appendTo(body);
|
||||
|
||||
var hilites = scope.schema.hilites || [];
|
||||
for (var i = 0; i < hilites.length; i++) {
|
||||
var hilite = hilites[i];
|
||||
if (axelor.$eval(evalScope, hilite.condition, scope.record)) {
|
||||
scope.hilite = hilite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.schema.width) {
|
||||
element.parent().css("width", scope.schema.width);
|
||||
}
|
||||
if (scope.schema.minWidth) {
|
||||
element.parent().css("min-width", scope.schema.minWidth);
|
||||
}
|
||||
if (scope.schema.maxWidth) {
|
||||
element.parent().css("max-width", scope.schema.maxWidth);
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
var elem = $(e.target);
|
||||
var selector = '[ng-click],[ui-action-click],button,a,.iswitch,.ibox,.kanban-card-menu';
|
||||
if (elem.is(selector) || element.find(selector).has(elem).length) {
|
||||
return;
|
||||
}
|
||||
var record = $(this).scope().record;
|
||||
scope.onEdit(record, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
function attachClick() {
|
||||
element.on('click', onClick);
|
||||
}
|
||||
|
||||
attachClick();
|
||||
|
||||
scope.$on('on:re-attach-click', function () {
|
||||
element.off('click', onClick);
|
||||
setTimeout(attachClick, 100);
|
||||
});
|
||||
|
||||
element.fadeIn("slow");
|
||||
|
||||
var summaryHandler;
|
||||
var summaryPlacement;
|
||||
var summary = body.find('.card-summary.popover');
|
||||
|
||||
var configureSummary = _.once(function configureSummary() {
|
||||
element.popover({
|
||||
placement: function (tip, el) {
|
||||
summaryPlacement = setTimeout(function () {
|
||||
$(tip).css('visibility', 'hidden').css('max-width', 400).position({
|
||||
my: 'left',
|
||||
at: 'right',
|
||||
of: el,
|
||||
using: function (pos, feedback) {
|
||||
$(feedback.element.element)
|
||||
.css(pos)
|
||||
.css('visibility', '')
|
||||
.removeClass('left right')
|
||||
.addClass(feedback.horizontal === 'left' ? 'right' : 'left');
|
||||
summaryPlacement = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
container: 'body',
|
||||
trigger: 'manual',
|
||||
title: summary.attr('title'),
|
||||
content: summary.html(),
|
||||
html: true
|
||||
});
|
||||
});
|
||||
|
||||
function showSummary() {
|
||||
configureSummary();
|
||||
if (summaryPlacement) {
|
||||
clearTimeout(summaryPlacement);
|
||||
summaryPlacement = null;
|
||||
}
|
||||
summaryHandler = setTimeout(function () {
|
||||
summaryHandler = null;
|
||||
element.popover('show');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function hideSummary() {
|
||||
if (summaryPlacement) {
|
||||
clearTimeout(summaryPlacement);
|
||||
summaryPlacement = null;
|
||||
}
|
||||
if (summaryHandler) {
|
||||
clearTimeout(summaryHandler);
|
||||
summaryHandler = null;
|
||||
}
|
||||
element.popover('hide');
|
||||
}
|
||||
|
||||
if (summary.length > 0) {
|
||||
element.on('mouseenter.summary', showSummary);
|
||||
element.on('mouseleave.summary', hideSummary);
|
||||
element.on('mousedown.summary', hideSummary);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (summaryHandler) {
|
||||
clearTimeout(summaryHandler);
|
||||
summaryHandler = null;
|
||||
}
|
||||
if (element) {
|
||||
element.off('mouseenter.summary');
|
||||
element.off('mouseleave.summary');
|
||||
element.off('mousedown.summary');
|
||||
element.popover('destroy');
|
||||
element = null;
|
||||
}
|
||||
}
|
||||
|
||||
element.on('$destroy', destroy);
|
||||
scope.$on('$destroy', destroy);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
511
sophal/js/view/view.popup.js
Normal file
511
sophal/js/view/view.popup.js
Normal file
@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 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");
|
||||
|
||||
EditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService', '$q'];
|
||||
|
||||
function EditorCtrl($scope, $element, DataSource, ViewService, $q) {
|
||||
|
||||
var parent = $scope.$parent;
|
||||
|
||||
$scope._viewParams = parent._viewParams;
|
||||
$scope.editorCanSave = parent.editorCanSave;
|
||||
$scope.editorCanReload = parent.editorCanReload;
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
var closeCallback = null;
|
||||
var originalEdit = $scope.edit;
|
||||
var originalShow = $scope.show;
|
||||
|
||||
var recordVersion = -1;
|
||||
var canClose = false;
|
||||
var isClosed = true;
|
||||
|
||||
$scope.show = function(record, callback) {
|
||||
originalShow();
|
||||
if (_.isFunction(record)) {
|
||||
callback = record;
|
||||
record = null;
|
||||
}
|
||||
closeCallback = callback;
|
||||
isClosed = false;
|
||||
recordVersion = record ? record.version : -1;
|
||||
if (recordVersion === undefined && record) {
|
||||
recordVersion = record.$version;
|
||||
}
|
||||
this.edit(record);
|
||||
};
|
||||
|
||||
function doEdit(record, fireOnLoad) {
|
||||
if (record && record.id > 0 && (!_.isNumber(record.version) || !record.$fetched)) {
|
||||
$scope.doRead(record.id).success(function(rec) {
|
||||
if (record.$dirty) {
|
||||
rec = _.extend({}, rec, record);
|
||||
}
|
||||
originalEdit(rec, fireOnLoad);
|
||||
});
|
||||
} else {
|
||||
originalEdit(record, fireOnLoad);
|
||||
}
|
||||
canClose = false;
|
||||
}
|
||||
|
||||
var parentCanEditTarget = null;
|
||||
|
||||
$scope.canEditTarget = function () {
|
||||
if (parentCanEditTarget === null) {
|
||||
var parent = $scope.$parent;
|
||||
var func = parent.canEditTarget;
|
||||
while (parent && func === $scope.canEditTarget) {
|
||||
parent = parent.$parent;
|
||||
func = parent.canEditTarget;
|
||||
}
|
||||
parentCanEditTarget = func || angular.noop;
|
||||
}
|
||||
return parentCanEditTarget() !== false;
|
||||
};
|
||||
|
||||
var isEditable = $scope.isEditable;
|
||||
$scope.isEditable = function () {
|
||||
var id = ($scope.record || {}).id;
|
||||
var perm = id > 0 ? 'write' : 'create';
|
||||
if (parent.isReadonly && parent.isReadonly()) return false;
|
||||
return $scope.hasPermission(perm)
|
||||
&& (id > 0 ? $scope.canEditTarget() : true)
|
||||
&& isEditable.call($scope);
|
||||
};
|
||||
|
||||
var canEdit = $scope.canEdit;
|
||||
$scope.canEdit = function() {
|
||||
return $scope.canEditTarget() && canEdit.call($scope);
|
||||
};
|
||||
|
||||
$scope.edit = function(record, fireOnLoad) {
|
||||
if (isClosed) return;
|
||||
$scope._viewPromise.then(function(){
|
||||
doEdit(record, fireOnLoad);
|
||||
$scope.setEditable(!$scope.$parent.$$readonly);
|
||||
});
|
||||
};
|
||||
|
||||
function isChanged() {
|
||||
if ($scope.isDirty()) return true;
|
||||
var record = $scope.record || {};
|
||||
var version = record.version;
|
||||
return recordVersion !== version || record.$forceDirty;
|
||||
}
|
||||
|
||||
function canOK() {
|
||||
if (isClosed) return false;
|
||||
return isChanged();
|
||||
}
|
||||
|
||||
function onOK() {
|
||||
|
||||
var record = $scope.record;
|
||||
|
||||
function close(value, forceSelect) {
|
||||
if (value && (forceSelect || canOK())) {
|
||||
value.$fetched = true;
|
||||
value.selected = true;
|
||||
$scope.$parent.select(value);
|
||||
}
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
if ($scope.editorCanReload) {
|
||||
$scope.$parent.parentReload();
|
||||
}
|
||||
if (closeCallback && value) {
|
||||
closeCallback(value);
|
||||
}
|
||||
closeCallback = null;
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
var event = $scope.$broadcast('on:before-save', record);
|
||||
if (event.defaultPrevented) {
|
||||
if (event.error) {
|
||||
axelor.dialogs.error(event.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for onChange actions
|
||||
$scope.waitForActions(function() {
|
||||
if ($scope.editorCanSave && isChanged()) {
|
||||
if (record.id < 0)
|
||||
record.id = null;
|
||||
return $scope.onSave({force: true}).then(function(record, page) {
|
||||
// wait for onSave actions
|
||||
$scope.waitForActions(function(){
|
||||
close(record, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
if ($scope.isValid()) {
|
||||
close(record);
|
||||
} else if ($scope.showErrorNotice) {
|
||||
$scope.showErrorNotice();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$scope.onOK = function() {
|
||||
$scope.$timeout(onOK, 10);
|
||||
};
|
||||
|
||||
$scope.onBeforeClose = function(event, ui) {
|
||||
|
||||
if (canClose || !$scope.isDirty()) {
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.edit(null, false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
$scope.confirmDirty(function(){
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onHotKey = function (e, action) {
|
||||
|
||||
if (action === "save") {
|
||||
$(e.target).blur().focus();
|
||||
$scope.onOK();
|
||||
}
|
||||
|
||||
$scope.$applyAsync();
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
SelectorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
||||
function SelectorCtrl($scope, $element, DataSource, ViewService) {
|
||||
|
||||
var parent = $scope.$parent;
|
||||
|
||||
$scope._viewParams = parent._viewParams;
|
||||
$scope.getDomain = parent.getDomain;
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
ui.GridViewCtrl.call(this, $scope, $element);
|
||||
|
||||
var searchLimit = (parent.field||{}).searchLimit || 0;
|
||||
if (searchLimit > 0) {
|
||||
$scope._dataSource._page.limit = searchLimit;
|
||||
}
|
||||
|
||||
function doFilter() {
|
||||
$scope.filter($scope.getDomain());
|
||||
}
|
||||
|
||||
var initialized = false;
|
||||
var origShow = $scope.show;
|
||||
$scope.show = function() {
|
||||
origShow();
|
||||
if (initialized) {
|
||||
doFilter();
|
||||
}
|
||||
};
|
||||
|
||||
var _getContext = $scope.getContext;
|
||||
$scope.getContext = function() {
|
||||
// selector popup should return parent's context
|
||||
if ($scope.$parent && $scope.$parent.getContext) {
|
||||
return $scope.$parent.getContext();
|
||||
}
|
||||
return _getContext();
|
||||
};
|
||||
|
||||
$scope.onItemClick = function(e, args) {
|
||||
$scope.$applyAsync($scope.onOK.bind($scope));
|
||||
};
|
||||
|
||||
var origOnShow = $scope.onShow;
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
viewPromise.then(function(){
|
||||
var view = $scope.schema;
|
||||
var field = $scope.field || $scope.$parent.field;
|
||||
if (field) {
|
||||
view.orderBy = field.orderBy || view.orderBy;
|
||||
}
|
||||
$element.dialog('open');
|
||||
initialized = true;
|
||||
origOnShow(viewPromise);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onOK = function() {
|
||||
|
||||
var selection = _.map($scope.selection, function(index){
|
||||
return $scope.dataView.getItem(index);
|
||||
});
|
||||
|
||||
if (!_.isEmpty(selection)) {
|
||||
$scope.$applyAsync(function () {
|
||||
$scope.$parent.select(selection);
|
||||
$scope.selection = [];
|
||||
});
|
||||
}
|
||||
|
||||
$element.dialog('close');
|
||||
};
|
||||
|
||||
$scope.onCreate = function () {
|
||||
$element.dialog('close');
|
||||
$scope.$parent.onNew();
|
||||
};
|
||||
|
||||
$scope.canNew = function () {
|
||||
return $scope.hasPermission('create') && $scope.$parent.canNew();
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiDialogSize', function() {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
// use only with dialogs
|
||||
if (attrs.uiDialog === undefined && !element.hasClass('ui-dialog-content')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var loaded = false;
|
||||
var addMaximizeButton = _.once(function () {
|
||||
var elemDialog = element.parent();
|
||||
var elemTitle = elemDialog.find('.ui-dialog-title');
|
||||
var elemButton = $('<a href="#" class="ui-dialog-titlebar-max"><i class="fa fa-expand"></i></a>')
|
||||
.click(function (e) {
|
||||
$(this).children('i').toggleClass('fa-expand fa-compress');
|
||||
elemDialog.toggleClass('maximized');
|
||||
axelor.$adjustSize();
|
||||
setTimeout(function () {
|
||||
scope.$broadcast('grid:adjust-columns');
|
||||
}, 350);
|
||||
return false;
|
||||
}).insertAfter(elemTitle);
|
||||
|
||||
// remove maximized state on close
|
||||
element.on('dialogclose', function(e, ui) {
|
||||
elemTitle.parent().find('i.fa-compress').toggleClass('fa-expand fa-compress');
|
||||
elemDialog.removeClass('maximized');
|
||||
});
|
||||
|
||||
var params = (scope._viewParams || {}).params || {};
|
||||
if (params['popup.maximized']) {
|
||||
elemButton.click();
|
||||
}
|
||||
});
|
||||
var addCollapseButton = _.once(function () {
|
||||
var elemDialog = element.parent();
|
||||
var elemTitle = elemDialog.find('.ui-dialog-title');
|
||||
$('<a href="#" class="ui-dialog-titlebar-collapse"><i class="fa fa-chevron-up"></i></a>')
|
||||
.click(function (e) {
|
||||
$(this).children('i').toggleClass('fa-chevron-up fa-chevron-down');
|
||||
elemDialog.toggleClass('collapsed');
|
||||
axelor.$adjustSize();
|
||||
return false;
|
||||
}).insertAfter(elemTitle);
|
||||
|
||||
// remove maximized and collapsed states on close
|
||||
element.on('dialogclose', function(e, ui) {
|
||||
elemTitle.parent().find('i.fa-compress').toggleClass('fa-expand fa-compress');
|
||||
elemTitle.parent().find('i.fa-chevron-down').toggleClass('fa-chevron-down fa-chevron-up');
|
||||
elemDialog.removeClass('maximized collapsed');
|
||||
});
|
||||
});
|
||||
|
||||
function doAdjust() {
|
||||
element.dialog('open');
|
||||
element.scrollTop(0);
|
||||
setTimeout(doFocus);
|
||||
if (scope._afterPopupShow) {
|
||||
scope._afterPopupShow();
|
||||
}
|
||||
}
|
||||
|
||||
function doShow() {
|
||||
addMaximizeButton();
|
||||
addCollapseButton();
|
||||
if (loaded) {
|
||||
return setTimeout(doAdjust);
|
||||
}
|
||||
loaded = true;
|
||||
scope.waitForActions(doAdjust);
|
||||
}
|
||||
|
||||
function doFocus() {
|
||||
var container = element.is('[ui-selector-popup]')
|
||||
? element.find('.slick-headerrow')
|
||||
: element;
|
||||
var focusElem = container.find('input:tabbable');
|
||||
if (focusElem.length == 0) {
|
||||
focusElem = element.parent().find('.ui-dialog-buttonset').find(':tabbable');
|
||||
}
|
||||
if (focusElem[0]) {
|
||||
focusElem[0].focus();
|
||||
}
|
||||
|
||||
//XXX: ui-dialog issue
|
||||
element.find('.slick-headerrow-column,.slickgrid,[ui-embedded-editor]').zIndex(element.zIndex());
|
||||
element.find('.record-toolbar .btn').zIndex(element.zIndex()+1);
|
||||
}
|
||||
|
||||
// a flag used by evalScope to detect popup (see form.base.js)
|
||||
scope._isPopup = true;
|
||||
scope._doShow = function(viewPromise) {
|
||||
if (viewPromise && viewPromise.then) {
|
||||
viewPromise.then(doShow);
|
||||
} else {
|
||||
doShow();
|
||||
}
|
||||
};
|
||||
|
||||
scope._setTitle = function (title) {
|
||||
if (title) {
|
||||
element.closest('.ui-dialog').find('.ui-dialog-title').text(title);
|
||||
}
|
||||
};
|
||||
|
||||
scope.adjustSize = function() {
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiEditorPopup', function() {
|
||||
|
||||
return {
|
||||
restrict: 'EA',
|
||||
controller: EditorCtrl,
|
||||
scope: {},
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
scope.onShow = function(viewPromise) {
|
||||
scope._doShow(viewPromise);
|
||||
};
|
||||
|
||||
scope.$watch('schema.title', function popupTitleWatch(title) {
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
element.scroll(function (e) {
|
||||
$(document).trigger('adjust:scroll', element);
|
||||
});
|
||||
|
||||
var onNewHandler = scope.onNewHandler;
|
||||
scope.onNewHandler = function (event) {
|
||||
if (scope.isPopupOpen) {
|
||||
return onNewHandler.apply(scope, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
scope.isPopupOpen = true;
|
||||
setTimeout(function () {
|
||||
var isOpen = false;
|
||||
element.on('dialogclose', function (e) {
|
||||
isOpen = false;
|
||||
scope.waitForActions(function () {
|
||||
scope.isPopupOpen = isOpen;
|
||||
scope.$$popupStack.pop(1);
|
||||
}, 2000); // delay couple of seconds to that popup can cleanup
|
||||
});
|
||||
element.on('dialogopen', function (e) {
|
||||
scope.isPopupOpen = isOpen = true;
|
||||
scope.$$popupStack.push(1);
|
||||
scope.$applyAsync();
|
||||
});
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-on-ok="onOK" x-on-before-close="onBeforeClose" ui-watch-if="isPopupOpen">'+
|
||||
'<div ui-view-form x-handler="this"></div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiSelectorPopup', function(){
|
||||
|
||||
return {
|
||||
restrict: 'EA',
|
||||
controller: SelectorCtrl,
|
||||
scope: {
|
||||
selectMode: "@"
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var onShow = scope.onShow;
|
||||
scope.onShow = function (viewPromise) {
|
||||
if (scope.clearFilters) {
|
||||
scope.clearFilters();
|
||||
scope.selection = [];
|
||||
}
|
||||
onShow(viewPromise);
|
||||
scope._doShow(viewPromise);
|
||||
};
|
||||
|
||||
scope.$watch('schema.title', function popupTitleWatch(title){
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
var btnOK = null;
|
||||
|
||||
function buttonState(count) {
|
||||
if (btnOK === null) {
|
||||
btnOK = element.siblings('.ui-dialog-buttonpane').find('.btn:last');
|
||||
}
|
||||
return btnOK.attr('disabled', !count || count <= 0);
|
||||
}
|
||||
|
||||
scope.$watch('selection.length', buttonState);
|
||||
|
||||
setTimeout(function(){
|
||||
var footer = element.closest('.ui-dialog').find('.ui-dialog-buttonpane'),
|
||||
header = element.closest('.ui-dialog').find('.ui-dialog-titlebar'),
|
||||
pager = element.find('.record-pager'),
|
||||
buttons = element.find('.ui-dialog-buttonset-left');
|
||||
header.find('.ui-dialog-title').after(pager);
|
||||
footer.prepend(buttons);
|
||||
footer.find('.button-ok').html(_t("Select"));
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-on-ok="onOK">'+
|
||||
'<div ui-view-grid x-view="schema" x-data-view="dataView" x-handler="this" x-editable="false" x-selector="{{selectMode}}"></div>'+
|
||||
'<div ui-record-pager></div>'+
|
||||
'<div class="ui-dialog-buttonset-left pull-left" ng-show="canNew()">'+
|
||||
'<button class="btn" ng-click="onCreate()" x-translate>Create</button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
391
sophal/js/view/view.portal.js
Normal file
391
sophal/js/view/view.portal.js
Normal file
@ -0,0 +1,391 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
PortalCtrl.$inject = ['$scope', '$element'];
|
||||
function PortalCtrl($scope, $element) {
|
||||
|
||||
var view = $scope._views.portal;
|
||||
if (view.items) {
|
||||
$scope.$timeout(function () {
|
||||
$scope.parse(view);
|
||||
});
|
||||
} else {
|
||||
$scope.loadView('portal', view.name).success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function(){
|
||||
if (view.deferred)
|
||||
view.deferred.resolve($scope);
|
||||
});
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
};
|
||||
|
||||
$scope.show = function(promise) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'portal',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var tmplPortlet =
|
||||
'<div ui-view-portlet '+
|
||||
'x-action="{{p.action}}" '+
|
||||
'x-can-search="{{p.canSearch}}" '+
|
||||
'x-col-span="{{p.colSpan}}" '+
|
||||
'x-row-span="{{p.rowSpan}}" ' +
|
||||
'x-height="{{p.height}}"></div>';
|
||||
|
||||
var tmplTabs =
|
||||
"<div ui-portal-tabs x-schema='p'></div>";
|
||||
|
||||
ui.directive('uiViewPortal', ['$compile', function($compile) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
controller: PortalCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
function init() {
|
||||
element.sortable({
|
||||
handle: ".portlet-header",
|
||||
items: "> .portlet, > .portal-tabs",
|
||||
forceHelperSize: true,
|
||||
forcePlaceholderSizeType: true,
|
||||
activate2: function(event, ui) {
|
||||
var width = ui.placeholder.width();
|
||||
var height = ui.placeholder.height();
|
||||
|
||||
ui.placeholder.width(width - 4);
|
||||
ui.placeholder.height(height - 4);
|
||||
|
||||
ui.placeholder.css({
|
||||
'left': '2px',
|
||||
'top': '2px',
|
||||
'margin-right': '4px'
|
||||
});
|
||||
},
|
||||
deactivate: function(event, ui) {
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.parse = function(schema) {
|
||||
scope.portletCols = schema.cols || 2;
|
||||
scope.portlets = schema.items;
|
||||
|
||||
_.each(scope.portlets, function (item) {
|
||||
var tmpl = item.type === 'tabs' ? tmplTabs : tmplPortlet;
|
||||
var child = scope.$new();
|
||||
child.p = item;
|
||||
|
||||
var elem = $compile(tmpl)(child);
|
||||
|
||||
element.append(elem);
|
||||
});
|
||||
|
||||
setTimeout(init);
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: '<div class="portal" ng-transclude></div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
PortletCtrl.$inject = ['$scope', '$element', 'MenuService', 'DataSource', 'ViewService'];
|
||||
function PortletCtrl($scope, $element, MenuService, DataSource, ViewService) {
|
||||
|
||||
var self = this;
|
||||
|
||||
function init() {
|
||||
|
||||
ui.ViewCtrl.call(self, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.show = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
$scope.initPortlet = function(action, options) {
|
||||
|
||||
MenuService.action(action, options).success(function(result){
|
||||
if (_.isEmpty(result.data)) {
|
||||
return;
|
||||
}
|
||||
var view = result.data[0].view;
|
||||
|
||||
$scope._viewParams = view;
|
||||
$scope._viewAction = action;
|
||||
|
||||
init();
|
||||
|
||||
$scope.title = view.title;
|
||||
$scope.parsePortlet(view);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('on:attrs-change:refresh', function(e) {
|
||||
e.preventDefault();
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('on:tab-reload', function(e) {
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setPortletSize(scope, element, attrs) {
|
||||
var cols = scope.portletCols;
|
||||
var colSpan = +attrs.colSpan || 1;
|
||||
var rowSpan = +attrs.rowSpan || 1;
|
||||
|
||||
var width = 100;
|
||||
var height = (+attrs.height || 250) * rowSpan;
|
||||
|
||||
width = (width / cols) * colSpan;
|
||||
|
||||
element.width(width + '%').height(height);
|
||||
}
|
||||
|
||||
ui.directive('uiViewPortlet', ['$compile', function($compile){
|
||||
return {
|
||||
scope: true,
|
||||
controller: PortletCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var lazy = true;
|
||||
(function () {
|
||||
var counter = 0;
|
||||
return function checkLoading() {
|
||||
scope.waitForActions(function () {
|
||||
if (counter < 10 && element.parent().is(":hidden")) {
|
||||
counter++;
|
||||
return setTimeout(checkLoading, 100);
|
||||
}
|
||||
|
||||
lazy = !element.parent().is(".portal");
|
||||
|
||||
var unwatch = scope.$watch(function portalVisibleWatch() {
|
||||
var action = attrs.action;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.parent().is(":hidden")) {
|
||||
lazy = true;
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
|
||||
var ctx;
|
||||
if (scope.getContext) {
|
||||
ctx = scope.getContext();
|
||||
}
|
||||
scope.initPortlet(action, {
|
||||
context: ctx
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
})()();
|
||||
|
||||
scope.parsePortlet = _.once(function(view) {
|
||||
|
||||
scope.noFilter = attrs.canSearch != "true";
|
||||
|
||||
var template = $compile($('<div ui-portlet-' + view.viewType + '></div>'))(scope);
|
||||
element.find('.portlet-content:first').append(template);
|
||||
|
||||
scope.show();
|
||||
|
||||
if (scope.portletCols) {
|
||||
setPortletSize(scope, element, attrs);
|
||||
}
|
||||
|
||||
// if lazy, load data
|
||||
if (scope.onRefresh && lazy) {
|
||||
scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
scope.onPortletToggle = function(event) {
|
||||
var e = $(event.target);
|
||||
e.toggleClass('fa-chevron-up fa-chevron-down');
|
||||
element.toggleClass('portlet-minimized');
|
||||
if (e.hasClass('fa-chevron-up')) {
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
};
|
||||
|
||||
scope.doNext = function() {
|
||||
if (this.canNext()) this.onNext();
|
||||
};
|
||||
|
||||
scope.doPrev = function() {
|
||||
if (this.canPrev()) this.onPrev();
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="portlet">'+
|
||||
'<div class="portlet-body stackbar">'+
|
||||
'<div class="portlet-header navbar">'+
|
||||
'<div class="navbar-inner">'+
|
||||
'<div class="container-fluid">'+
|
||||
'<span class="brand" ng-bind-html="title"></span>'+
|
||||
'<ul class="nav pull-right">'+
|
||||
'<li class="portlet-pager" ng-show="showPager">'+
|
||||
'<span class="portlet-pager-text">{{pagerText()}}</span>'+
|
||||
'<span class="icons-bar">'+
|
||||
'<i ng-click="doPrev()" ng-class="{disabled: !canPrev()}" class="fa fa-step-backward"></i>'+
|
||||
'<i ng-click="doNext()" ng-class="{disabled: !canNext()}" class="fa fa-step-forward"></i>'+
|
||||
'</span>'+
|
||||
'</li>'+
|
||||
'<li class="divider-vertical"></li>'+
|
||||
'<li>'+
|
||||
'<span class="icons-bar">'+
|
||||
'<i title="{{\'Refresh\' | t}}" ng-click="onRefresh()" class="fa fa-refresh"></i>'+
|
||||
'<i title="{{\'Toggle\' | t}}" ng-click="onPortletToggle($event)" class="fa fa-chevron-up"></i>'+
|
||||
'</span>'+
|
||||
'</li>'+
|
||||
'</ul>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<div class="portlet-content"></div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiPortalTabs', function() {
|
||||
return {
|
||||
scope: {
|
||||
schema: '='
|
||||
},
|
||||
replace: true,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var schema = scope.schema;
|
||||
|
||||
var first = _.first(schema.tabs);
|
||||
if (first) {
|
||||
first.active = true;
|
||||
}
|
||||
|
||||
scope.tabClick = function (tab) {
|
||||
_.each(schema.tabs, function (item) {
|
||||
item.active = false;
|
||||
});
|
||||
tab.active = true;
|
||||
axelor.$adjustSize();
|
||||
};
|
||||
|
||||
scope.tabs = schema.tabs;
|
||||
scope.portletCols = scope.$parent.portletCols;
|
||||
|
||||
setPortletSize(scope, element, {
|
||||
colSpan: schema.colSpan,
|
||||
rowSpan: schema.rowSpan,
|
||||
height: schema.height
|
||||
});
|
||||
|
||||
element.height('auto');
|
||||
},
|
||||
template:
|
||||
"<div class='tabbable-tabs portal-tabs'>" +
|
||||
"<ul class='nav nav-tabs nav-tabs-scrollable'>" +
|
||||
"<li ng-repeat='tab in tabs' ng-class='{active: tab.active}'>" +
|
||||
"<a href='' ng-click='tabClick(tab)' >{{tab.title}}</a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"<div class='tab-content portal-tab-content'>" +
|
||||
"<div ng-repeat='tab in tabs' ng-class='{active: tab.active}' class='tab-pane'>" +
|
||||
"<div ui-portal-tab x-schema='tab'></div>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
ui.directive('uiPortalTab', function() {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
schema: '='
|
||||
},
|
||||
controller: ['$scope', 'DataSource', 'ViewService', function ($scope, DataSource, ViewService) {
|
||||
|
||||
var view = $scope.schema;
|
||||
var params = {
|
||||
viewType: 'portal',
|
||||
views: [ view ]
|
||||
};
|
||||
|
||||
view.type = 'portal';
|
||||
|
||||
$scope._viewParams = params;
|
||||
$scope.isNested = true;
|
||||
$scope._model = null;
|
||||
|
||||
ui.ViewCtrl.apply(this, arguments);
|
||||
}],
|
||||
template: "<div ui-view-portal></div>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
856
sophal/js/view/view.search.js
Normal file
856
sophal/js/view/view.search.js
Normal file
@ -0,0 +1,856 @@
|
||||
/*
|
||||
* 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.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: '<div ui-view-form x-handler="this"></div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
795
sophal/js/view/view.tree.js
Normal file
795
sophal/js/view/view.tree.js
Normal file
@ -0,0 +1,795 @@
|
||||
/*
|
||||
* 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.controller('TreeViewCtrl', TreeViewCtrl);
|
||||
|
||||
TreeViewCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
|
||||
function TreeViewCtrl($scope, $element, DataSource, ActionService) {
|
||||
|
||||
var view = $scope._views.tree;
|
||||
var viewPromise = $scope.loadView('tree', view.name);
|
||||
|
||||
$scope.$applyAsync(function() {
|
||||
if (view.deferred) {
|
||||
view.deferred.resolve($scope);
|
||||
}
|
||||
});
|
||||
|
||||
viewPromise.success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function(promise) {
|
||||
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: "tree"
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onSort = function(column) {
|
||||
if (column) {
|
||||
column.sort = true;
|
||||
column.desc = column.desc !== undefined && !column.desc;
|
||||
column.sortCss = column.desc ? "slick-sort-indicator-desc" : "slick-sort-indicator-asc";
|
||||
}
|
||||
$scope.onRefresh();
|
||||
};
|
||||
|
||||
var first = null;
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
|
||||
var columns = _.map(schema.columns, function(col) {
|
||||
return new Column($scope, col);
|
||||
});
|
||||
|
||||
var last = null;
|
||||
var draggable = false;
|
||||
|
||||
var loaders = _.map(schema.nodes, function(node) {
|
||||
var loader = new Loader($scope, node, DataSource);
|
||||
if (last) {
|
||||
last.child = loader;
|
||||
}
|
||||
if (loader.draggable) {
|
||||
draggable = true;
|
||||
}
|
||||
return last = loader;
|
||||
});
|
||||
|
||||
$scope.viewTitle = schema.title;
|
||||
|
||||
$scope.columns = columns;
|
||||
$scope.loaders = loaders;
|
||||
$scope.draggable = draggable;
|
||||
|
||||
first = _.first(loaders);
|
||||
|
||||
first.domain = $scope._domain;
|
||||
first.context = $scope._context;
|
||||
|
||||
// recursive tree (parent -> child on same object)
|
||||
if (loaders.length === 2 && first.model === last.model) {
|
||||
last.child = last;
|
||||
$scope._countOn = _.last(schema.nodes).parent;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onNext = function() {
|
||||
return first && first.onNext();
|
||||
};
|
||||
|
||||
$scope.onPrev = function() {
|
||||
return first && first.onPrev();
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return first && first.canNext();
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return first && first.canPrev();
|
||||
};
|
||||
|
||||
$scope.pagerText = function() {
|
||||
return first ? first.pagerText() : "";
|
||||
};
|
||||
|
||||
$scope.resetPager = function() {
|
||||
if (first) {
|
||||
first.resetPager();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onClick = function(e, options) {
|
||||
|
||||
var loader = options.loader,
|
||||
record = options.record;
|
||||
|
||||
var target = $(e.target);
|
||||
if (target.is('img,i')) {
|
||||
target = target.parent();
|
||||
}
|
||||
if (e.type === 'click' && !target.is('.tree-button')) {
|
||||
return;
|
||||
}
|
||||
var action = target.attr('x-action') || loader.action;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $handler = ActionService.handler($scope.$new(), $(e.currentTarget), {
|
||||
action: action
|
||||
});
|
||||
|
||||
var model = loader.model;
|
||||
var context = record.$record;
|
||||
|
||||
$handler.scope.record = context;
|
||||
$handler.scope.getContext = function() {
|
||||
return _.extend({
|
||||
_model: model
|
||||
}, context);
|
||||
};
|
||||
|
||||
$handler.onClick().then(function(res){
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('on:tab-reload', function(e, tab) {
|
||||
if ($scope === e.targetScope && $scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Column controller.
|
||||
*
|
||||
*/
|
||||
function Column(scope, col) {
|
||||
|
||||
this.css = col.type || 'string';
|
||||
this.name = col.name;
|
||||
this.title = col.title || col.autoTitle;
|
||||
|
||||
if (this.title === null || this.title === undefined) {
|
||||
this.title = _.humanize(col.name);
|
||||
}
|
||||
if (col.type == 'button') {
|
||||
this.title = null;
|
||||
}
|
||||
|
||||
this.cellCss = function(record) {
|
||||
return this.css;
|
||||
};
|
||||
|
||||
this.cellText = function(record) {
|
||||
|
||||
if (col.type === 'button') {
|
||||
var template = "---";
|
||||
var item = _.findWhere(record.$node.items, { type: 'button', name: col.name });
|
||||
if (item) {
|
||||
template = "<a href='javascript:' class='tree-button' x-action='"+ item.onClick +"'>";
|
||||
if (item.icon) {
|
||||
if (item.icon.indexOf('fa') === 0) {
|
||||
template += "<i class='fa " + item.icon + "'></i>";
|
||||
} else {
|
||||
template += "<img width='16px' src='"+ item.icon +"'>";
|
||||
}
|
||||
}
|
||||
if (item.title) {
|
||||
template += item.title;
|
||||
}
|
||||
template += "</a>";
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
var value = record[this.name];
|
||||
if (value === undefined || value === null) {
|
||||
return '---';
|
||||
}
|
||||
|
||||
var selection = (record.$selection || {})[this.name];
|
||||
if (selection) {
|
||||
var cmp = col.type === "integer" ? function(a, b) { return a == b ; } : _.isEqual;
|
||||
var res = _.find(selection, function(item){
|
||||
return cmp(item.value, value);
|
||||
}) || {};
|
||||
|
||||
if (col.widget === 'ImageSelect' && res.icon) {
|
||||
var image = "<img style='max-height: 24px;' src='" + (res.icon || res.value) + "'>";
|
||||
if (col.labels === false) {
|
||||
return image;
|
||||
}
|
||||
return image + " " + res.title;
|
||||
}
|
||||
|
||||
return res.title;
|
||||
}
|
||||
var type = col.type;
|
||||
if (type === 'reference') {
|
||||
type = 'many-to-one';
|
||||
}
|
||||
var item = _.findWhere(record.$node.items, { type: 'field', as: col.name });
|
||||
var attrs = _.extend({}, item, col);
|
||||
var fn = ui.formatters[type];
|
||||
if (fn) {
|
||||
value = fn(attrs, value, record);
|
||||
}
|
||||
return value === undefined || value === null ? '---' : value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node loader.
|
||||
*
|
||||
*/
|
||||
function Loader(scope, node, DataSource) {
|
||||
|
||||
var ds = DataSource.create(node.model);
|
||||
var names = _.pluck(node.items, 'name');
|
||||
var domain = null;
|
||||
|
||||
if (node.parent) {
|
||||
domain = "self." + node.parent + ".id = :parentId";
|
||||
ds._page.limit = -1;
|
||||
}
|
||||
|
||||
if (node.domain) {
|
||||
if (domain) {
|
||||
domain = '(' + domain + ') AND (' + node.domain + ')';
|
||||
} else {
|
||||
domain = node.domain;
|
||||
}
|
||||
}
|
||||
|
||||
this.node = node;
|
||||
|
||||
this.child = null;
|
||||
|
||||
this.model = node.model;
|
||||
|
||||
this.action = node.onClick;
|
||||
|
||||
this.draggable = node.draggable;
|
||||
|
||||
this.getDomain = function(context) {
|
||||
var _domain = domain,
|
||||
_context = context;
|
||||
|
||||
if (_domain && this.domain) {
|
||||
_domain = "(" + this.domain + ") AND (" + domain + ")";
|
||||
}
|
||||
|
||||
_domain = _domain || this.domain;
|
||||
_context = _.extend({}, this.context, context);
|
||||
|
||||
return {
|
||||
domain: _domain,
|
||||
context: _context
|
||||
};
|
||||
};
|
||||
|
||||
this.resetPager = function () {
|
||||
ds._page.from = 0;
|
||||
};
|
||||
|
||||
this.load = function(item, callback) {
|
||||
|
||||
var context = _.extend({}, scope._context),
|
||||
current = item && item.$record;
|
||||
|
||||
var sortOn = _.filter(scope.columns, function (col) { return col.sort; });
|
||||
var sortBy = _.map(sortOn, function (col) {
|
||||
var field = _.findWhere(node.items, { as: col.name });
|
||||
if (field) {
|
||||
return col.desc ? '-' + field.name : field.name;
|
||||
}
|
||||
});
|
||||
|
||||
sortBy = _.compact(sortBy).join(',') || node.orderBy;
|
||||
|
||||
if (scope.getContext) {
|
||||
context = _.extend(context, scope.getContext());
|
||||
}
|
||||
if (current) {
|
||||
context.parentId = current.id;
|
||||
}
|
||||
|
||||
if (scope._countOn) {
|
||||
context._countOn = scope._countOn;
|
||||
} else if (this.child) {
|
||||
var child = this.child.node;
|
||||
context._childOn = {
|
||||
model: child.model,
|
||||
parent: child.parent
|
||||
};
|
||||
}
|
||||
|
||||
var opts = _.extend(this.getDomain(context), {
|
||||
fields: names,
|
||||
action: scope._viewAction
|
||||
});
|
||||
|
||||
if (sortBy) {
|
||||
opts.sortBy = sortBy.split(',');
|
||||
}
|
||||
|
||||
var promise = ds.search(opts);
|
||||
|
||||
promise.success(function(records) {
|
||||
if (callback) {
|
||||
callback(accept(item, records));
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
this.move = function(item, callback) {
|
||||
|
||||
var record = item.$record,
|
||||
parent = { id: item.$parentId };
|
||||
|
||||
record[node.parent || scope._countOn] = parent;
|
||||
|
||||
return ds.save(record).success(function(rec) {
|
||||
record.version = rec.version;
|
||||
if (callback) {
|
||||
callback(rec);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var that = this;
|
||||
|
||||
function accept(current, records) {
|
||||
|
||||
var fields = node.items,
|
||||
parent = current && current.$record,
|
||||
child = that.child;
|
||||
|
||||
return _.map(records, function(record) {
|
||||
|
||||
var $id = _.uniqueId('row');
|
||||
var $parent = current ? current.$id : null;
|
||||
|
||||
var item = {
|
||||
'$id': $id,
|
||||
'$model': node.model,
|
||||
'$node': node,
|
||||
'$record': record,
|
||||
'$selection': {},
|
||||
'$parent': $parent,
|
||||
'$parentId': parent && parent.id,
|
||||
'$parentModel': current && current.$model,
|
||||
'$draggable': node.draggable,
|
||||
'$folder': child && (record._children === undefined || record._children > 0)
|
||||
};
|
||||
|
||||
item.$expand = function(callback) {
|
||||
if (child) {
|
||||
return child.load(this, callback);
|
||||
}
|
||||
};
|
||||
|
||||
item.$move = function(callback) {
|
||||
return that.move(this, callback);
|
||||
};
|
||||
|
||||
item.$click = function(e) {
|
||||
if (node.onClick) {
|
||||
scope.onClick(e, {
|
||||
loader: that,
|
||||
record: item,
|
||||
parent: parent
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_.each(fields, function(field) {
|
||||
var name = field.as || field.name;
|
||||
item[name] = record[field.name];
|
||||
item.$selection[name] = field.selectionList;
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
var page = {};
|
||||
|
||||
ds.on('change', function(e, _records, _page) {
|
||||
page = _page;
|
||||
});
|
||||
|
||||
this.canNext = function() {
|
||||
return ds.canNext();
|
||||
};
|
||||
|
||||
this.canPrev = function() {
|
||||
return ds.canPrev();
|
||||
};
|
||||
|
||||
this.onNext = function() {
|
||||
ds.next(names).success(function(records){
|
||||
scope.setRootNodes(accept(null, records));
|
||||
});
|
||||
};
|
||||
|
||||
this.onPrev = function() {
|
||||
ds.prev(names).success(function(records){
|
||||
scope.setRootNodes(accept(null, records));
|
||||
});
|
||||
};
|
||||
|
||||
this.pagerText = function() {
|
||||
if (page && page.from !== undefined) {
|
||||
if (page.total === 0) return null;
|
||||
return _t("{0} to {1} of {2}", page.from + 1, page.to, page.total);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewTree', function(){
|
||||
|
||||
return {
|
||||
|
||||
replace: true,
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var table = element.find('.tree-table > table');
|
||||
|
||||
table.treetable({
|
||||
|
||||
indent: 16,
|
||||
|
||||
expandable: true,
|
||||
|
||||
clickableNodeNames: true,
|
||||
|
||||
nodeIdAttr: "id",
|
||||
|
||||
parentIdAttr: "parent",
|
||||
|
||||
branchAttr: "folder",
|
||||
|
||||
onNodeCollapse: function onNodeCollapse() {
|
||||
var node = this,
|
||||
row = node.row;
|
||||
|
||||
if (node._state === "collapsed") {
|
||||
return;
|
||||
}
|
||||
node._state = "collapsed";
|
||||
|
||||
table.treetable("collapseNode", row.data("id"));
|
||||
adjustCols();
|
||||
},
|
||||
|
||||
onNodeExpand: function onNodeExpand() {
|
||||
|
||||
var node = this,
|
||||
row = this.row,
|
||||
record = row.data('$record');
|
||||
|
||||
if (node._loading || node._state === "expanded") {
|
||||
return;
|
||||
}
|
||||
|
||||
node._state = "expanded";
|
||||
|
||||
if (node._loaded) {
|
||||
table.treetable("expandNode", row.data("id"));
|
||||
return adjustCols();
|
||||
}
|
||||
|
||||
node._loading = true;
|
||||
|
||||
if (record.$expand) {
|
||||
record.$expand(function(records) {
|
||||
acceptNodes(records, node);
|
||||
node._loading = false;
|
||||
node._loaded = true;
|
||||
adjustCols();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function acceptNodes(records, after) {
|
||||
var rows = _.map(records, makeRow);
|
||||
table.treetable("loadBranch", after, rows);
|
||||
}
|
||||
|
||||
function makeRow(record) {
|
||||
|
||||
var tr = $('<tr>')
|
||||
.attr('data-id', record.$id)
|
||||
.attr('data-parent', record.$parent)
|
||||
.attr('data-folder', record.$folder);
|
||||
|
||||
tr.data('$record', record);
|
||||
|
||||
_.each(scope.columns, function(col) {
|
||||
$('<td>').html(col.cellText(record)).appendTo(tr);
|
||||
});
|
||||
|
||||
if (scope.draggable && (record.$folder || scope._countOn || !record.$parent)) {
|
||||
makeDroppable(tr);
|
||||
}
|
||||
if (record.$draggable || (scope.draggable && scope._countOn)) {
|
||||
makeDraggable(tr);
|
||||
}
|
||||
|
||||
tr.on('click dblclick taphold', function(e) {
|
||||
record.$click(e);
|
||||
});
|
||||
|
||||
return tr[0];
|
||||
}
|
||||
|
||||
function onDrop(e, ui) {
|
||||
/* jshint validthis: true */
|
||||
var row = ui.draggable,
|
||||
record = row.data('$record'),
|
||||
current = $(this).data('$record'),
|
||||
node = table.treetable("node", row.data("id")),
|
||||
nodeParent = node.parentNode();
|
||||
|
||||
table.treetable("move", node.id, $(this).data("id"));
|
||||
|
||||
// make sure to remove expander icon if no children left
|
||||
if (nodeParent && nodeParent.children.length === 0) {
|
||||
nodeParent.row.removeClass('expanded');
|
||||
nodeParent.row.removeClass('branch');
|
||||
nodeParent.row.addClass('leaf');
|
||||
|
||||
nodeParent.treeCell.off('click.treetable');
|
||||
nodeParent.treeCell.off('keydown.treetable');
|
||||
nodeParent.indenter.empty();
|
||||
}
|
||||
|
||||
record.$parentId = current.$record.id;
|
||||
record.$move(function(result) {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function isParent(source, target) {
|
||||
var parent = target.parent().find('[data-id=' + target.data('parent') + ']');
|
||||
if (parent.data('id') === source.data('id')) {
|
||||
return true;
|
||||
}
|
||||
if (parent.length) {
|
||||
return isParent(source, parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeDroppable(row) {
|
||||
|
||||
row.droppable({
|
||||
accept: function(draggable, x) {
|
||||
var source = draggable.data('$record'),
|
||||
target = row.data('$record');
|
||||
|
||||
// don't allow moving parent to child
|
||||
if (scope._countOn) {
|
||||
return !isParent(draggable, $(this));
|
||||
}
|
||||
|
||||
return source && target && target.$model === source.$parentModel;
|
||||
},
|
||||
hoverClass: "accept",
|
||||
drop: onDrop,
|
||||
over: function(e, ui) {
|
||||
var row = ui.draggable;
|
||||
if(this != row[0] && !$(this).is(".expanded")) {
|
||||
table.treetable("expandNode", $(this).data("id"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeDraggable(row) {
|
||||
|
||||
var record = row.data('$record');
|
||||
if (!record.$draggable && !scope._countOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.draggable({
|
||||
helper: function() {
|
||||
return $('<span></span>').append(row.children('td:first').clone());
|
||||
},
|
||||
opacity: 0.75,
|
||||
containment: 'document',
|
||||
refreshPositions: true,
|
||||
revert: "invalid",
|
||||
revertDuration: 300,
|
||||
delay: 300,
|
||||
scroll: true
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
||||
var tree = table.data('treetable');
|
||||
if (tree === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(tree.roots, function(node) {
|
||||
tree.unloadBranch(node);
|
||||
node.row.remove();
|
||||
delete tree.tree[node.id];
|
||||
});
|
||||
|
||||
tree.nodes.length = 0;
|
||||
tree.roots.length = 0;
|
||||
}
|
||||
|
||||
scope.onRefresh = function() {
|
||||
var root = _.first(scope.loaders);
|
||||
if (root) {
|
||||
root.load(null, function(nodes) {
|
||||
scope.setRootNodes(nodes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
scope.setRootNodes = function(nodes) {
|
||||
clear();
|
||||
acceptNodes(nodes);
|
||||
};
|
||||
|
||||
scope.onHeaderClick = function (event, column) {
|
||||
if (!event.shiftKey) {
|
||||
_.each(scope.columns, function (col) {
|
||||
if (col !== column) {
|
||||
col.sort = false;
|
||||
col.desc = undefined;
|
||||
col.sortCss = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
scope.onSort(column);
|
||||
};
|
||||
|
||||
var watcher = scope.$watch('loaders', function treeLoadersWatch(loaders) {
|
||||
|
||||
if (loaders === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
watcher();
|
||||
|
||||
var root = _.first(loaders);
|
||||
if (root) {
|
||||
root.load(null, acceptNodes).then(adjustCols);
|
||||
}
|
||||
});
|
||||
|
||||
var adjustCounter = 0;
|
||||
|
||||
function adjustCols() {
|
||||
|
||||
if (element.is(':hidden')) {
|
||||
if (adjustCounter++ < 10) {
|
||||
_.delay(adjustCols, 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
adjustCounter = 0;
|
||||
|
||||
var tds = table.find('tr:first').find('td');
|
||||
var ths = element.find('.tree-header').find('th');
|
||||
var widths = [];
|
||||
|
||||
if (tds.length !== ths.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
tds.each(function() {
|
||||
widths.push($(this).outerWidth());
|
||||
});
|
||||
|
||||
ths.each(function(i) {
|
||||
$(this).width(widths[i] - 12);
|
||||
});
|
||||
}
|
||||
|
||||
scope.$onAdjust(adjustCols, 100);
|
||||
|
||||
table.on('mousedown.treeview', 'tbody tr', function(e) {
|
||||
table.find('tr.selected').removeClass('selected');
|
||||
$(this).addClass("selected");
|
||||
});
|
||||
},
|
||||
template:
|
||||
'<div class="tree-view-container" ui-attach-scroll="> .tree-table">'+
|
||||
'<table class="tree-header">'+
|
||||
'<thead>'+
|
||||
'<tr>'+
|
||||
'<th ng-repeat="column in columns" ng-class="column.css" ng-click="onHeaderClick($event, column)">' +
|
||||
'<span>{{column.title}}</span>'+
|
||||
'<span ng-if="column.sort" class="slick-sort-indicator" ng-class="column.sortCss"></span>'+
|
||||
'</th>'+
|
||||
'</tr>'+
|
||||
'</thead>'+
|
||||
'</table>'+
|
||||
'<div class="tree-table">'+
|
||||
'<table>'+
|
||||
'<tbody></tbody>'+
|
||||
'</table>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
TreePortletCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
|
||||
function TreePortletCtrl($scope, $element, DataSource, ActionService) {
|
||||
|
||||
TreeViewCtrl.call(this, $scope, $element, DataSource, ActionService);
|
||||
|
||||
$scope.showPager = true;
|
||||
|
||||
$scope.$on("on:new", function(e) {
|
||||
$scope.resetPager();
|
||||
$scope.onRefresh();
|
||||
});
|
||||
$scope.$on("on:edit", function(e) {
|
||||
$scope.resetPager();
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
ui.directive('uiPortletTree', function(){
|
||||
|
||||
return {
|
||||
controller: TreePortletCtrl,
|
||||
template: '<div ui-view-tree></div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
137
sophal/js/widget/widget.dialog.js
Normal file
137
sophal/js/widget/widget.dialog.js
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.directive('uiDialog', function() {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var onBeforeClose = scope.$eval(attrs.onBeforeClose);
|
||||
|
||||
var onOpen = scope.$eval(attrs.onOpen);
|
||||
var onClose = scope.$eval(attrs.onClose);
|
||||
var onOK = scope.$eval(attrs.onOk);
|
||||
var cssClass = attrs.css;
|
||||
var buttons = scope.$eval(attrs.buttons) || [];
|
||||
|
||||
if(_.isEmpty(buttons) || (_.isUndefined(onClose) || _.isFunction(onClose))) {
|
||||
buttons.push({
|
||||
text: _t('Close'),
|
||||
'class': 'btn button-close',
|
||||
click: function() {
|
||||
element.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(_.isEmpty(buttons) || _.isUndefined(onOK) || _.isFunction(onOK)){
|
||||
buttons.push({
|
||||
text: _t('OK'),
|
||||
'class': 'btn btn-primary button-ok',
|
||||
click: function() {
|
||||
if (onOK) {
|
||||
onOK();
|
||||
}
|
||||
else
|
||||
element.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var dialog = element.dialog({
|
||||
dialogClass: 'ui-dialog-responsive ' + (cssClass || ''),
|
||||
resizable: false,
|
||||
draggable: false,
|
||||
autoOpen: false,
|
||||
closeOnEscape: true,
|
||||
modal: true,
|
||||
zIndex: 1100,
|
||||
show: {
|
||||
effect: 'fade',
|
||||
duration: 300
|
||||
},
|
||||
buttons: buttons
|
||||
});
|
||||
|
||||
// fix IE11 issue
|
||||
if (axelor.browser.msie && axelor.browser.rv) {
|
||||
var headerHeight = 46;
|
||||
var footerHeight = 52;
|
||||
var dialogMargin = 64;
|
||||
|
||||
function onResize() {
|
||||
var availableHeight = $(window).height() - headerHeight - footerHeight - dialogMargin - 8;
|
||||
var contentHeight = element.children().height();
|
||||
var myHeight = Math.min(availableHeight, contentHeight);
|
||||
element.height(myHeight);
|
||||
}
|
||||
|
||||
dialog.on('dialogopen', function (e, ui) {
|
||||
$(window).on('resize', onResize);
|
||||
setTimeout(onResize);
|
||||
});
|
||||
dialog.on('dialogclose', function (e, ui) {
|
||||
$(window).off('resize', onResize);
|
||||
});
|
||||
scope.$on('$destroy', function() {
|
||||
$(window).off('resize', onResize);
|
||||
});
|
||||
|
||||
element.addClass('ui-dialog-ie11');
|
||||
}
|
||||
|
||||
// focus the previous visible dialog
|
||||
dialog.on('dialogclose', function(e, ui){
|
||||
var target = element.data('$target');
|
||||
if (target) {
|
||||
return setTimeout(function(){
|
||||
if (!axelor.device.mobile) {
|
||||
var input = target.find(':input:first');
|
||||
input.addClass('x-focus').focus().select();
|
||||
setTimeout(function () {
|
||||
input.removeClass('x-focus');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
$('body .ui-dialog:visible:last').focus();
|
||||
});
|
||||
|
||||
dialog.on('dialogopen', onOpen)
|
||||
.on('dialogclose', onClose)
|
||||
.on('dialogbeforeclose', onBeforeClose);
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
if (dialog) {
|
||||
if (dialog.data('dialog')) {
|
||||
dialog.dialog('destroy');
|
||||
}
|
||||
dialog.remove();
|
||||
dialog = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
285
sophal/js/widget/widget.menubar.js
Normal file
285
sophal/js/widget/widget.menubar.js
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
MenuBarCtrl.$inject = ['$scope', '$element'];
|
||||
function MenuBarCtrl($scope, $element) {
|
||||
|
||||
this.isDivider = function(item) {
|
||||
return !item.title && !item.icon;
|
||||
};
|
||||
|
||||
this.isSubMenu = function(item) {
|
||||
return item && item.items && item.items.length > 0;
|
||||
};
|
||||
|
||||
$scope.isImage = function (menu) {
|
||||
return menu.icon && menu.icon.indexOf('fa-') !== 0;
|
||||
};
|
||||
|
||||
$scope.isIcon = function (menu) {
|
||||
return menu.icon && menu.icon.indexOf('fa-') === 0;
|
||||
};
|
||||
|
||||
$scope.canShowTitle = function(menu) {
|
||||
return menu.showTitle === null || menu.showTitle === undefined || menu.showTitle;
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiMenuBar', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
controller: MenuBarCtrl,
|
||||
scope: {
|
||||
menus: '=',
|
||||
handler: '='
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
ctrl.handler = scope.handler;
|
||||
|
||||
scope.onMenuClick = _.once(function onMenuClick(e) {
|
||||
element.find('.dropdown-toggle').dropdown();
|
||||
$(e.currentTarget).dropdown('toggle');
|
||||
});
|
||||
},
|
||||
|
||||
template:
|
||||
"<ul class='nav menu-bar'>" +
|
||||
"<li class='menu dropdown button-menu' ng-class='::{\"button-menu\": menu.isButton, \"has-icon\": menu.icon}' ng-repeat='menu in ::menus'>" +
|
||||
"<a href='' class='dropdown-toggle btn' ng-class='::{\"btn\": menu.isButton}' data-toggle='dropdown' ng-click='onMenuClick($event)'>" +
|
||||
"<img ng-if='::isImage(menu)' ng-src='{{menu.icon}}'> " +
|
||||
"<i class='fa {{::menu.icon}}' ng-if='::isIcon(menu)'></i> " +
|
||||
"<span class='menu-title' ng-show='::canShowTitle(menu)'>{{::menu.title}}</span> " +
|
||||
"<b class='caret'></b>" +
|
||||
"</a>" +
|
||||
"<ul ui-menu='menu'></ul>" +
|
||||
"</li>" +
|
||||
"</ul>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiMenu', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
require: '^uiMenuBar',
|
||||
scope: {
|
||||
menu: '=uiMenu'
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
},
|
||||
template:
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li ng-repeat='item in ::menu.items' ui-menu-item='item'>" +
|
||||
"</ul>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiMenuItem', ['$compile', 'ActionService', function($compile, ActionService) {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
require: '^uiMenuBar',
|
||||
scope: {
|
||||
item: '=uiMenuItem'
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
var item = scope.item;
|
||||
var handler = null;
|
||||
|
||||
scope.field = item;
|
||||
scope.isDivider = ctrl.isDivider(item);
|
||||
scope.isSubMenu = ctrl.isSubMenu(item);
|
||||
|
||||
if (item.action) {
|
||||
handler = ActionService.handler(ctrl.handler, element, {
|
||||
action: item.action,
|
||||
prompt: item.prompt
|
||||
});
|
||||
element.addClass("action-item").attr("x-name", item.name);
|
||||
}
|
||||
|
||||
scope.isRequired = function(){};
|
||||
scope.isValid = function(){};
|
||||
|
||||
attrs = {
|
||||
hidden: !!item.hidden,
|
||||
readonly: !!item.readonly
|
||||
};
|
||||
|
||||
scope.attr = function(name, value) {
|
||||
attrs[name] = value;
|
||||
};
|
||||
|
||||
scope.isReadonly = function(){
|
||||
if (attrs.readonly) return true;
|
||||
if (_.isFunction(item.active)) {
|
||||
return !item.active();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
scope.isHidden = function(){
|
||||
if (attrs.hidden) return true;
|
||||
if (_.isFunction(item.visible)) {
|
||||
return !item.visible();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var form = element.parents('.form-view:first');
|
||||
var formScope = form.data('$scope');
|
||||
|
||||
if (formScope) {
|
||||
formScope.$watch('record', function menubarRecordWatch(rec) {
|
||||
scope.record = rec;
|
||||
});
|
||||
}
|
||||
|
||||
scope.onClick = function(e) {
|
||||
element.parents('.dropdown').dropdown('toggle');
|
||||
if (scope.isSubMenu) return;
|
||||
if (item.action) {
|
||||
return handler.onClick();
|
||||
}
|
||||
if (_.isFunction(item.click)) {
|
||||
return item.click(e);
|
||||
}
|
||||
};
|
||||
|
||||
scope.cssClass = function() {
|
||||
if (scope.isDivider) {
|
||||
return 'divider';
|
||||
}
|
||||
if (scope.isSubMenu) {
|
||||
return 'dropdown-submenu';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (scope.isSubMenu) {
|
||||
$compile('<ul ui-menu="item"></ul>')(scope, function(cloned, scope) {
|
||||
element.append(cloned);
|
||||
});
|
||||
}
|
||||
},
|
||||
template:
|
||||
"<li ng-class='cssClass()' ui-widget-states ng-show='!isHidden()'>" +
|
||||
"<a href='' ng-show='isReadonly()' class='disabled'>{{item.title}}</a>" +
|
||||
"<a href='' ng-show='!isDivider && !isReadonly()' ng-click='onClick($event)'>{{item.title}}</a>" +
|
||||
"</li>"
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiToolbarAdjust', function() {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
var elemMenubar = null;
|
||||
var elemToolbar = null;
|
||||
var elemSiblings = null;
|
||||
|
||||
var elemToolbarMobile = null;
|
||||
|
||||
function setup() {
|
||||
elemMenubar = element.children('.view-menubar');
|
||||
elemToolbar = element.children('.view-toolbar');
|
||||
elemSiblings = element.children(':not(.view-menubar,.view-toolbar,.view-toolbar-mobile)');
|
||||
|
||||
elemToolbarMobile = element.children('.view-toolbar-mobile').hide();
|
||||
|
||||
var running = false;
|
||||
scope.$onAdjust(function () {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
try {
|
||||
adjust();
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
});
|
||||
|
||||
scope.$callWhen(adjust, function () {
|
||||
return element.is(':visible');
|
||||
});
|
||||
}
|
||||
|
||||
function hideAndShow(first, second, visibility) {
|
||||
[elemMenubar, elemToolbar, elemToolbarMobile].forEach(function (elem) {
|
||||
elem.hide().css('visibility', 'hidden');
|
||||
});
|
||||
[first, second].forEach(function (elem) {
|
||||
if (elem && element.is(':visible')) {
|
||||
elem.show().css('visibility', visibility || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function adjust() {
|
||||
|
||||
if (elemMenubar === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var width = element.width() - 8;
|
||||
elemSiblings.each(function (i) {
|
||||
width -= $(this).width();
|
||||
});
|
||||
|
||||
if (axelor.device.small) {
|
||||
if (width > elemToolbarMobile.width() + elemMenubar.width()) {
|
||||
hideAndShow(elemToolbarMobile, elemMenubar);
|
||||
} else if (width > elemToolbarMobile.width()) {
|
||||
hideAndShow(elemToolbarMobile);
|
||||
} else if (width > elemMenubar.width()) {
|
||||
hideAndShow(elemMenubar);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function canShow(first, second) {
|
||||
hideAndShow(first, second, 'hidden');
|
||||
if (width > first.width() + second.width()) {
|
||||
first.css('visibility', '');
|
||||
second.css('visibility', '');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canShow(elemToolbar, elemMenubar) ||
|
||||
canShow(elemToolbarMobile, elemMenubar) ||
|
||||
canShow(elemToolbar, $()) ||
|
||||
canShow(elemToolbarMobile, $());
|
||||
}
|
||||
|
||||
scope.waitForActions(setup, 100);
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
447
sophal/js/widget/widget.navmenu.js
Normal file
447
sophal/js/widget/widget.navmenu.js
Normal file
@ -0,0 +1,447 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
NavMenuCtrl.$inject = ['$scope', '$element', 'MenuService', 'NavService'];
|
||||
function NavMenuCtrl($scope, $element, MenuService, NavService) {
|
||||
|
||||
$scope.menus = []; // the first four visible menus
|
||||
$scope.more = []; // rest of the menus
|
||||
|
||||
var hasSideBar = axelor.config['view.menubar.location'] !== 'top';
|
||||
|
||||
MenuService.all().then(function(response) {
|
||||
var res = response.data,
|
||||
data = res.data;
|
||||
|
||||
var items = {};
|
||||
var all = [];
|
||||
|
||||
_.each(data, function(item) {
|
||||
items[item.name] = item;
|
||||
if (item.children === undefined) {
|
||||
item.children = [];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
_.each(data, function(item) {
|
||||
|
||||
if (hasSideBar && !item.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.parent) {
|
||||
return all.push(item);
|
||||
}
|
||||
var parent = items[item.parent];
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.menus = all;
|
||||
$scope.more = all;
|
||||
|
||||
$scope.extra = {
|
||||
title: 'More',
|
||||
children: $scope.more
|
||||
};
|
||||
});
|
||||
|
||||
this.isSubMenu = function(item) {
|
||||
return item && item.children && item.children.length > 0;
|
||||
};
|
||||
|
||||
this.onItemClick = function(item) {
|
||||
if (item.action && !this.isSubMenu(item)) {
|
||||
NavService.openTabByName(item.action);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.hasImage = function (menu) {
|
||||
return menu.icon && menu.icon.indexOf('fa-') !== 0 && menu.icon.indexOf('empty') != -1;
|
||||
};
|
||||
|
||||
$scope.hasIcon = function (menu) {
|
||||
return menu.icon && menu.icon.indexOf('fa-') === 0 && menu.icon.indexOf('empty') != -1;
|
||||
};
|
||||
|
||||
$scope.hasText = function (menu) {
|
||||
return !menu.icon || menu.icon.indexOf('empty') === -1;
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('navMenuBar', function() {
|
||||
|
||||
return {
|
||||
|
||||
replace: true,
|
||||
|
||||
controller: NavMenuCtrl,
|
||||
|
||||
scope: true,
|
||||
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
var elemTop,
|
||||
elemSub,
|
||||
elemMore;
|
||||
|
||||
var siblingsWidth = 0;
|
||||
var adjusting = false;
|
||||
|
||||
element.hide();
|
||||
|
||||
function adjust() {
|
||||
|
||||
if (adjusting) {
|
||||
return;
|
||||
}
|
||||
|
||||
adjusting = true;
|
||||
|
||||
var count = 0;
|
||||
var parentWidth = element.parent().width() - 32;
|
||||
|
||||
elemMore.hide();
|
||||
elemTop.hide();
|
||||
elemSub.hide();
|
||||
|
||||
while (count < elemTop.length) {
|
||||
var elem = $(elemTop[count]).show();
|
||||
var width = siblingsWidth + element.width();
|
||||
if (width > parentWidth) {
|
||||
elem.hide();
|
||||
|
||||
// show more...
|
||||
elemMore.show();
|
||||
width = siblingsWidth + element.width();
|
||||
if (width > parentWidth) {
|
||||
count--;
|
||||
$(elemTop[count]).hide();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count === elemTop.length) {
|
||||
elemMore.hide();
|
||||
}
|
||||
while(count < elemTop.length) {
|
||||
$(elemSub[count++]).show();
|
||||
}
|
||||
|
||||
adjusting = false;
|
||||
}
|
||||
|
||||
function setup() {
|
||||
|
||||
element.siblings().each(function () {
|
||||
siblingsWidth += $(this).width();
|
||||
});
|
||||
|
||||
elemTop = element.find('.nav-menu.dropdown:not(.nav-menu-more)');
|
||||
elemMore = element.find('.nav-menu.dropdown.nav-menu-more');
|
||||
elemSub = elemMore.find('.dropdown-menu:first > .dropdown-submenu');
|
||||
|
||||
element.show();
|
||||
adjust();
|
||||
|
||||
$(window).on("resize.menubar", adjust);
|
||||
}
|
||||
|
||||
element.on('$destroy', function () {
|
||||
if (element) {
|
||||
$(window).off("resize.menubar");
|
||||
element = null;
|
||||
}
|
||||
});
|
||||
|
||||
var unwatch = scope.$watch('menus', function navMenusWatch(menus, old) {
|
||||
if (!menus || menus.length === 0 || menus === old) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
setTimeout(setup, 100);
|
||||
});
|
||||
},
|
||||
|
||||
template:
|
||||
"<ul class='nav nav-menu-bar'>" +
|
||||
"<li class='nav-menu dropdown' ng-class='{empty: !hasText(menu)}' ng-repeat='menu in menus track by menu.name'>" +
|
||||
"<a href='javascript:' class='dropdown-toggle' data-toggle='dropdown'>" +
|
||||
"<img ng-if='hasImage(menu)' ng-src='{{menu.icon}}'> " +
|
||||
"<i ng-if='hasIcon(menu)' class='fa {{menu.icon}}'></i> " +
|
||||
"<span ng-if='hasText(menu)' ng-bind='menu.title'></span> " +
|
||||
"<b class='caret'></b>" +
|
||||
"</a>" +
|
||||
"<ul nav-menu='menu'></ul>" +
|
||||
"</li>" +
|
||||
"<li class='nav-menu nav-menu-more dropdown' style='display: none;'>" +
|
||||
"<a href='javascript:' class='dropdown-toggle' data-toggle='dropdown'>" +
|
||||
"<span x-translate>More</span>" +
|
||||
"<b class='caret'></b>" +
|
||||
"</a>" +
|
||||
"<ul nav-menu='extra'></ul>" +
|
||||
"</li>" +
|
||||
"</ul>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('navMenu', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
require: '^navMenuBar',
|
||||
scope: {
|
||||
menu: '=navMenu'
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
},
|
||||
template:
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li ng-repeat='item in menu.children track by item.name' nav-menu-item='item'>" +
|
||||
"</ul>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('navMenuItem', ['$compile', function($compile) {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
require: '^navMenuBar',
|
||||
scope: {
|
||||
item: '=navMenuItem'
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
var item = scope.item;
|
||||
|
||||
scope.isSubMenu = ctrl.isSubMenu(item);
|
||||
scope.isActionMenu = !!item.action;
|
||||
|
||||
scope.onClick = function (e, item) {
|
||||
ctrl.onItemClick(item);
|
||||
};
|
||||
|
||||
if (ctrl.isSubMenu(item)) {
|
||||
element.addClass("dropdown-submenu");
|
||||
$compile('<ul nav-menu="item"></ul>')(scope, function(cloned, scope) {
|
||||
element.append(cloned);
|
||||
});
|
||||
}
|
||||
},
|
||||
template:
|
||||
"<li>" +
|
||||
"<a href='javascript:' ng-click='onClick($event, item)'>{{item.title}}</a>" +
|
||||
"</li>"
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('navMenuFav', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
controller: ['$scope', '$location', 'DataSource', 'NavService', function ($scope, $location, DataSource, NavService) {
|
||||
|
||||
var ds = DataSource.create("com.axelor.meta.db.MetaMenu", {
|
||||
domain: "self.user = :__user__ and self.link is not null"
|
||||
});
|
||||
|
||||
$scope.items = [];
|
||||
|
||||
function update() {
|
||||
ds.search({
|
||||
fields: ["id", "name", "title", "link"],
|
||||
sortBy: ["-priority"]
|
||||
}).success(function (records, page) {
|
||||
$scope.items = records;
|
||||
});
|
||||
}
|
||||
|
||||
function add(values, callback) {
|
||||
var item = _.findWhere($scope.items, { link: values.link });
|
||||
if (item && item.title === values.title) {
|
||||
return callback();
|
||||
}
|
||||
if (item) {
|
||||
item.title = values.title;
|
||||
} else {
|
||||
item = values;
|
||||
item.name = values.link;
|
||||
item.user = {
|
||||
id: axelor.config['user.id']
|
||||
};
|
||||
item.hidden = true;
|
||||
}
|
||||
|
||||
ds.save(item).success(update).then(callback, callback);
|
||||
}
|
||||
|
||||
$scope.addFav = function () {
|
||||
|
||||
var link = $location.path();
|
||||
if (link === "/") {
|
||||
return;
|
||||
}
|
||||
|
||||
var tab = NavService.getSelected() || {};
|
||||
var vs = tab.$viewScope || {};
|
||||
var title = tab.title || (vs.schema || {}).title || "";
|
||||
|
||||
if (vs.record && vs.record.id > 0) {
|
||||
title = title + " (" + vs.record.id + ")";
|
||||
}
|
||||
|
||||
var item = _.findWhere($scope.items, { link: link });
|
||||
if (item) {
|
||||
title = item.title;
|
||||
}
|
||||
|
||||
var dialog = axelor.dialogs.box("<input type='text' style='width: 100%;box-sizing: border-box;height: 28px;margin: 0;'>", {
|
||||
title: _t('Add to favorites...'),
|
||||
buttons: [{
|
||||
text: _t('Cancel'),
|
||||
'class': 'btn btn-default',
|
||||
click: function (e) {
|
||||
$(this).dialog('close');
|
||||
}
|
||||
}, {
|
||||
text: _t('OK'),
|
||||
'class': 'btn btn-primary',
|
||||
click: function (e) {
|
||||
title = dialog.find("input").val();
|
||||
add({ title: title, link: link }, function () {
|
||||
dialog.dialog('close');
|
||||
});
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
dialog.find("input").val(title).focus().select();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.manageFav = function () {
|
||||
NavService.openTabByName('menus.fav');
|
||||
};
|
||||
|
||||
function onUpdate(e, _ds) {
|
||||
if (ds !== _ds && ds._model === _ds._model) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on("ds:saved", onUpdate);
|
||||
$scope.$on("ds:removed", onUpdate);
|
||||
|
||||
update();
|
||||
}],
|
||||
template:
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li><a href='' ng-click='addFav()' x-translate>Add to favorites...</a></li>" +
|
||||
"<li class='divider'></li>" +
|
||||
"<li ng-repeat='item in items track by item.name'><a ng-href='#{{item.link}}'>{{item.title}}</a></li>" +
|
||||
"<li class='divider'></li>" +
|
||||
"<li><a href='' ng-click='manageFav()' x-translate>Organize favorites...</a></li>" +
|
||||
"</ul>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('navMenuTasks', function() {
|
||||
return {
|
||||
replace: true,
|
||||
controller: ['$scope', '$location', 'TagService', 'NavService', function ($scope, $location, TagService, NavService) {
|
||||
|
||||
var TEAM_TASK = "com.axelor.team.db.TeamTask";
|
||||
|
||||
function taskText(count) {
|
||||
var n = count || 0;
|
||||
if (n <= 0) return _t('no tasks');
|
||||
return n > 1 ? _t('{0} tasks', n) : _t('{0} task', n);
|
||||
}
|
||||
|
||||
function update(data) {
|
||||
var counts = data || {};
|
||||
if (counts.current) {
|
||||
counts.css = 'badge-primary';
|
||||
}
|
||||
if (counts.pending) {
|
||||
counts.css = 'badge-important';
|
||||
}
|
||||
counts.currentText = taskText(counts.current);
|
||||
counts.pendingText = taskText(counts.pending);
|
||||
counts.total = Math.min(99, counts.current);
|
||||
|
||||
$scope.counts = counts;
|
||||
}
|
||||
|
||||
TagService.listen(function (data) {
|
||||
update(data.tasks || {});
|
||||
});
|
||||
|
||||
$scope.showTasks = function (type) {
|
||||
NavService.openTabByName('team.tasks.' + type);
|
||||
};
|
||||
|
||||
function onDataChange(e, ds) {
|
||||
if (ds._model === TEAM_TASK) {
|
||||
TagService.find();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on('ds:saved', onDataChange);
|
||||
$scope.$on('ds:removed', onDataChange);
|
||||
|
||||
update({});
|
||||
}],
|
||||
template:
|
||||
"<li class='dropdown'>" +
|
||||
"<a href='' class='nav-link-tasks dropdown-toggle' data-toggle='dropdown'>" +
|
||||
"<i class='fa fa-bell'></i>" +
|
||||
"<span class='badge' ng-show='counts.css' ng-class='counts.css'>{{counts.total}}</span>" +
|
||||
"</a>" +
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li>" +
|
||||
"<a href='' ng-click='showTasks(\"due\")'>" +
|
||||
"<span class='nav-link-user-name' x-translate>Tasks due</span>" +
|
||||
"<span class='nav-link-user-sub' ng-class='{\"fg-red\": counts.pending > 0}'>{{counts.pendingText}}</span>" +
|
||||
"</a>" +
|
||||
"</li>" +
|
||||
"<li class='divider'></li>" +
|
||||
"<li>" +
|
||||
"<a href='' ng-click='showTasks((\"todo\"))'>" +
|
||||
"<span class='nav-link-user-name' x-translate>Tasks todo</span>" +
|
||||
"<span class='nav-link-user-sub'>{{counts.currentText}}</span>" +
|
||||
"</a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</li>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
85
sophal/js/widget/widget.navtabs.js
Normal file
85
sophal/js/widget/widget.navtabs.js
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.directive('uiNavTabs', function() {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
replace: true,
|
||||
link: function(scope, elem, attrs) {
|
||||
|
||||
if (scope.singleTabOnly) {
|
||||
return elem.parent().addClass("view-tabs-single");
|
||||
}
|
||||
|
||||
scope.$watch('tabs.length', function navTabsWatch(value, oldValue){
|
||||
if (value != oldValue) elem.trigger('adjust:tabs');
|
||||
});
|
||||
|
||||
var menu = $();
|
||||
|
||||
setTimeout(function () {
|
||||
|
||||
elem.bsTabs();
|
||||
elem.on('contextmenu', '.nav-tabs-main > li > a', showMenu);
|
||||
|
||||
menu = elem.find('#nav-tabs-menu');
|
||||
menu.css({
|
||||
position: 'absolute',
|
||||
zIndex: 1000
|
||||
}).hide();
|
||||
});
|
||||
|
||||
function showMenu(e) {
|
||||
var tabElem = $(e.target).parents('li:first');
|
||||
var tabScope = tabElem.data('$scope');
|
||||
if (!tabScope || !tabScope.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
scope.current = tabScope.tab;
|
||||
scope.$timeout(function () {
|
||||
var offset = elem.offset();
|
||||
menu.show().css({
|
||||
left: e.pageX - offset.left,
|
||||
top: e.pageY - offset.top
|
||||
});
|
||||
$(document).on('click.nav-tabs-menu', hideMenu);
|
||||
});
|
||||
}
|
||||
|
||||
function hideMenu(e) {
|
||||
scope.$timeout(function () {
|
||||
scope.current = null;
|
||||
menu.hide();
|
||||
});
|
||||
$(document).off('click.nav-tabs-menu');
|
||||
}
|
||||
},
|
||||
templateUrl: 'partials/nav-tabs.html'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
436
sophal/js/widget/widget.navtree.js
Normal file
436
sophal/js/widget/widget.navtree.js
Normal file
@ -0,0 +1,436 @@
|
||||
/*
|
||||
* 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.directive('uiNavTree', ['MenuService', 'TagService', function(MenuService, TagService) {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
itemClick: "&"
|
||||
},
|
||||
controller: ["$scope", "$q", function ($scope, $q) {
|
||||
|
||||
var items = [];
|
||||
var menus = [];
|
||||
var nodes = {};
|
||||
var searchItems = [];
|
||||
var deferred = $q.defer();
|
||||
|
||||
var handler = $scope.itemClick();
|
||||
|
||||
function canAccept(item) {
|
||||
return item.left || item.left === undefined;
|
||||
}
|
||||
|
||||
this.onClick = function (e, menu) {
|
||||
if (menu.action && (menu.children||[]).length === 0) {
|
||||
handler(e, menu);
|
||||
}
|
||||
};
|
||||
|
||||
this.load = function (data) {
|
||||
if (!data || !data.length) return;
|
||||
|
||||
items = data;
|
||||
items.forEach(function (item) {
|
||||
nodes[item.name] = item;
|
||||
item.children = [];
|
||||
});
|
||||
|
||||
items.forEach(function (item) {
|
||||
var node = nodes[item.parent];
|
||||
if (node) {
|
||||
node.children.push(item);
|
||||
} else if (canAccept(item)){
|
||||
menus.push(item);
|
||||
item.icon = item.icon || 'fa-bars';
|
||||
item.iconBackground = item.iconBackground || 'green';
|
||||
}
|
||||
});
|
||||
|
||||
var markForSidebar = function (item) {
|
||||
item.sidebar = true;
|
||||
item.children.forEach(markForSidebar);
|
||||
};
|
||||
menus.forEach(markForSidebar);
|
||||
|
||||
items.forEach(function (item) {
|
||||
if (item.children.length === 0) {
|
||||
delete item.children;
|
||||
var label = item.title;
|
||||
var parent = nodes[item.parent];
|
||||
var lastParent;
|
||||
while (parent) {
|
||||
lastParent = parent;
|
||||
parent = nodes[parent.parent];
|
||||
if (parent) {
|
||||
label = lastParent.title + "/" + label;
|
||||
}
|
||||
}
|
||||
searchItems.push(_.extend({
|
||||
title: item.title,
|
||||
label: label,
|
||||
action: item.action,
|
||||
category: lastParent ? lastParent.name : '',
|
||||
categoryTitle: lastParent ? lastParent.title : ''
|
||||
}));
|
||||
}
|
||||
});
|
||||
$scope.menus = menus;
|
||||
$scope.searchItems = searchItems;
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
this.update = function (data) {
|
||||
if (!data || data.length === 0) return;
|
||||
data.forEach(function (item) {
|
||||
var node = nodes[item.name];
|
||||
if (node) {
|
||||
node.tag = item.tag;
|
||||
node.tagStyle = item.tagStyle;
|
||||
if (node.tagStyle) {
|
||||
node.tagCss = "label-" + node.tagStyle;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var that = this;
|
||||
TagService.listen(function (data) {
|
||||
that.update(data.tags);
|
||||
});
|
||||
|
||||
function findProp(node, name) {
|
||||
if (node[name]) {
|
||||
return node[name];
|
||||
}
|
||||
var parent = nodes[node.parent];
|
||||
if (parent) {
|
||||
return findProp(parent, name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateTabStyle(tab) {
|
||||
if (tab.icon || tab.fa) {
|
||||
return;
|
||||
}
|
||||
var node = _.findWhere(nodes, { action: tab.action, sidebar: true });
|
||||
if (node) {
|
||||
tab.icon = tab.icon || findProp(node, 'icon');
|
||||
tab.color = tab.color || findProp(node, 'iconBackground');
|
||||
if (tab.icon && tab.icon.indexOf('fa') === 0) {
|
||||
tab.fa = tab.icon;
|
||||
delete tab.icon;
|
||||
} else {
|
||||
tab.fa = tab.fa || findProp(node, 'fa');
|
||||
}
|
||||
if (tab.icon) {
|
||||
tab.fa = null;
|
||||
}
|
||||
if (tab.color && tab.color.indexOf('#') != 0) {
|
||||
tab.topCss = 'bg-' + tab.color;
|
||||
tab.fa = tab.fa ? tab.fa + ' fg-' + tab.color : null;
|
||||
tab.color = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuService.updateTabStyle = function (tab) {
|
||||
deferred.promise.then(function () {
|
||||
updateTabStyle(tab);
|
||||
});
|
||||
};
|
||||
}],
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
var input = element.find('input');
|
||||
scope.showSearch = !!axelor.device.mobile;
|
||||
scope.toggleSearch = function (show) {
|
||||
input.val('');
|
||||
if (!axelor.device.mobile) {
|
||||
scope.showSearch = show === undefined ? !scope.showSearch : show;
|
||||
}
|
||||
};
|
||||
scope.onShowSearch = function () {
|
||||
scope.showSearch = true;
|
||||
setTimeout(function () {
|
||||
input.val('').focus();
|
||||
});
|
||||
};
|
||||
|
||||
input.attr('placeholder', _t('Search...'));
|
||||
input.blur(function (e) {
|
||||
scope.$timeout(function () {
|
||||
scope.toggleSearch(false);
|
||||
});
|
||||
});
|
||||
input.keydown(function (e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
scope.$timeout(function () {
|
||||
scope.toggleSearch(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function search(request, response) {
|
||||
var term = request.term;
|
||||
var items = _.filter(scope.searchItems, function (item) {
|
||||
var text = item.categoryTitle + '/' + item.label;
|
||||
var search = term;
|
||||
if (search[0] === '/') {
|
||||
search = search.substring(1);
|
||||
text = item.title;
|
||||
}
|
||||
text = text.replace('/', '').toLowerCase();
|
||||
if (search[0] === '"' || search[0] === '=') {
|
||||
search = search.substring(1);
|
||||
if (search.indexOf('"') === search.length - 1) {
|
||||
search = search.substring(0, search.length - 1);
|
||||
}
|
||||
return text.indexOf(search) > -1;
|
||||
}
|
||||
var parts = search.toLowerCase().split(/\s+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (text.indexOf(parts[i]) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return parts.length > 0;
|
||||
});
|
||||
response(items);
|
||||
}
|
||||
|
||||
MenuService.all().success(function (res) {
|
||||
ctrl.load(res.data);
|
||||
input.autocomplete({
|
||||
source: search,
|
||||
select: function (e, ui) {
|
||||
ctrl.onClick(e, ui.item);
|
||||
scope.$timeout(function () {
|
||||
scope.toggleSearch(false);
|
||||
});
|
||||
},
|
||||
appendTo: element.parent(),
|
||||
open: function () {
|
||||
element.children('.nav-tree').hide();
|
||||
},
|
||||
close: function (e) {
|
||||
element.children('.nav-tree').show();
|
||||
}
|
||||
});
|
||||
|
||||
input.data('autocomplete')._renderMenu = function (ul, items) {
|
||||
var all = _.groupBy(items, 'category');
|
||||
var that = this;
|
||||
scope.menus.forEach(function (menu) {
|
||||
var found = all[menu.name];
|
||||
if (found) {
|
||||
ul.append($("<li class='ui-menu-category'>").html(menu.title));
|
||||
found.forEach(function (item) {
|
||||
that._renderItemData(ul, item);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div>" +
|
||||
"<div class='nav-search-toggle' ng-show='!showSearch'>" +
|
||||
"<i ng-click='onShowSearch()' class='fa fa-angle-down'></i>" +
|
||||
"</div>" +
|
||||
"<div class='nav-search' ng-show='showSearch'>" +
|
||||
"<input type='text'>" +
|
||||
"</div>" +
|
||||
"<ul class='nav nav-tree'>" +
|
||||
"<li ng-repeat='menu in menus track by menu.name' ui-nav-sub-tree x-menu='menu'></li>" +
|
||||
"</ul>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiNavSubTree', ['$compile', function ($compile) {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
menu: "="
|
||||
},
|
||||
require: "^uiNavTree",
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
var menu = scope.menu;
|
||||
if (menu.icon && menu.icon.indexOf('fa') === 0) {
|
||||
menu.fa = menu.icon;
|
||||
delete menu.icon;
|
||||
}
|
||||
if (menu.tagStyle) {
|
||||
menu.tagCss = "label-" + menu.tagStyle;
|
||||
}
|
||||
if (menu.children) {
|
||||
var sub = $(
|
||||
"<ul class='nav ui-nav-sub-tree'>" +
|
||||
"<li ng-repeat='child in menu.children track by child.name' ui-nav-sub-tree x-menu='child'></li>" +
|
||||
"</ul>");
|
||||
sub = $compile(sub)(scope);
|
||||
sub.appendTo(element);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
var icon = element.find("span.nav-icon:first");
|
||||
if (menu.iconBackground && icon.length > 0) {
|
||||
var cssName = menu.parent ? 'color' : 'background-color';
|
||||
var clsName = menu.parent ? 'fg-' : 'bg-';
|
||||
|
||||
if (!menu.parent) {
|
||||
icon.addClass("fg-white");
|
||||
}
|
||||
|
||||
if (menu.iconBackground.indexOf("#") === 0) {
|
||||
icon.css(cssName, menu.iconBackground);
|
||||
} else {
|
||||
icon.addClass(clsName + menu.iconBackground);
|
||||
}
|
||||
|
||||
// get computed color value
|
||||
var color = icon.css(cssName);
|
||||
var bright = d3.rgb(color).brighter(.3).toString();
|
||||
|
||||
// add hover effect
|
||||
element.hover(function () {
|
||||
icon.css(cssName, bright);
|
||||
}, function () {
|
||||
icon.css(cssName, color);
|
||||
});
|
||||
|
||||
// use same color for vertical line
|
||||
if (!menu.parent) {
|
||||
element.css("border-left-color", color);
|
||||
element.hover(function () {
|
||||
element.css("border-left-color", color);
|
||||
}, function () {
|
||||
element.css("border-left-color", bright);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var animation = false;
|
||||
|
||||
function show(el) {
|
||||
|
||||
var parent = el.parent("li");
|
||||
|
||||
if (animation || parent.hasClass('open')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function done() {
|
||||
parent.addClass('open');
|
||||
parent.removeClass('animate');
|
||||
el.height('');
|
||||
animation = false;
|
||||
}
|
||||
|
||||
hide(parent.siblings("li.open").children('ul'));
|
||||
|
||||
animation = true;
|
||||
|
||||
parent.addClass('animate');
|
||||
el.height(el[0].scrollHeight);
|
||||
|
||||
setTimeout(done, 300);
|
||||
}
|
||||
|
||||
function hide(el) {
|
||||
|
||||
var parent = el.parent("li");
|
||||
|
||||
if (animation || !parent.hasClass('open')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function done() {
|
||||
parent.removeClass('open');
|
||||
parent.removeClass('animate');
|
||||
animation = false;
|
||||
}
|
||||
|
||||
animation = true;
|
||||
|
||||
el.height(el.height())[0].offsetHeight;
|
||||
parent.addClass('animate');
|
||||
el.height(0);
|
||||
|
||||
setTimeout(done, 300);
|
||||
}
|
||||
|
||||
element.on('click', '> a', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (animation) return;
|
||||
|
||||
var $list = element.children('ul');
|
||||
|
||||
element.parents('.nav-tree').find('li.active').not(element).removeClass('active');
|
||||
element.addClass('active');
|
||||
|
||||
if (menu.action && (menu.children||[]).length === 0) {
|
||||
scope.$applyAsync(function () {
|
||||
ctrl.onClick(e, menu);
|
||||
});
|
||||
}
|
||||
if ($list.length === 0) return;
|
||||
if (element.hasClass('open')) {
|
||||
hide($list);
|
||||
} else {
|
||||
show($list);
|
||||
}
|
||||
});
|
||||
|
||||
if (menu.help) {
|
||||
setTimeout(function () {
|
||||
var tooltip = element.children('a')
|
||||
.addClass('has-help')
|
||||
.tooltip({
|
||||
html: true,
|
||||
title: menu.help,
|
||||
placement: 'right',
|
||||
delay: { show: 500, hide: 100 },
|
||||
container: 'body'
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<li ng-class='{folder: menu.children, tagged: menu.tag }' data-name='{{::menu.name}}'>" +
|
||||
"<a href='#'>" +
|
||||
"<img class='nav-image' ng-if='::menu.icon' ng-src='{{::menu.icon}}'></img>" +
|
||||
"<span class='nav-icon' ng-if='::menu.fa'><i class='fa' ng-class='::menu.fa'></i></span>" +
|
||||
"<span ng-show='menu.tag' ng-class='menu.tagCss' class='nav-tag label'>{{menu.tag}}</span>" +
|
||||
"<span class='nav-title'>{{::menu.title}}</span>" +
|
||||
"</a>" +
|
||||
"</li>"
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1573
sophal/js/widget/widget.search.js
Normal file
1573
sophal/js/widget/widget.search.js
Normal file
File diff suppressed because it is too large
Load Diff
2913
sophal/js/widget/widget.slickgrid.js
Normal file
2913
sophal/js/widget/widget.slickgrid.js
Normal file
File diff suppressed because it is too large
Load Diff
2893
sophal/js/widget/widget.slickgrid_old.js
Normal file
2893
sophal/js/widget/widget.slickgrid_old.js
Normal file
File diff suppressed because it is too large
Load Diff
473
sophal/js/widget/widget.update.js
Normal file
473
sophal/js/widget/widget.update.js
Normal file
@ -0,0 +1,473 @@
|
||||
/*
|
||||
* 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.directive('uiDeleteButton', [function () {
|
||||
return {
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='btn-group delete-button'>" +
|
||||
"<button class='btn' ng-click='onDelete()' ng-if='hasButton(\"delete\")' ng-disabled='!canDelete()' title='{{ \"Delete\" | t}}'>" +
|
||||
"<i class='fa fa-trash-o'></i> <span ng-if='::!tbTitleHide' x-translate>Delete</span>" +
|
||||
"</button>" +
|
||||
"<button class='btn dropdown-toggle' data-toggle='dropdown' ng-if='hasButton(\"archive\")' ng-disabled='!canArchive()'>" +
|
||||
"<i class='fa fa-caret-down'></i>" +
|
||||
"</button>" +
|
||||
"<ul class='dropdown-menu' ng-if='hasButton(\"archive\")'>" +
|
||||
"<li><a href='' ng-click='onArchive()' x-translate>Archive</a></li>" +
|
||||
"<li><a href='' ng-click='onUnarchive()' x-translate>Unarchive</a></li>" +
|
||||
"</ul>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiUpdateButton', ['$compile', function ($compile) {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
handler: '='
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var menu = element.find('.update-menu'),
|
||||
toggleButton = null;
|
||||
|
||||
scope.visible = false;
|
||||
|
||||
scope.onMassUpdate = function (e) {
|
||||
if (menu && menu.is(':visible')) {
|
||||
hideMenu();
|
||||
return;
|
||||
}
|
||||
toggleButton = $(e.currentTarget);
|
||||
toggleButton.addClass("active");
|
||||
|
||||
scope.onShow(e, menu);
|
||||
|
||||
$(document).on('mousedown.update-menu', onMouseDown);
|
||||
|
||||
scope.$applyAsync(function () {
|
||||
scope.visible = true;
|
||||
});
|
||||
};
|
||||
|
||||
scope.onCancel = function () {
|
||||
hideMenu();
|
||||
};
|
||||
|
||||
scope.canMassUpdate = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
if (scope.handler && scope.handler.canMassUpdate) {
|
||||
scope.canMassUpdate = scope.handler.canMassUpdate;
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
$(document).off('mousedown.update-menu', onMouseDown);
|
||||
if (toggleButton) {
|
||||
toggleButton.removeClass("active");
|
||||
}
|
||||
scope.$applyAsync(function () {
|
||||
scope.visible = false;
|
||||
});
|
||||
return menu.hide();
|
||||
}
|
||||
|
||||
function onMouseDown(e) {
|
||||
var all = $(menu).add(toggleButton);
|
||||
if (all.is(e.target) || all.has(e.target).length > 0) {
|
||||
return;
|
||||
}
|
||||
all = $('.ui-widget-overlay,.ui-datepicker:visible,.ui-dialog:visible,.ui-menu:visible');
|
||||
if (all.is(e.target) || all.has(e.target).length > 0) {
|
||||
return;
|
||||
}
|
||||
if(menu){
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// append box after the button
|
||||
scope.$timeout(function () {
|
||||
element.parents('.view-container').after(menu);
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$(document).off('mousedown.update-menu', onMouseDown);
|
||||
if (menu) {
|
||||
menu.remove();
|
||||
menu = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<button class='btn update-menu-button' ng-click='onMassUpdate($event)' ng-disabled='!canMassUpdate()' >" +
|
||||
"<i class='fa fa-caret-down'></i>" +
|
||||
"<div ui-update-menu x-handler='handler' x-visible='visible'></div>" +
|
||||
"</button>"
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiUpdateDummy', function () {
|
||||
|
||||
return {
|
||||
require: '^uiUpdateForm',
|
||||
scope: {
|
||||
record: '='
|
||||
},
|
||||
controller: ['$scope', '$element', 'DataSource', 'ViewService', function($scope, $element, DataSource, ViewService) {
|
||||
|
||||
var parent = $scope.$parent;
|
||||
var handler = parent.handler;
|
||||
|
||||
$scope._viewParams = {
|
||||
model: handler._model,
|
||||
views: []
|
||||
};
|
||||
|
||||
ui.ViewCtrl($scope, DataSource, ViewService);
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
function prepare(fields) {
|
||||
|
||||
var schema = {
|
||||
cols: 1,
|
||||
type: 'form',
|
||||
items: _.values(fields)
|
||||
};
|
||||
|
||||
$scope.fields = fields;
|
||||
$scope.schema = schema;
|
||||
$scope.schema.loaded = true;
|
||||
}
|
||||
|
||||
var initialized = false;
|
||||
$scope.show = function () {
|
||||
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
var unwatch = parent.$watch('fields', function massFieldsWatch(fields) {
|
||||
if (_.isEmpty(fields)) return;
|
||||
unwatch();
|
||||
prepare(fields);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setEditable();
|
||||
$scope.show();
|
||||
}],
|
||||
link: function (scope, element, attrs) {
|
||||
element.hide();
|
||||
},
|
||||
template: "<div class='hide' ui-view-form x-handler='true'></div>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiUpdateForm', function () {
|
||||
|
||||
function findFields(fields, items) {
|
||||
|
||||
var all = {};
|
||||
var accept = function (field) {
|
||||
var name = field.name;
|
||||
if (!field.massUpdate) return;
|
||||
if (/^(id|version|selected|archived|((updated|created)(On|By)))$/.test(name)) return;
|
||||
if (field.large || field.unique) return;
|
||||
switch (field.type) {
|
||||
case 'one-to-many':
|
||||
case 'many-to-many':
|
||||
case 'binary':
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.target) {
|
||||
field.canNew = false;
|
||||
field.canEdit = false;
|
||||
}
|
||||
field.hidden = false;
|
||||
field.required = false;
|
||||
field.readonly = false;
|
||||
field.onChange = null;
|
||||
field.placeholder = field.placeholder || field.title;
|
||||
|
||||
all[name] = field;
|
||||
};
|
||||
|
||||
_.each(fields, function (field, name) { accept(field); });
|
||||
_.each(items, function (item) {
|
||||
var field = fields[item.name];
|
||||
if (field) {
|
||||
accept(_.extend({}, field, item, { type: field.type }));
|
||||
}
|
||||
});
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
controller: ['$scope', 'ViewService', function ($scope, ViewService) {
|
||||
|
||||
$scope.filters = [{}];
|
||||
$scope.options = [];
|
||||
|
||||
$scope.onInit = _.once(function (view) {
|
||||
|
||||
var handler = $scope.handler;
|
||||
var promise = ViewService.getFields(handler._model);
|
||||
promise.success(function (fields) {
|
||||
$scope.fields = findFields(fields, view.items);
|
||||
$scope.options = _.sortBy(_.values($scope.fields), 'title');
|
||||
$scope.record = {};
|
||||
});
|
||||
});
|
||||
|
||||
$scope.addFilter = function (filter) {
|
||||
var all = $scope.filters;
|
||||
var last = _.last(all);
|
||||
if (last && !last.field) return;
|
||||
if (all.length > 0 && all.length === $scope.options.length) return;
|
||||
$scope.filters.push(filter || {});
|
||||
$scope.updateSelection();
|
||||
};
|
||||
|
||||
$scope.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.notSelected = function (filter) {
|
||||
return function (opt) {
|
||||
return filter.field === opt.name || !opt.selected;
|
||||
};
|
||||
};
|
||||
|
||||
var values = null;
|
||||
var canUpdate = false;
|
||||
|
||||
function updateValues(record) {
|
||||
var keys = _.pluck($scope.filters, 'field'),
|
||||
vals = {};
|
||||
|
||||
_.each(keys, function (key) {
|
||||
if (key) {
|
||||
vals[key] = (record || {})[key];
|
||||
if (vals[key] === undefined) {
|
||||
vals[key] = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
values = vals;
|
||||
canUpdate = !_.isEmpty(values);
|
||||
}
|
||||
|
||||
$scope.updateSelection = function updateSelection () {
|
||||
|
||||
var selected = _.pluck($scope.filters, 'field');
|
||||
_.each($scope.options, function (opt) {
|
||||
opt.selected = selected.indexOf(opt.name) > -1;
|
||||
});
|
||||
|
||||
updateValues($scope.record);
|
||||
};
|
||||
|
||||
$scope.$watch('record', updateValues, true);
|
||||
|
||||
$scope.canUpdate = function () {
|
||||
return canUpdate;
|
||||
};
|
||||
|
||||
$scope.updateAll = false;
|
||||
|
||||
$scope.applyUpdate = function () {
|
||||
|
||||
var handler = $scope.handler;
|
||||
var ds = handler._dataSource;
|
||||
|
||||
function doUpdate() {
|
||||
var promise, items;
|
||||
|
||||
items = _.map(handler.selection, function(index) {
|
||||
return handler.dataView.getItem(index);
|
||||
});
|
||||
items = _.pluck(items, "id");
|
||||
|
||||
if ($scope.updateAll) {
|
||||
items = null;
|
||||
} else if (items.length === 0) {
|
||||
return $scope.onCancel();
|
||||
}
|
||||
|
||||
promise = ds.updateMass(values, items);
|
||||
promise.success(function () {
|
||||
handler.onRefresh();
|
||||
$scope.onCancel();
|
||||
});
|
||||
}
|
||||
|
||||
var count;
|
||||
if ($scope.updateAll) {
|
||||
count = ds._page.total;
|
||||
} else if(handler.selection && handler.selection.length > 0) {
|
||||
count = handler.selection.length;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var message = _t('Do you really want to update all {0} record(s)?', count);
|
||||
axelor.dialogs.confirm(message, function (confirmed) {
|
||||
if (confirmed) {
|
||||
doUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
}],
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.onSelect = function (name) {
|
||||
scope.updateSelection();
|
||||
setTimeout(adjustEditors);
|
||||
};
|
||||
|
||||
scope.clearFilter = function() {
|
||||
scope.filters.length = 0;
|
||||
scope.addFilter();
|
||||
scope.record = {};
|
||||
adjustEditors();
|
||||
};
|
||||
|
||||
scope.remove = function(filter) {
|
||||
scope.removeFilter(filter);
|
||||
adjustEditors();
|
||||
};
|
||||
|
||||
scope.onCancel = function () {
|
||||
if (scope.$parent.onCancel) {
|
||||
scope.$parent.onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
function adjustEditors() {
|
||||
|
||||
element.find('[x-place-for] [x-field]').each(function () {
|
||||
var editor = $(this);
|
||||
var parent = editor.data('$parent');
|
||||
editor.appendTo(parent);
|
||||
});
|
||||
|
||||
_.each(scope.filters, function (filter) {
|
||||
adjustEditor(filter.field);
|
||||
});
|
||||
}
|
||||
|
||||
function adjustEditor(name) {
|
||||
var span = element.find('[x-place-for=' + name + ']');
|
||||
var editor = element.find('[x-field=' + name + '].form-item-container,[x-field=' + name + '].boolean-item').first();
|
||||
var parent = editor.data('$parent');
|
||||
if (!parent) {
|
||||
parent = editor.parent();
|
||||
editor.data('$parent', parent);
|
||||
}
|
||||
editor.appendTo(span);
|
||||
}
|
||||
},
|
||||
template:
|
||||
"<form class='form-inline update-form filter-form'>" +
|
||||
"<strong x-translate>Mass Update</strong> " +
|
||||
"<hr>" +
|
||||
"<table class='form-layout'>" +
|
||||
"<tr ng-repeat='filter in filters' class='form-inline'>" +
|
||||
"<td class='filter-remove'>" +
|
||||
"<a href='' ng-click='remove(filter)'><i class='fa fa-times'></i></a>" +
|
||||
"</td>" +
|
||||
"<td class='form-item'>" +
|
||||
"<span class='form-item-container'>" +
|
||||
"<select ng-model='filter.field' ng-options='v.name as v.title for v in options | filter:notSelected(filter)' ng-change='onSelect(filter.field)'></select>" +
|
||||
"</span>" +
|
||||
"</td>" +
|
||||
"<td class='form-item' x-place-for='{{filter.field}}'>" +
|
||||
"</td>" +
|
||||
"</tr>" +
|
||||
"</table>" +
|
||||
"<div class='links'>"+
|
||||
"<a href='' ng-click='addFilter()' x-translate>Add Field</a>" +
|
||||
"<span class='divider'>|</span>"+
|
||||
"<a href='' ng-click='clearFilter()' x-translate>Clear</a>" +
|
||||
"</div>" +
|
||||
"<div ui-update-dummy x-record='record'></div>"+
|
||||
"</form>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiUpdateMenu', function () {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
scope: {
|
||||
handler: '='
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.$parent.onShow = function (event, menu) {
|
||||
scope.handler._viewPromise.then(function (view) {
|
||||
var elem = $(event.currentTarget);
|
||||
if (scope.onInit) {
|
||||
scope.onInit(view);
|
||||
}
|
||||
menu.show();
|
||||
menu.position({
|
||||
my: "left top",
|
||||
at: "left bottom",
|
||||
of: elem
|
||||
});
|
||||
});
|
||||
};
|
||||
},
|
||||
template:
|
||||
"<div class='update-menu filter-menu' ui-watch-if='$parent.visible'>" +
|
||||
"<div ui-update-form></div>" +
|
||||
"<hr>" +
|
||||
"<div class='form-inline'>" +
|
||||
"<button class='btn btn-small' ng-disabled='!canUpdate()' ng-click='applyUpdate()'><span x-translate>Update</span></button> " +
|
||||
"<button class='btn btn-small' ng-click='onCancel()'><span x-translate>Cancel</span></button> " +
|
||||
"<label class='checkbox update-all'>" +
|
||||
"<input type='checkbox' ng-model='updateAll'> <span x-translate>Update all</span>" +
|
||||
"</label> " +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user