First commit waiting for Budget Alert

This commit is contained in:
2025-09-04 13:37:35 +01:00
commit 2d681f27f5
4563 changed files with 1061534 additions and 0 deletions

View 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;
}
});
}
};
});
})();

View 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);
};
});
})();

View 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>"
};
});
})();

View 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'
};
});
})();

View 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>"
};
}]);
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>"
};
});
})();