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

867
sophal/js/view/view.base.js Normal file
View 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');
});
};
});
})();

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

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

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

View 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

File diff suppressed because it is too large Load Diff

1507
sophal/js/view/view.form.js Normal file

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

130
sophal/js/view/view.html.js Normal file
View 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);
})();

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

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

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

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