689 lines
19 KiB
JavaScript
689 lines
19 KiB
JavaScript
/*
|
|
* Axelor Business Solutions
|
|
*
|
|
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
/**
|
|
* 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);
|
|
});
|
|
});
|
|
|
|
})();
|