First commit waiting for Budget Alert
This commit is contained in:
867
sophal/js/view/view.base.js
Normal file
867
sophal/js/view/view.base.js
Normal file
@ -0,0 +1,867 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module("axelor.ui");
|
||||
|
||||
ui.ViewCtrl = ViewCtrl;
|
||||
ui.ViewCtrl.$inject = ['$scope', 'DataSource', 'ViewService'];
|
||||
|
||||
function ViewCtrl($scope, DataSource, ViewService) {
|
||||
|
||||
$scope._viewParams = $scope._viewParams || $scope.selectedTab;
|
||||
if (!$scope._viewParams) {
|
||||
throw "View parameters are not provided.";
|
||||
}
|
||||
|
||||
var params = $scope._viewParams;
|
||||
|
||||
$scope._views = ViewService.accept(params);
|
||||
$scope._viewType = params.viewType;
|
||||
|
||||
if ($scope.$parent && $scope.$parent._model) {
|
||||
$scope._parentModel = $scope.$parent._model;
|
||||
}
|
||||
|
||||
$scope._model = params.model;
|
||||
$scope._fields = {};
|
||||
|
||||
$scope._dataSource = null;
|
||||
$scope._domain = params.domain;
|
||||
$scope._context = params.context;
|
||||
|
||||
if (params.model) {
|
||||
$scope._dataSource = DataSource.create(params.model, params);
|
||||
}
|
||||
|
||||
$scope._defer = function() {
|
||||
return ViewService.defer();
|
||||
};
|
||||
|
||||
$scope.loadView = function(viewType, viewName) {
|
||||
var view = $scope._views[viewType] || {
|
||||
type: viewType,
|
||||
name: viewName
|
||||
};
|
||||
var ctx = $scope._context;
|
||||
if ($scope.getContext) {
|
||||
ctx = $scope.getContext();
|
||||
}
|
||||
return ViewService.getMetaDef($scope._model, view, ctx);
|
||||
};
|
||||
|
||||
$scope.loadFields = function() {
|
||||
return ViewService.getFields($scope._model);
|
||||
};
|
||||
|
||||
$scope.updateRoute = function() {
|
||||
this.$emit("on:update-route");
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
throw "Not Implemented.";
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
throw "Not Implemented.";
|
||||
};
|
||||
|
||||
var switchedTo = null;
|
||||
|
||||
$scope.switchTo = function(viewType, /* optional */ callback) {
|
||||
|
||||
var view = $scope._views[viewType];
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = view.deferred.promise;
|
||||
promise.then(function(viewScope){
|
||||
|
||||
if (!viewScope || switchedTo === viewType) {
|
||||
return;
|
||||
}
|
||||
|
||||
switchedTo = viewType;
|
||||
|
||||
$scope._viewTypeLast = $scope._viewType === 'form' ? $scope._viewTypeLast : $scope._viewType;
|
||||
$scope._viewType = viewType;
|
||||
$scope._viewParams.viewType = viewType; //XXX: remove
|
||||
$scope._viewParams.$viewScope = viewScope;
|
||||
|
||||
viewScope.show();
|
||||
|
||||
if (viewScope.updateRoute) {
|
||||
viewScope.updateRoute();
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(viewScope);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!params.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hide toolbar button titles
|
||||
$scope.tbTitleHide = !axelor.config['view.toolbar.titles'];
|
||||
|
||||
function switchAndEdit(id, readonly) {
|
||||
$scope.switchTo('form', function(scope) {
|
||||
scope._viewPromise.then(function() {
|
||||
scope.doRead(id).success(function(record) {
|
||||
scope.edit(record);
|
||||
scope.setEditable(!readonly);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// show single or default record if specified
|
||||
var context = params.context || {};
|
||||
if (context._showSingle || context._showRecord) {
|
||||
var ds = $scope._dataSource;
|
||||
var forceEdit = (params.params||{}).forceEdit === true;
|
||||
|
||||
if (context._showRecord > 0) {
|
||||
params.viewType = "form";
|
||||
return $scope.switchTo('form');
|
||||
}
|
||||
|
||||
return ds.search({
|
||||
offset: 0,
|
||||
limit: 2,
|
||||
fields: ["id"]
|
||||
}).success(function(records, page){
|
||||
if (page.total === 1 && records.length === 1) {
|
||||
return switchAndEdit(records[0].id, !forceEdit);
|
||||
}
|
||||
return $scope.switchTo($scope._viewType || 'grid');
|
||||
});
|
||||
}
|
||||
|
||||
// switch to the the current viewType
|
||||
$scope.switchTo($scope._viewType || 'grid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Base controller for DataSource views. This controller should not be used
|
||||
* directly but actual controller should inherit from it.
|
||||
*
|
||||
*/
|
||||
ui.DSViewCtrl = function DSViewCtrl(type, $scope, $element) {
|
||||
|
||||
if (!type) {
|
||||
throw "No view type provided.";
|
||||
}
|
||||
if (!$scope._dataSource) {
|
||||
throw "DataSource is not provided.";
|
||||
}
|
||||
|
||||
$scope._viewResolver = $scope._defer();
|
||||
$scope._viewPromise = $scope._viewResolver.promise;
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
var view = $scope._views[type] || {};
|
||||
var viewPromise = null;
|
||||
var hiddenButtons = {};
|
||||
|
||||
var params = $scope._viewParams;
|
||||
|
||||
if (params.params && params.params.limit) {
|
||||
if (ds && ds._page) {
|
||||
ds._page.limit = +(params.params.limit) || ds._page.limit;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.fields = {};
|
||||
$scope.fields_related = {};
|
||||
$scope.schema = null;
|
||||
|
||||
$scope.show = function() {
|
||||
if (!viewPromise) {
|
||||
viewPromise = $scope.loadView(type, view.name);
|
||||
viewPromise.then(function(meta){
|
||||
var schema = meta.view;
|
||||
var fields = meta.fields || params.fields;
|
||||
var toolbar = [];
|
||||
_.each(schema.toolbar, function(button){
|
||||
button.custom = true;
|
||||
if (/^(new|edit|save|delete|copy|cancel|back|refresh|search|export|log|files)$/.test(button.name)) {
|
||||
hiddenButtons[button.name] = button;
|
||||
button.custom = false;
|
||||
}
|
||||
toolbar.push(button);
|
||||
});
|
||||
var forceTitle = params.forceTitle;
|
||||
if (forceTitle === undefined) {
|
||||
forceTitle = (params.params||{}).forceTitle;
|
||||
}
|
||||
if (!forceTitle && schema.title) {
|
||||
$scope.viewTitle = schema.title;
|
||||
}
|
||||
$scope.fields = fields;
|
||||
$scope.fields_related = meta.related;
|
||||
$scope.schema = schema;
|
||||
$scope.toolbar = toolbar;
|
||||
$scope.menubar = schema.menubar;
|
||||
|
||||
$scope.toolbarAsMenu = _.isEmpty(toolbar) ? null : [{
|
||||
icon: 'fa-wrench',
|
||||
isButton: true,
|
||||
items: _.map(toolbar, function (item) {
|
||||
return _.extend({}, item, {
|
||||
name: item.name,
|
||||
action: item.onClick,
|
||||
title: item.title || item.autoTitle || item.name
|
||||
});
|
||||
})
|
||||
}];
|
||||
|
||||
// watch on view.loaded to improve performance
|
||||
schema.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.onShow(viewPromise);
|
||||
};
|
||||
|
||||
$scope.onShow = function(promise) {
|
||||
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return ds && ds.canNext();
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return ds && ds.canPrev();
|
||||
};
|
||||
|
||||
$scope.getPageSize = function() {
|
||||
var page = ds && ds._page;
|
||||
if (page) {
|
||||
return page.limit;
|
||||
}
|
||||
return 40;
|
||||
};
|
||||
|
||||
$scope.setPageSize = function(value) {
|
||||
var page = ds && ds._page,
|
||||
limit = Math.max(0, +value) || 40;
|
||||
if (page && page.limit != limit) {
|
||||
page.limit = limit;
|
||||
$scope.onRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
var can = (function (scope) {
|
||||
var fn = null;
|
||||
var perms = {
|
||||
'new': 'create',
|
||||
'copy': 'create',
|
||||
'edit': 'write',
|
||||
'save': 'write',
|
||||
'delete': 'remove',
|
||||
'archive': 'remove',
|
||||
'export': 'export'
|
||||
};
|
||||
var actions = {
|
||||
'new': 'canNew',
|
||||
'edit': 'canEdit',
|
||||
'save': 'canSave',
|
||||
'copy': 'canCopy',
|
||||
'delete': 'canDelete',
|
||||
'archive': 'canArchive',
|
||||
'attach': 'canAttach'
|
||||
};
|
||||
|
||||
function attr(which) {
|
||||
if (fn === null && _.isFunction(scope.attr)) {
|
||||
fn = scope.attr;
|
||||
}
|
||||
return !fn || fn(which) !== false;
|
||||
}
|
||||
|
||||
function perm(which) {
|
||||
return which === undefined || scope.hasPermission(which);
|
||||
}
|
||||
|
||||
return function can(what) {
|
||||
return attr(actions[what]) && perm(perms[what]);
|
||||
};
|
||||
})($scope);
|
||||
|
||||
$scope.hasButton = function(name) {
|
||||
if (!can(name)) {
|
||||
return false;
|
||||
}
|
||||
if (_(hiddenButtons).has(name)) {
|
||||
var button = hiddenButtons[name];
|
||||
if (button.isHidden) {
|
||||
return !button.isHidden();
|
||||
}
|
||||
return !button.hidden;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.hasPermission = function(perm) {
|
||||
var view = $scope.schema;
|
||||
var defaultValue = arguments.length === 2 ? arguments[1] : true;
|
||||
if (!view || !view.perms) return defaultValue;
|
||||
var perms = view.perms;
|
||||
var permitted = perms[perm];
|
||||
if (permitted === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
return _.toBoolean(permitted);
|
||||
};
|
||||
|
||||
$scope.isPermitted = function(perm, record, callback) {
|
||||
var ds = this._dataSource;
|
||||
ds.isPermitted(perm, record).success(function(res){
|
||||
var errors = res.errors;
|
||||
if (errors) {
|
||||
return axelor.dialogs.error(errors.read);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.canShowToolbar = function() {
|
||||
var params = ($scope._viewParams || {}).params;
|
||||
if (params && params['show-toolbar'] === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.hasHelp = function() {
|
||||
var view = $scope.schema;
|
||||
return view ? view.helpLink : false;
|
||||
};
|
||||
|
||||
$scope.onShowHelp = function() {
|
||||
if ($scope.hasHelp()) {
|
||||
window.open($scope.schema.helpLink);
|
||||
}
|
||||
};
|
||||
|
||||
if (view.deferred) {
|
||||
view.deferred.resolve($scope);
|
||||
}
|
||||
|
||||
$scope.$on('on:tab-reload', function(e, tab) {
|
||||
if ($scope === e.targetScope && $scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ui.directive('uiViewPane', function() {
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
controller: ['$scope', '$attrs', 'DataSource', 'ViewService', function ($scope, $attrs, DataSource, ViewService) {
|
||||
|
||||
var params = $scope.$eval($attrs.uiViewPane);
|
||||
|
||||
$scope._viewParams = params;
|
||||
ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.viewList = [];
|
||||
$scope.viewType = null;
|
||||
|
||||
var switchTo = $scope.switchTo;
|
||||
$scope.switchTo = function (type, callback) {
|
||||
var view = $scope._views[type];
|
||||
if (view && $scope.viewList.indexOf(type) === -1) {
|
||||
$scope.viewList.push(type);
|
||||
}
|
||||
var viewScope = !$scope._isPopup && $scope.selectedTab && $scope.selectedTab.$viewScope;
|
||||
if (viewScope && viewScope.viewType === 'form' && viewScope.viewType !== type) {
|
||||
viewScope.$$resetForm();
|
||||
}
|
||||
$scope.viewType = type;
|
||||
return switchTo(type, callback);
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTab.viewType', function viewTypeWatch(type) {
|
||||
var params = $scope._viewParams;
|
||||
if (params && params.$viewScope !== ($scope.selectedTab || {}).$viewScope) {
|
||||
return;
|
||||
}
|
||||
if ($scope.viewType !== type && type) {
|
||||
$scope.switchTo(type);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.viewTemplate = function (type) {
|
||||
var tname = "ui-template:" + type;
|
||||
var template = type;
|
||||
if (params.params && params.params[tname]) {
|
||||
template = params.params[tname];
|
||||
}
|
||||
return 'partials/views/' + template + '.html';
|
||||
};
|
||||
|
||||
var type = params.viewType || params.type;
|
||||
$scope.keepAttached = $scope._isPopup || (params.params||{}).popup || type === 'html';
|
||||
$scope.switchTo(type);
|
||||
}],
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
},
|
||||
template:
|
||||
"<div class='view-pane' ui-attach='keepAttached || tab.selected'>" +
|
||||
"<div class='view-container'" +
|
||||
" ng-repeat='type in viewList'" +
|
||||
" ui-show='type === viewType'" +
|
||||
" ui-attach-scroll ui-attach='keepAttached || type == viewType'" +
|
||||
" ng-include='viewTemplate(type)'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewPopup', function() {
|
||||
|
||||
return {
|
||||
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
|
||||
var params = $scope.$eval($attrs.uiViewPopup);
|
||||
|
||||
$scope.tab = params;
|
||||
$scope._isPopup = true;
|
||||
|
||||
$scope.onHotKey = function (e, action) {
|
||||
return false;
|
||||
};
|
||||
|
||||
var canClose = false;
|
||||
|
||||
$scope.onOK = function () {
|
||||
$scope.closeTab($scope.tab, function() {
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onBeforeClose = function(e) {
|
||||
if (canClose) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$scope.onOK();
|
||||
};
|
||||
|
||||
$scope.onPopupClose = function () {
|
||||
var tab = $scope.tab,
|
||||
params = tab.params || {},
|
||||
parent = tab.$popupParent;
|
||||
|
||||
while (parent && parent.$$destroyed && parent.tab) {
|
||||
parent = parent.tab.$popupParent;
|
||||
}
|
||||
if (parent && parent.reload && params.popup === "reload") {
|
||||
parent.reload();
|
||||
}
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
|
||||
$scope.onPopupOK = function () {
|
||||
var viewScope = $scope._viewParams.$viewScope;
|
||||
if (!viewScope.onSave || (!viewScope.isDirty() && viewScope.id)) {
|
||||
return $scope.onOK();
|
||||
}
|
||||
return viewScope.onSave({ fireOnLoad: false }).then(function(record, page) {
|
||||
viewScope.edit(record);
|
||||
viewScope.$timeout($scope.onOK.bind($scope));
|
||||
});
|
||||
};
|
||||
|
||||
params = $scope.tab.params || {};
|
||||
if (params['popup-save'] === false) {
|
||||
$scope.onPopupOK = false;
|
||||
}
|
||||
}],
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.$watch('viewTitle', function viewTitleWatch(title) {
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
scope.waitForActions(function () {
|
||||
if (scope._viewParams.viewType === 'html') {
|
||||
scope.viewTitle = scope.tabTitle(scope._viewParams);
|
||||
scope._doShow();
|
||||
return;
|
||||
}
|
||||
|
||||
var unwatch = scope.$watch("_viewParams.$viewScope.schema.loaded", function viewLoadedWatch(loaded) {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
var viewScope = scope._viewParams.$viewScope;
|
||||
var viewPromise = viewScope._viewPromise;
|
||||
scope.viewTitle = scope.tabTitle(scope._viewParams);
|
||||
scope.$broadcast('grid:adjust-size', viewScope);
|
||||
scope._doShow(viewPromise);
|
||||
});
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-resizable="true" x-on-close="onPopupClose" x-on-ok="onPopupOK" x-on-before-close="onBeforeClose">' +
|
||||
'<div ui-view-pane="tab"></div>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiRecordPager', function(){
|
||||
|
||||
return {
|
||||
replace: true,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var elText = element.find('.record-pager-text').show(),
|
||||
elChanger = element.find('.record-pager-change').hide(),
|
||||
elInput = elChanger.find('input');
|
||||
|
||||
scope.showText = attrs.uiRecordPager !== "no-text";
|
||||
|
||||
function updatePageSize() {
|
||||
var size = +(elInput.val()) || 0;
|
||||
if (scope.setPageSize && size > 0) {
|
||||
scope.setPageSize(size);
|
||||
}
|
||||
elText.add(elChanger).toggle();
|
||||
}
|
||||
|
||||
elText.click(function(e) {
|
||||
elText.add(elChanger).toggle();
|
||||
elInput.zIndex(elInput.parent().zIndex() + 1).focus().select();
|
||||
});
|
||||
|
||||
elInput.on('click', function () {
|
||||
elInput.zIndex(elInput.parent().zIndex() + 1).focus().select();
|
||||
});
|
||||
|
||||
elChanger.on('click', 'button', function() {
|
||||
updatePageSize();
|
||||
});
|
||||
|
||||
elChanger.keyup(function(e) {
|
||||
if(e.keyCode == 13) { // ENTER
|
||||
updatePageSize();
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
template:
|
||||
'<div class="record-pager hidden-phone">'+
|
||||
'<span ng-show="showText">'+
|
||||
'<span class="record-pager-text">{{pagerText()}}</span>'+
|
||||
'<span class="input-append record-pager-change">'+
|
||||
'<input type="text" style="width: 30px;" value="{{getPageSize()}}">'+
|
||||
'<button type="button" class="btn add-on"><i class="fa fa-check"></i></button>'+
|
||||
'</span>'+
|
||||
'</span>'+
|
||||
'<div class="btn-group">'+
|
||||
'<button class="btn" ng-disabled="!canPrev()" ng-click="onPrev()"><i class="fa fa-chevron-left"></i></button>'+
|
||||
'<button class="btn" ng-disabled="!canNext()" ng-click="onNext()"><i class="fa fa-chevron-right"></i></button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewCustomize', ['NavService', function(NavService) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
scope.canShow = function () {
|
||||
if (!axelor.config['user.technical']) {
|
||||
return false;
|
||||
}
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && (view.viewId || view.modelId);
|
||||
};
|
||||
|
||||
scope.hasViewID = function () {
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && view.viewId;
|
||||
};
|
||||
|
||||
scope.hasModelID = function () {
|
||||
var viewScope = (scope.selectedTab || {}).$viewScope;
|
||||
var view = viewScope && viewScope.schema;
|
||||
return view && view.modelId;
|
||||
};
|
||||
|
||||
scope.hasActionID = function () {
|
||||
return (scope.selectedTab || {}).actionId;
|
||||
};
|
||||
|
||||
scope.onShowView = function () {
|
||||
var id = scope.hasViewID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaView", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
scope.waitForActions(function () {
|
||||
var vs = (scope.selectedTab || {}).$viewScope;
|
||||
if (vs && vs.setEditable) {
|
||||
vs.setEditable();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.onShowModel = function () {
|
||||
var id = scope.hasModelID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaModel", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
};
|
||||
|
||||
scope.onShowAction = function () {
|
||||
var id = scope.hasActionID();
|
||||
NavService.openTabByName("form::com.axelor.meta.db.MetaAction", {
|
||||
mode: "edit",
|
||||
state: id
|
||||
});
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<ul ng-show='canShow()' class='nav menu-bar view-customize hidden-phone'>" +
|
||||
"<li class='dropdown menu'>" +
|
||||
"<a class='dropdown-toggle btn' data-toggle='dropdown' title='{{ \"Customize...\" | t}}'>" +
|
||||
"<i class='fa fa-wrench'></i>" +
|
||||
"</a>" +
|
||||
"<ul class='dropdown-menu pull-right'>" +
|
||||
"<li><a ng-click='onShowView()' ng-show='hasViewID()'>View...</a></li>" +
|
||||
"<li><a ng-click='onShowModel()' ng-show='hasModelID()'>Model...</a></li>" +
|
||||
"<li><a ng-click='onShowAction()' ng-show='hasActionID()'>Action...</a></li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"</ul>"
|
||||
};
|
||||
}]);
|
||||
|
||||
function viewSwitcher(scope, element, attrs) {
|
||||
|
||||
var params = (scope._viewParams || scope.tab);
|
||||
var viewTypes = _.pluck(params.views, 'type');
|
||||
|
||||
if ((params.viewType || params.type) === 'dashboard') {
|
||||
element.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
element.find("[x-view-type]").click(function(e) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
var type = $(this).attr("x-view-type");
|
||||
var vs = params.$viewScope || (scope.selectedTab || {}).$viewScope;
|
||||
var ds = vs._dataSource;
|
||||
var page = ds && ds._page;
|
||||
|
||||
if (type === "form" && page) {
|
||||
if (page.index === -1) page.index = 0;
|
||||
}
|
||||
|
||||
if ((scope.selectedTab || {}).viewType === 'grid') {
|
||||
var items = vs.getItems() || [];
|
||||
var index = _.first(vs.selection || []);
|
||||
if (index === undefined && items.length === 0 && vs.schema.canNew === false) {
|
||||
return;
|
||||
}
|
||||
if (index !== undefined) page.index = index;
|
||||
}
|
||||
|
||||
vs.switchTo(type);
|
||||
vs.$applyAsync();
|
||||
}).each(function() {
|
||||
var type = $(this).attr("x-view-type");
|
||||
if (viewTypes.indexOf(type) === -1) {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
|
||||
var watchExpr = scope._viewParams ? '_viewType' : 'tab.viewType';
|
||||
scope.$watch(watchExpr, function viewTypeWatch(type) {
|
||||
element.find("[x-view-type]").attr("disabled", false);
|
||||
element.find("[x-view-type][x-view-type=" + type + "]").attr("disabled", true);
|
||||
});
|
||||
}
|
||||
|
||||
ui.directive('uiViewSwitcher', function(){
|
||||
return {
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
element.parents('.view-container:first').addClass('has-toolbar');
|
||||
viewSwitcher(scope, element, attrs);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="view-switcher pull-right hidden-phone">'+
|
||||
'<div class="btn-group">'+
|
||||
'<button class="btn" x-view-type="grid"><i class="fa fa-list"></i></button>'+
|
||||
'<button class="btn" x-view-type="cards"><i class="fa fa-th-large"></i></button>'+
|
||||
'<button class="btn" x-view-type="kanban"><i class="fa fa-columns"></i></button>'+
|
||||
'<button class="btn" x-view-type="calendar"><i class="fa fa-calendar"></i></button>'+
|
||||
'<button class="btn" x-view-type="gantt"><i class="fa fa-calendar"></i></button>'+
|
||||
'<button class="btn" x-view-type="chart"><i class="fa fa-bar-chart-o"></i></button>'+
|
||||
'<button class="btn" x-view-type="form" ><i class="fa fa-file-text-o"></i></button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiViewSwitcherMenu', function(){
|
||||
return {
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
viewSwitcher(scope, element, attrs);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<span class='view-switch-menu dropdown pull-right'>" +
|
||||
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-ellipsis-v'></i></a>" +
|
||||
"<ul class='dropdown-menu'>" +
|
||||
"<li><a href='' x-view-type='grid' x-translate>Grid</a></li>" +
|
||||
"<li><a href='' x-view-type='cards' x-translate>Cards</a></li>" +
|
||||
"<li><a href='' x-view-type='kanban' x-translate>Kanban</a></li>" +
|
||||
"<li><a href='' x-view-type='calendar' x-translate>Calendar</a></li>" +
|
||||
"<li><a href='' x-view-type='gantt' x-translate>Gantt</a></li>" +
|
||||
"<li><a href='' x-view-type='chart' x-translate>Chart</a></li>" +
|
||||
"<li><a href='' x-view-type='form' x-translate>Form</a></li>" +
|
||||
"</ul>" +
|
||||
"</span>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiHotKeys', function() {
|
||||
|
||||
var keys = {
|
||||
45: 'new', // insert
|
||||
69: 'edit', // e
|
||||
83: 'save', // s
|
||||
68: 'delete', // d
|
||||
82: 'refresh', // r
|
||||
70: 'search', // f
|
||||
71: 'select', // g
|
||||
74: 'prev', // j
|
||||
75: 'next', // n
|
||||
|
||||
77: 'focus-menu', // m
|
||||
120: 'toggle-menu', // F9
|
||||
|
||||
81: 'close' // q
|
||||
};
|
||||
|
||||
return function(scope, element, attrs) {
|
||||
|
||||
var loginWindow = $("#loginWindow");
|
||||
|
||||
$(document).on('keydown.axelor-keys', function (e) {
|
||||
|
||||
if (loginWindow.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// disable backspace as back button
|
||||
if (e.which === 8 && e.target === document.body) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
var action = keys[e.which];
|
||||
|
||||
if (action === "toggle-menu") {
|
||||
$('#offcanvas-toggle a').click();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.altKey || e.shiftKey || !e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "focus-menu") {
|
||||
var activeMenu = $('.sidebar .nav-tree li.active');
|
||||
if (activeMenu.length === 0) {
|
||||
activeMenu = $('.sidebar .nav-tree li:first');
|
||||
}
|
||||
|
||||
var navTree = activeMenu.parents('[nav-tree]:first');
|
||||
if (navTree.length) {
|
||||
navTree.navtree('selectItem', activeMenu);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var tab = scope.selectedTab,
|
||||
dlg = $('[ui-editor-popup]:visible:last,[ui-view-popup]:visible:last,[ui-dms-popup]:visible:last').first(),
|
||||
vs = tab ? tab.$viewScope : null;
|
||||
|
||||
if (dlg.length) {
|
||||
vs = dlg.scope();
|
||||
}
|
||||
|
||||
if (!vs || !keys.hasOwnProperty(e.which)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "close") {
|
||||
scope.closeTab(tab, function() {
|
||||
scope.$applyAsync();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action === "search") {
|
||||
var filterBox = $('.filter-box .search-query:visible');
|
||||
if (filterBox.length) {
|
||||
filterBox.focus().select();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isFunction(vs.onHotKey)) {
|
||||
return vs.onHotKey(e, action);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$(document).off('keydown.axelor-keys');
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
769
sophal/js/view/view.calendar.js
Normal file
769
sophal/js/view/view.calendar.js
Normal file
@ -0,0 +1,769 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
/* global d3: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.controller('CalendarViewCtrl', CalendarViewCtrl);
|
||||
|
||||
CalendarViewCtrl.$inject = ['$scope', '$element'];
|
||||
function CalendarViewCtrl($scope, $element) {
|
||||
|
||||
ui.DSViewCtrl('calendar', $scope, $element);
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
|
||||
var view = {};
|
||||
var colors = {};
|
||||
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.refresh();
|
||||
}
|
||||
|
||||
viewPromise.then(function(){
|
||||
|
||||
var schema = $scope.schema;
|
||||
|
||||
initialized = true;
|
||||
|
||||
view = {
|
||||
start: schema.eventStart,
|
||||
stop: schema.eventStop,
|
||||
length: parseInt(schema.eventLength) || 0,
|
||||
color: schema.colorBy,
|
||||
title: schema.items[0].name
|
||||
};
|
||||
|
||||
$scope._viewResolver.resolve(schema, $element);
|
||||
$scope.updateRoute();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isAgenda = function() {
|
||||
var field = this.fields[view.start];
|
||||
return field && field.type === "datetime";
|
||||
};
|
||||
|
||||
var d3_colors = d3.scale.category10().range().concat(
|
||||
d3.scale.category20().range());
|
||||
|
||||
function nextColor(n) {
|
||||
if (n === undefined || n < 0 || n >= d3_colors.length) {
|
||||
n = _.random(0, d3_colors.length);
|
||||
}
|
||||
var c = d3.rgb(d3_colors[n]);
|
||||
return {
|
||||
bg: "" + c,
|
||||
fg: "" + c.brighter(99),
|
||||
bc: "" + c.darker(0.9)
|
||||
};
|
||||
}
|
||||
|
||||
$scope.fetchItems = function(start, end, callback) {
|
||||
var fields = _.pluck(this.fields, 'name');
|
||||
var criteria = {
|
||||
operator: "and",
|
||||
criteria: [{
|
||||
fieldName: view.start,
|
||||
operator: ">=",
|
||||
value: start
|
||||
}, {
|
||||
fieldName: view.start,
|
||||
operator: "<=",
|
||||
value: end
|
||||
}]
|
||||
};
|
||||
|
||||
// make sure to include items whose end date falls in current range
|
||||
if (view.stop) {
|
||||
criteria = {
|
||||
operator: "or",
|
||||
criteria: [criteria, {
|
||||
operator: "and",
|
||||
criteria: [{
|
||||
fieldName: view.stop,
|
||||
operator: ">=",
|
||||
value: start
|
||||
}, {
|
||||
fieldName: view.stop,
|
||||
operator: "<=",
|
||||
value: end
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// consider stored filter
|
||||
if (ds._filter) {
|
||||
if (ds._filter.criteria) {
|
||||
criteria = {
|
||||
operator: "and",
|
||||
criteria: [criteria].concat(ds._filter.criteria)
|
||||
};
|
||||
}
|
||||
if (_.size(ds._filter._domains) > 0) {
|
||||
criteria._domains = ds._filter._domains;
|
||||
}
|
||||
}
|
||||
|
||||
var opts = {
|
||||
fields: fields,
|
||||
filter: criteria,
|
||||
domain: this._domain,
|
||||
context: this._context,
|
||||
store: false,
|
||||
limit: -1
|
||||
};
|
||||
|
||||
ds.search(opts).success(function(records) {
|
||||
var items = _.clone(records);
|
||||
items.sort(function (x, y) { return x.id - y.id; });
|
||||
updateColors(items, true);
|
||||
callback(records);
|
||||
});
|
||||
};
|
||||
|
||||
function updateColors(records, reset) {
|
||||
var colorBy = view.color;
|
||||
var colorField = $scope.fields[colorBy];
|
||||
|
||||
if (!colorField) {
|
||||
return colors;
|
||||
}
|
||||
if (reset) {
|
||||
colors = {};
|
||||
}
|
||||
|
||||
_.each(records, function(record) {
|
||||
var item = record[colorBy];
|
||||
if (item === null || item === undefined) {
|
||||
return;
|
||||
}
|
||||
var key = $scope.getColorKey(record, item);
|
||||
var title = colorField.targetName ? item[colorField.targetName] : item;
|
||||
if (colorField.selectionList) {
|
||||
var select = _.find(colorField.selectionList, function (select) {
|
||||
return ("" + select.value) === ("" + title);
|
||||
});
|
||||
if (select) {
|
||||
title = select.title;
|
||||
}
|
||||
}
|
||||
if (!colors[key]) {
|
||||
colors[key] = {
|
||||
item: item,
|
||||
title: title || _t('Unknown'),
|
||||
color: nextColor(_.size(colors))
|
||||
};
|
||||
}
|
||||
record.$colorKey = key;
|
||||
});
|
||||
return colors;
|
||||
}
|
||||
|
||||
$scope.getColors = function() {
|
||||
return colors;
|
||||
};
|
||||
|
||||
$scope.getColor = function(record) {
|
||||
var key = this.getColorKey(record);
|
||||
if (key && !colors[key]) {
|
||||
updateColors([record], false);
|
||||
}
|
||||
if (colors[key]) {
|
||||
return colors[key].color;
|
||||
}
|
||||
return nextColor(0);
|
||||
};
|
||||
|
||||
$scope.getColorKey = function(record, key) {
|
||||
if (key) {
|
||||
return "" + (key.id || key);
|
||||
}
|
||||
if (record) {
|
||||
return this.getColorKey(null, record[view.color]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.getEventInfo = function(record) {
|
||||
var info = {},
|
||||
value;
|
||||
|
||||
value = record[view.start];
|
||||
info.start = value ? moment(value) : moment();
|
||||
|
||||
value = record[view.stop];
|
||||
info.end = value ? moment(value) : moment(info.start).add(view.length || 1, "hours");
|
||||
|
||||
var title = this.fields[view.title];
|
||||
var titleText = null;
|
||||
|
||||
if (title) {
|
||||
value = record[title.name];
|
||||
if (title.targetName) {
|
||||
value = value[title.targetName];
|
||||
} else if (title.selectionList) {
|
||||
var select = _.find(title.selectionList, function (select) {
|
||||
return ("" + select.value) === ("" + value);
|
||||
});
|
||||
if (select) {
|
||||
titleText = select.title;
|
||||
}
|
||||
}
|
||||
titleText = titleText || value || _t('Unknown');
|
||||
}
|
||||
|
||||
info.title = ("" + titleText);
|
||||
info.allDay = isAllDay(info);
|
||||
info.className = info.allDay ? "calendar-event-allDay" : "calendar-event-day";
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
function isAllDay(event) {
|
||||
if($scope.fields[view.start] && $scope.fields[view.start].type === 'date') {
|
||||
return true;
|
||||
}
|
||||
var start = moment(event.start);
|
||||
var end = moment(event.end);
|
||||
if (start.format("HH:mm") !== "00:00") {
|
||||
return false;
|
||||
}
|
||||
return !event.end || end.format("HH:mm") === "00:00";
|
||||
}
|
||||
|
||||
$scope.onEventChange = function(event, delta) {
|
||||
|
||||
var record = _.clone(event.record);
|
||||
|
||||
var start = event.start;
|
||||
var end = event.end;
|
||||
|
||||
if (isAllDay(event)) {
|
||||
start = start.clone().startOf("day").local();
|
||||
end = (end || start).clone().startOf("day").local();
|
||||
}
|
||||
|
||||
record[view.start] = start;
|
||||
record[view.stop] = end;
|
||||
|
||||
$scope.record = record;
|
||||
|
||||
function reset() {
|
||||
$scope.record = null;
|
||||
}
|
||||
|
||||
function doSave() {
|
||||
return ds.save(record).success(function(res){
|
||||
return $scope.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
var handler = $scope.onChangeHandler;
|
||||
if (handler) {
|
||||
var promise = handler.onChange().then(function () {
|
||||
return doSave();
|
||||
});
|
||||
promise.success = function(fn) {
|
||||
promise.then(fn);
|
||||
return promise;
|
||||
};
|
||||
promise.error = function (fn) {
|
||||
promise.then(null, fn);
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
return doSave();
|
||||
};
|
||||
|
||||
$scope.removeEvent = function(event, callback) {
|
||||
ds.remove(event.record).success(callback);
|
||||
};
|
||||
|
||||
$scope.select = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
var args = [],
|
||||
query = {};
|
||||
|
||||
return {
|
||||
mode: 'calendar',
|
||||
args: args,
|
||||
query: query
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
|
||||
if (opts.mode === "calendar") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "calendar") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('axelor.ui').directive('uiViewCalendar', ['ViewService', 'ActionService', function(ViewService, ActionService) {
|
||||
|
||||
function link(scope, element, attrs, controller) {
|
||||
|
||||
var main = element.children('.calendar-main');
|
||||
var mini = element.find('.calendar-mini');
|
||||
var legend = element.find('.calendar-legend');
|
||||
|
||||
var ctx = (scope._viewParams.context||{});
|
||||
var params = (scope._viewParams.params||{});
|
||||
|
||||
var schema = scope.schema;
|
||||
var mode = ctx.calendarMode || params.calendarMode || schema.mode || "month";
|
||||
var date = ctx.calendarDate || params.calendarDate;
|
||||
|
||||
var editable = schema.editable === undefined ? true : schema.editable;
|
||||
var calRange = {};
|
||||
|
||||
var RecordManager = (function () {
|
||||
|
||||
var records = [],
|
||||
current = [];
|
||||
|
||||
function add(record) {
|
||||
if (!record || _.isArray(record)) {
|
||||
records = record || [];
|
||||
return filter();
|
||||
}
|
||||
var found = _.findWhere(records, {
|
||||
id: record.id
|
||||
});
|
||||
if (!found) {
|
||||
found = record;
|
||||
records.push(record);
|
||||
}
|
||||
if (found !== record) {
|
||||
_.extend(found, record);
|
||||
}
|
||||
return filter();
|
||||
}
|
||||
|
||||
function remove(record) {
|
||||
records = _.filter(records, function (item) {
|
||||
return record.id !== item.id;
|
||||
});
|
||||
return filter();
|
||||
}
|
||||
|
||||
function filter() {
|
||||
|
||||
var selected = [];
|
||||
|
||||
_.each(scope.getColors(), function (color) {
|
||||
if (color.checked) {
|
||||
selected.push(scope.getColorKey(null, color.item));
|
||||
}
|
||||
});
|
||||
|
||||
main.fullCalendar('removeEventSource', current);
|
||||
current = records;
|
||||
|
||||
if (selected.length) {
|
||||
current = _.filter(records, function(record) {
|
||||
return _.contains(selected, record.$colorKey);
|
||||
});
|
||||
}
|
||||
|
||||
main.fullCalendar('addEventSource', current);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
function events(start, end, timezone, callback) {
|
||||
calRange.start = start;
|
||||
calRange.end = end;
|
||||
scope._viewPromise.then(function(){
|
||||
scope.fetchItems(start, end, function(items) {
|
||||
callback([]);
|
||||
add(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
add: add,
|
||||
remove: remove,
|
||||
filter: filter,
|
||||
events: events
|
||||
};
|
||||
}());
|
||||
|
||||
if (date) {
|
||||
date = moment(date).toDate();
|
||||
}
|
||||
|
||||
mini.datepicker({
|
||||
showOtherMonths: true,
|
||||
selectOtherMonths: true,
|
||||
onSelect: function(dateStr) {
|
||||
main.fullCalendar('gotoDate', mini.datepicker('getDate'));
|
||||
}
|
||||
});
|
||||
|
||||
if (date) {
|
||||
mini.datepicker('setDate', date);
|
||||
}
|
||||
|
||||
var lang = axelor.config["user.lang"] || 'en';
|
||||
|
||||
var options = {
|
||||
|
||||
header: false,
|
||||
|
||||
timeFormat: 'h(:mm)t',
|
||||
|
||||
axisFormat: 'h(:mm)t',
|
||||
|
||||
timezone: 'local',
|
||||
|
||||
lang: lang,
|
||||
|
||||
editable: editable,
|
||||
|
||||
selectable: editable,
|
||||
|
||||
selectHelper: editable,
|
||||
|
||||
select: function(start, end) {
|
||||
var event = {
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
|
||||
// all day
|
||||
if (!start.hasTime() && !end.hasTime()) {
|
||||
event.start = start.clone().startOf("day").local();
|
||||
event.end = end.clone().startOf("day").local();
|
||||
}
|
||||
|
||||
scope.$applyAsync(function(){
|
||||
scope.showEditor(event);
|
||||
});
|
||||
main.fullCalendar('unselect');
|
||||
},
|
||||
|
||||
defaultDate: date,
|
||||
|
||||
events: RecordManager,
|
||||
|
||||
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
|
||||
hideBubble();
|
||||
scope.onEventChange(event, delta).error(function(){
|
||||
revertFunc();
|
||||
});
|
||||
},
|
||||
|
||||
eventResize: function( event, delta, revertFunc, jsEvent, ui, view ) {
|
||||
scope.onEventChange(event, delta).error(function(){
|
||||
revertFunc();
|
||||
});
|
||||
},
|
||||
|
||||
eventClick: function(event, jsEvent, view) {
|
||||
showBubble(event, jsEvent.target);
|
||||
},
|
||||
|
||||
eventDataTransform: function(record) {
|
||||
return updateEvent(null, record);
|
||||
},
|
||||
|
||||
viewDisplay: function(view) {
|
||||
hideBubble();
|
||||
mini.datepicker('setDate', main.fullCalendar('getDate'));
|
||||
},
|
||||
|
||||
allDayText: _t('All Day')
|
||||
};
|
||||
|
||||
if (lang.indexOf('fr') === 0) {
|
||||
_.extend(options, {
|
||||
timeFormat: 'H:mm',
|
||||
axisFormat: 'H:mm',
|
||||
firstDay: 1,
|
||||
|
||||
views: {
|
||||
week: {
|
||||
titleFormat: 'D MMM YYYY',
|
||||
columnFormat: 'ddd DD/MM'
|
||||
},
|
||||
|
||||
day: {
|
||||
titleFormat: 'D MMM YYYY'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main.fullCalendar(options);
|
||||
|
||||
var editor = null;
|
||||
var bubble = null;
|
||||
|
||||
function hideBubble() {
|
||||
if (bubble) {
|
||||
bubble.popover('destroy');
|
||||
bubble = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showBubble(event, elem) {
|
||||
hideBubble();
|
||||
bubble = $(elem).popover({
|
||||
html: true,
|
||||
title: "<b>" + event.title + "</b>",
|
||||
placement: "top",
|
||||
container: 'body',
|
||||
content: function() {
|
||||
var html = $("<div></div>").addClass("calendar-bubble-content");
|
||||
var start = event.start;
|
||||
var end = event.end && event.allDay ? moment(event.end).add(-1, 'second') : event.end;
|
||||
var singleDay = (event.allDay || !scope.isAgenda()) && (!end || moment(start).isSame(end, 'day'));
|
||||
var dateFormat = !scope.isAgenda() || event.allDay ? "ddd D MMM" : "ddd D MMM HH:mm";
|
||||
|
||||
$("<span>").text(moment(start).format(dateFormat)).appendTo(html);
|
||||
|
||||
if (schema.eventStop && end && !singleDay) {
|
||||
$("<span> - </span>").appendTo(html);
|
||||
$("<span>").text(moment(end).format(dateFormat)).appendTo(html);
|
||||
}
|
||||
|
||||
$("<hr>").appendTo(html);
|
||||
|
||||
if (scope.isEditable()) {
|
||||
$('<a href="javascript: void(0)" style="margin-right: 5px;"></a>').text(_t("Delete"))
|
||||
.appendTo(html)
|
||||
.click(function(e){
|
||||
hideBubble();
|
||||
scope.$applyAsync(function(){
|
||||
scope.removeEvent(event, function() {
|
||||
RecordManager.remove(event.record);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('<a class="pull-right" href="javascript: void(0)"></a>')
|
||||
.append(_t("Edit event")).append("<strong> »</strong>")
|
||||
.appendTo(html)
|
||||
.click(function(e){
|
||||
hideBubble();
|
||||
scope.$applyAsync(function(){
|
||||
scope.showEditor(event);
|
||||
});
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
|
||||
bubble.popover('show');
|
||||
}
|
||||
|
||||
$("body").on("mousedown", function(e){
|
||||
var elem = $(e.target || e.srcElement);
|
||||
if (!bubble || bubble.is(elem) || bubble.has(elem).length) {
|
||||
return;
|
||||
}
|
||||
if (!elem.parents().is(".popover")) {
|
||||
hideBubble();
|
||||
}
|
||||
});
|
||||
|
||||
function updateEvent(event, record) {
|
||||
if (!event || !event.id) {
|
||||
var color = scope.getColor(record);
|
||||
event = {
|
||||
id: record.id,
|
||||
record: record,
|
||||
backgroundColor: color.bg,
|
||||
borderColor: color.bc,
|
||||
textColor: color.fg
|
||||
};
|
||||
} else {
|
||||
_.extend(event.record, record);
|
||||
}
|
||||
|
||||
event = _.extend(event, scope.getEventInfo(record));
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
scope.editorCanSave = true;
|
||||
|
||||
scope.showEditor = function(event) {
|
||||
|
||||
var view = this.schema;
|
||||
var record = _.extend({}, event.record);
|
||||
|
||||
record[view.eventStart] = event.start;
|
||||
record[view.eventStop] = event.end;
|
||||
|
||||
if (!editor) {
|
||||
editor = ViewService.compile('<div ui-editor-popup></div>')(scope.$new());
|
||||
editor.data('$target', element);
|
||||
}
|
||||
|
||||
var popup = editor.isolateScope();
|
||||
|
||||
popup.show(record, function(result) {
|
||||
RecordManager.add(result);
|
||||
});
|
||||
popup.waitForActions(function() {
|
||||
if (!record || !record.id) {
|
||||
popup.$broadcast("on:new");
|
||||
} else {
|
||||
popup.setEditable(scope.isEditable());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.isEditable = function() {
|
||||
return editable;
|
||||
};
|
||||
|
||||
scope.refresh = function(record) {
|
||||
if (calRange.start && calRange.end) {
|
||||
return RecordManager.events(calRange.start, calRange.end, options.timezone, function () {});
|
||||
}
|
||||
return main.fullCalendar("refetchEvents");
|
||||
};
|
||||
|
||||
scope.filterEvents = function() {
|
||||
RecordManager.filter();
|
||||
};
|
||||
|
||||
scope.pagerText = function() {
|
||||
return main.fullCalendar("getView").title;
|
||||
};
|
||||
|
||||
scope.isMode = function(name) {
|
||||
return mode === name;
|
||||
};
|
||||
|
||||
scope.onMode = function(name) {
|
||||
mode = name;
|
||||
if (name === "week") {
|
||||
name = scope.isAgenda() ? "agendaWeek" : "basicWeek";
|
||||
}
|
||||
if (name === "day") {
|
||||
name = scope.isAgenda() ? "agendaDay" : "basicDay";
|
||||
}
|
||||
main.fullCalendar("changeView", name);
|
||||
};
|
||||
|
||||
scope.onRefresh = function () {
|
||||
scope.refresh();
|
||||
};
|
||||
|
||||
scope.onNext = function() {
|
||||
main.fullCalendar('next');
|
||||
};
|
||||
|
||||
scope.onPrev = function() {
|
||||
main.fullCalendar('prev');
|
||||
};
|
||||
|
||||
scope.onToday = function() {
|
||||
main.fullCalendar('today');
|
||||
};
|
||||
|
||||
scope.onChangeHandler = null;
|
||||
if (schema.onChange) {
|
||||
scope.onChangeHandler = ActionService.handler(scope, element, {
|
||||
action: schema.onChange
|
||||
});
|
||||
}
|
||||
|
||||
function adjustSize() {
|
||||
if (main.is(':hidden')) {
|
||||
return;
|
||||
}
|
||||
hideBubble();
|
||||
main.fullCalendar('render');
|
||||
main.fullCalendar('option', 'height', element.height());
|
||||
legend.css("max-height", legend.parent().height() - mini.height()
|
||||
- (parseInt(legend.css('marginTop')) || 0)
|
||||
- (parseInt(legend.css('marginBottom')) || 0));
|
||||
}
|
||||
|
||||
scope.$onAdjust(adjustSize, 100);
|
||||
|
||||
scope.$callWhen(function () {
|
||||
return main.is(':visible');
|
||||
}, function() {
|
||||
element.parents('.view-container:first').css('overflow', 'inherit');
|
||||
scope.onMode(mode);
|
||||
adjustSize();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return {
|
||||
link: function(scope, element, attrs, controller) {
|
||||
scope._viewPromise.then(function(){
|
||||
link(scope, element, attrs, controller);
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div>'+
|
||||
'<div class="calendar-main" ui-attach-scroll=".fc-scroller"></div>'+
|
||||
'<div class="calendar-side">'+
|
||||
'<div class="calendar-mini"></div>'+
|
||||
'<form class="form calendar-legend">'+
|
||||
'<label class="checkbox" ng-repeat="color in getColors()" style="color: {{color.color.bc}}">'+
|
||||
'<input type="checkbox" ng-click="filterEvents()" ng-model="color.checked"> {{color.title}}</label>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
991
sophal/js/view/view.chart.js
Normal file
991
sophal/js/view/view.chart.js
Normal file
@ -0,0 +1,991 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function(){
|
||||
|
||||
/* global d3: true, nv: true, D3Funnel: true, RadarChart: true, GaugeChart: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.ChartCtrl = ChartCtrl;
|
||||
ui.ChartCtrl.$inject = ['$scope', '$element', '$http', 'ActionService'];
|
||||
|
||||
function ChartCtrl($scope, $element, $http, ActionService) {
|
||||
|
||||
var views = $scope._views;
|
||||
var view = $scope.view = views.chart;
|
||||
|
||||
var viewChart = null;
|
||||
var searchScope = null;
|
||||
var clickHandler = null;
|
||||
var actionHandler = null;
|
||||
|
||||
var loading = false;
|
||||
var unwatch = null;
|
||||
|
||||
function refresh() {
|
||||
|
||||
if (viewChart && searchScope && $scope.searchFields && !searchScope.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var context = $scope._context || {};
|
||||
if ($scope.getContext) {
|
||||
context = _.extend({}, $scope.getContext(), context);
|
||||
}
|
||||
|
||||
if (searchScope) {
|
||||
context = _.extend({}, context, searchScope.getContext());
|
||||
}
|
||||
|
||||
context = _.extend({}, context, { _domainAction: $scope._viewAction });
|
||||
loading = true;
|
||||
|
||||
var params = {
|
||||
data: context
|
||||
};
|
||||
|
||||
if (viewChart) {
|
||||
params.fields = ['dataset'];
|
||||
}
|
||||
|
||||
return $http.post('ws/meta/chart/' + view.name, params).then(function(response) {
|
||||
var res = response.data;
|
||||
var data = res.data;
|
||||
var isInitial = viewChart === null;
|
||||
|
||||
if (viewChart === null) {
|
||||
viewChart = data;
|
||||
if (data.config && data.config.onClick) {
|
||||
clickHandler = ActionService.handler($scope, $element, {
|
||||
action: data.config.onClick
|
||||
});
|
||||
}
|
||||
if (data.config && data.config.onAction) {
|
||||
actionHandler = ActionService.handler($scope, $element, {
|
||||
action: data.config.onAction
|
||||
});
|
||||
}
|
||||
if (data.config && data.config.onActionTitle) {
|
||||
$scope.actionTitle = data.config.onActionTitle;
|
||||
}
|
||||
} else {
|
||||
data = _.extend({}, viewChart, data);
|
||||
}
|
||||
|
||||
if ($scope.searchFields === undefined && data.search) {
|
||||
$scope.searchFields = data.search;
|
||||
$scope.searchInit = data.onInit;
|
||||
$scope.usingSQL = data.usingSQL;
|
||||
} else {
|
||||
$scope.render(data);
|
||||
if (isInitial) {
|
||||
refresh(); // force loading data
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
}, function () {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.setSearchScope = function (formScope) {
|
||||
searchScope = formScope;
|
||||
};
|
||||
|
||||
$scope.hasAction = function () {
|
||||
return !!actionHandler;
|
||||
};
|
||||
|
||||
$scope.handleAction = function (data) {
|
||||
if (actionHandler) {
|
||||
actionHandler._getContext = function () {
|
||||
return _.extend({}, { _data: data }, {
|
||||
_model: $scope._model || 'com.axelor.meta.db.MetaView',
|
||||
_chart: view.name
|
||||
});
|
||||
};
|
||||
actionHandler.handle();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleClick = function (e) {
|
||||
if (clickHandler) {
|
||||
clickHandler._getContext = function () {
|
||||
return _.extend({}, e.data.raw, {
|
||||
_model: $scope._model || 'com.axelor.meta.db.MetaView',
|
||||
_chart: view.name
|
||||
});
|
||||
};
|
||||
clickHandler.handle();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onRefresh = function(force) {
|
||||
if (unwatch || loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in case of onInit
|
||||
if ($scope.searchInit && !(searchScope||{}).record && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch = $scope.$watch(function chartRefreshWatch() {
|
||||
if ($element.is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.render = function(data) {
|
||||
|
||||
};
|
||||
|
||||
// refresh to load chart
|
||||
$scope.onRefresh();
|
||||
}
|
||||
|
||||
ChartFormCtrl.$inject = ['$scope', '$element', 'ViewService', 'DataSource'];
|
||||
function ChartFormCtrl($scope, $element, ViewService, DataSource) {
|
||||
|
||||
$scope._dataSource = DataSource.create('com.axelor.meta.db.MetaView');
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
$scope.setEditable();
|
||||
$scope.setSearchScope($scope);
|
||||
|
||||
function fixFields(fields) {
|
||||
_.each(fields, function(field){
|
||||
if (field.type == 'reference') {
|
||||
field.type = 'MANY_TO_ONE';
|
||||
field.canNew = false;
|
||||
field.canEdit = false;
|
||||
}
|
||||
|
||||
if (field.type)
|
||||
field.type = field.type.toUpperCase();
|
||||
else
|
||||
field.type = 'STRING';
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
var unwatch = $scope.$watch('searchFields', function chartSearchFieldsWatch(fields) {
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
|
||||
var meta = { fields: fixFields(fields) };
|
||||
var view = {
|
||||
type: 'form',
|
||||
items: [{
|
||||
type: 'panel',
|
||||
noframe: true,
|
||||
items: _.map(meta.fields, function (item) {
|
||||
var props = _.extend({}, item, {
|
||||
showTitle: false,
|
||||
placeholder: item.title || item.autoTitle
|
||||
});
|
||||
if (item.multiple && (item.target || item.selection)) {
|
||||
item.widget = item.target ? "TagSelect" : "MultiSelect";
|
||||
}
|
||||
return props;
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
ViewService.process(meta, view);
|
||||
|
||||
view.onLoad = $scope.searchInit;
|
||||
|
||||
$scope.fields = meta.fields;
|
||||
$scope.schema = view;
|
||||
$scope.schema.loaded = true;
|
||||
|
||||
var interval;
|
||||
|
||||
function reload() {
|
||||
$scope.$parent.onRefresh();
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
|
||||
function delayedReload() {
|
||||
clearTimeout(interval);
|
||||
interval = setTimeout(reload, 500);
|
||||
}
|
||||
|
||||
function onNewOrEdit() {
|
||||
if ($scope.$events.onLoad) {
|
||||
$scope.$events.onLoad().then(delayedReload);
|
||||
}
|
||||
}
|
||||
|
||||
var __getContext = $scope.getContext;
|
||||
$scope.getContext = function () {
|
||||
var ctx = __getContext.call(this);
|
||||
_.each(meta.fields, function (item) {
|
||||
if (item.multiple && (item.target || item.selection)) {
|
||||
var value = ctx[item.name];
|
||||
if (_.isArray(value)) value = _.pluck(value, "id");
|
||||
if (_.isString(value)) value = value.split(/\s*,\s*/g);
|
||||
ctx[item.name] = value;
|
||||
} else if (item.target && $scope.usingSQL) {
|
||||
var value = ctx[item.name];
|
||||
if (value) {
|
||||
ctx[item.name] = value.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
return ctx;
|
||||
};
|
||||
|
||||
$scope.$on('on:new', onNewOrEdit);
|
||||
$scope.$on('on:edit', onNewOrEdit);
|
||||
|
||||
$scope.$watch('record', function chartSearchRecordWatch(record) {
|
||||
if (interval === undefined) {
|
||||
interval = null;
|
||||
return;
|
||||
}
|
||||
if ($scope.isValid()) delayedReload();
|
||||
}, true);
|
||||
|
||||
$scope.$watch('$events.onLoad', function chartOnLoadWatch(handler) {
|
||||
if (handler) {
|
||||
handler().then(delayedReload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function $conv(value, type) {
|
||||
if (!value && type === 'text') return 'N/A';
|
||||
if (!value) return 0;
|
||||
if (_.isNumber(value)) return value;
|
||||
if (/^(-)?\d+(\.\d+)?$/.test(value)) {
|
||||
return +value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function applyXY(chart, data) {
|
||||
var type = data.xType;
|
||||
chart.y(function (d) { return d.y; });
|
||||
if (type == "date") {
|
||||
return chart.x(function (d) { return moment(d.x).toDate(); });
|
||||
}
|
||||
return chart.x(function (d) { return d.x; });
|
||||
}
|
||||
|
||||
var themes = {
|
||||
|
||||
// default
|
||||
d3: d3.scale.category10().range(),
|
||||
|
||||
// material
|
||||
material: [
|
||||
'#f44336', // Red
|
||||
'#E91E63', // Pink
|
||||
'#9c27b0', // Purple
|
||||
'#673ab7', // Deep Purple
|
||||
'#3f51b5', // Indigo
|
||||
'#2196F3', // Blue
|
||||
'#03a9f4', // Light Blue
|
||||
'#00bcd4', // Cyan
|
||||
'#009688', // Teal
|
||||
'#4caf50', // Green
|
||||
'#8bc34a', // Light Green
|
||||
'#cddc39', // Lime
|
||||
'#ffeb3b', // Yellow
|
||||
'#ffc107', // Amber
|
||||
'#ff9800', // Orange
|
||||
'#ff5722', // Deep Orange
|
||||
'#795548', // Brown
|
||||
'#9e9e9e', // Grey
|
||||
'#607d8b', // Blue Grey
|
||||
],
|
||||
|
||||
// chart.js
|
||||
chartjs: [
|
||||
'#ff6384', '#ff9f40', '#ffcd56', '#4bc0c0',
|
||||
'#36a2eb', '#9966ff', '#c9cbcf',
|
||||
],
|
||||
|
||||
// echart - roma
|
||||
roma: [
|
||||
'#E01F54','#001852','#f5e8c8','#b8d2c7','#c6b38e',
|
||||
'#a4d8c2','#f3d999','#d3758f','#dcc392','#2e4783',
|
||||
'#82b6e9','#ff6347','#a092f1','#0a915d','#eaf889',
|
||||
'#6699FF','#ff6666','#3cb371','#d5b158','#38b6b6',
|
||||
],
|
||||
|
||||
// echart - macarons
|
||||
macarons: [
|
||||
'#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80',
|
||||
'#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa',
|
||||
'#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050',
|
||||
'#59678c','#c9ab00','#7eb00a','#6f5553','#c14089',
|
||||
]
|
||||
};
|
||||
|
||||
function colors(names, shades, type) {
|
||||
var given = themes[names] ? themes[names] : names;
|
||||
given = given || themes.material;
|
||||
given = _.isArray(given) ? given : given.split(',');
|
||||
if (given && shades > 1) {
|
||||
var n = Math.max(0, Math.min(+(shades) || 4, 4));
|
||||
return _.flatten(given.map(function (c) {
|
||||
return _.first(_.range(0, n + 1).map(d3.scale.linear().domain([0, n + 1]).range([c, 'white'])), n);
|
||||
}));
|
||||
}
|
||||
return given;
|
||||
}
|
||||
|
||||
var CHARTS = {};
|
||||
|
||||
function PlusData(series, data) {
|
||||
var result = _.chain(data.dataset)
|
||||
.groupBy(data.xAxis)
|
||||
.map(function (group, name) {
|
||||
var value = 0;
|
||||
_.each(group, function (item) {
|
||||
value += $conv(item[series.key]);
|
||||
});
|
||||
var raw = {};
|
||||
if (group[0]) {
|
||||
raw[data.xAxis] = name;
|
||||
raw[series.key] = value;
|
||||
raw[data.xAxis + 'Id'] = group[0][data.xAxis + 'Id'];
|
||||
}
|
||||
return {
|
||||
x: name === 'null' ? 'N/A' : name,
|
||||
y: value,
|
||||
raw: raw
|
||||
};
|
||||
}).value();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function PlotData(series, data) {
|
||||
var ticks = _.chain(data.dataset).pluck(data.xAxis).unique().map(function (v) { return $conv(v, data.xType); }).value();
|
||||
var groupBy = series.groupBy;
|
||||
var datum = [];
|
||||
|
||||
_.chain(data.dataset).groupBy(groupBy)
|
||||
.map(function (group, groupName) {
|
||||
var name = groupBy ? groupName : null;
|
||||
var values = _.map(group, function (item) {
|
||||
var x = $conv(item[data.xAxis], data.xType) || 0;
|
||||
var y = $conv(item[series.key] || name || 0);
|
||||
return { x: x, y: y, raw: item };
|
||||
});
|
||||
|
||||
var my = _.pluck(values, 'x');
|
||||
var missing = _.difference(ticks, my);
|
||||
if (ticks.length === missing.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(missing, function(x) {
|
||||
values.push({ x: x, y: 0 });
|
||||
});
|
||||
|
||||
values = _.sortBy(values, 'x');
|
||||
|
||||
datum.push({
|
||||
key: name || series.title,
|
||||
type: series.type,
|
||||
values: values
|
||||
});
|
||||
});
|
||||
|
||||
return datum;
|
||||
}
|
||||
|
||||
function PieChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlusData(series, data);
|
||||
var config = data.config || {};
|
||||
|
||||
var chart = nv.models.pieChart()
|
||||
.showLabels(false)
|
||||
.height(null)
|
||||
.width(null)
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
if (series.type === "donut") {
|
||||
chart.donut(true)
|
||||
.donutRatio(0.40);
|
||||
}
|
||||
|
||||
if (_.toBoolean(config.percent)) {
|
||||
chart.showLabels(true)
|
||||
.labelType("percent")
|
||||
.labelThreshold(0.05);
|
||||
}
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(1200).call(chart);
|
||||
|
||||
chart.pie.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.pie = PieChart;
|
||||
CHARTS.donut = PieChart;
|
||||
|
||||
function DBarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlusData(series, data);
|
||||
|
||||
datum = [{
|
||||
key: data.title,
|
||||
values: datum
|
||||
}];
|
||||
|
||||
var chart = nv.models.discreteBarChart()
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; })
|
||||
.staggerLabels(true)
|
||||
.showValues(true);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.discretebar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function BarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.multiBarChart()
|
||||
.reduceXTicks(false);
|
||||
|
||||
chart.multibar.hideable(true);
|
||||
chart.stacked(data.stacked);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function HBarChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.multiBarHorizontalChart();
|
||||
|
||||
chart.stacked(data.stacked);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function (e) {
|
||||
scope.handleClick(e);
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function FunnelChart(scope, element, data) {
|
||||
|
||||
if(!data.dataset){
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = new D3Funnel(element[0]);
|
||||
var w = element.width();
|
||||
var h = element.height();
|
||||
var config = _.extend({}, data.config);
|
||||
var props = {
|
||||
fillType: 'gradient',
|
||||
hoverEffects: true,
|
||||
dynamicArea: true,
|
||||
animation: 200};
|
||||
|
||||
if(config.width){
|
||||
props.width = w*config.width/100;
|
||||
}
|
||||
if(config.height){
|
||||
props.height = h*config.height/100;
|
||||
}
|
||||
|
||||
var series = _.first(data.series) || {};
|
||||
var opts = [];
|
||||
_.each(data.dataset, function(dat){
|
||||
opts.push([dat[data.xAxis],($conv(dat[series.key])||0)]);
|
||||
});
|
||||
chart.draw(opts, props);
|
||||
|
||||
chart.update = function(){};
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.bar = BarChart;
|
||||
CHARTS.dbar = DBarChart;
|
||||
CHARTS.hbar = HBarChart;
|
||||
CHARTS.funnel = FunnelChart;
|
||||
|
||||
function LineChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.lineChart()
|
||||
.showLegend(true)
|
||||
.showYAxis(true)
|
||||
.showXAxis(true);
|
||||
|
||||
applyXY(chart, data);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
function AreaChart(scope, element, data) {
|
||||
|
||||
var series = _.first(data.series);
|
||||
var datum = PlotData(series, data);
|
||||
|
||||
var chart = nv.models.stackedAreaChart();
|
||||
|
||||
applyXY(chart, data);
|
||||
|
||||
d3.select(element[0])
|
||||
.datum(datum)
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
CHARTS.line = LineChart;
|
||||
CHARTS.area = AreaChart;
|
||||
|
||||
function RadarCharter(scope, element, data) {
|
||||
|
||||
var result = _.map(data.dataset, function(item) {
|
||||
return _.map(data.series, function(s) {
|
||||
var title = s.title || s.key,
|
||||
value = item[s.key];
|
||||
return {
|
||||
axis: title,
|
||||
value: $conv(value) || 0
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
var id = _.uniqueId('_radarChart'),
|
||||
parent = element.parent();
|
||||
|
||||
parent.attr('id', id)
|
||||
.addClass('radar-chart')
|
||||
.empty();
|
||||
|
||||
var size = Math.min(parent.innerWidth(), parent.innerHeight());
|
||||
|
||||
RadarChart.draw('#'+id, result, {
|
||||
w: size,
|
||||
h: size
|
||||
});
|
||||
|
||||
parent.children('svg')
|
||||
.css('width', 'auto')
|
||||
.css('margin', 'auto')
|
||||
.css('margin-top', 10);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function GaugeCharter(scope, element, data) {
|
||||
|
||||
var config = data.config,
|
||||
min = +(config.min) || 0,
|
||||
max = +(config.max) || 100,
|
||||
value = 0;
|
||||
|
||||
var item = _.first(data.dataset),
|
||||
series = _.first(data.series),
|
||||
key = series.key || data.xAxis;
|
||||
|
||||
if (item) {
|
||||
value = item[key] || value;
|
||||
}
|
||||
|
||||
var w = element.width();
|
||||
var h = element.height();
|
||||
|
||||
var parent = element.hide().parent();
|
||||
|
||||
parent.children('svg').remove();
|
||||
parent.append(element);
|
||||
|
||||
var chart = GaugeChart(parent[0], {
|
||||
size: 300,
|
||||
clipWidth: 300,
|
||||
clipHeight: h,
|
||||
ringWidth: 60,
|
||||
minValue: min,
|
||||
maxValue: max,
|
||||
transitionMs: 4000
|
||||
});
|
||||
|
||||
chart.render();
|
||||
chart.update(value);
|
||||
|
||||
parent.children('svg:last')
|
||||
.css('display', 'block')
|
||||
.css('width', 'auto')
|
||||
.css('margin', 'auto')
|
||||
.css('margin-top', 0);
|
||||
}
|
||||
|
||||
function TextChart(scope, element, data) {
|
||||
|
||||
var config = _.extend({
|
||||
strong: true,
|
||||
shadow: false,
|
||||
fontSize: 22
|
||||
}, data.config);
|
||||
|
||||
var values = _.first(data.dataset) || {};
|
||||
var series = _.first(data.series) || {};
|
||||
|
||||
var value = values[series.key];
|
||||
|
||||
if (config.format) {
|
||||
value = _t(config.format, value);
|
||||
}
|
||||
|
||||
var svg = d3.select(element.empty()[0]);
|
||||
var text = svg.append("svg:text")
|
||||
.attr("x", "50%")
|
||||
.attr("y", "50%")
|
||||
.attr("dy", ".3em")
|
||||
.attr("text-anchor", "middle")
|
||||
.text(value);
|
||||
|
||||
if (config.color) text.attr("fill", config.color);
|
||||
if (config.fontSize) text.style("font-size", config.fontSize);
|
||||
if (_.toBoolean(config.strong)) text.style("font-weight", "bold");
|
||||
if (_.toBoolean(config.shadow)) text.style("text-shadow", "0 1px 2px rgba(0, 0, 0, .5)");
|
||||
}
|
||||
|
||||
CHARTS.text = TextChart;
|
||||
CHARTS.radar = RadarCharter;
|
||||
CHARTS.gauge = GaugeCharter;
|
||||
|
||||
function Chart(scope, element, data) {
|
||||
|
||||
var type = null;
|
||||
var config = data.config || {};
|
||||
|
||||
for(var i = 0 ; i < data.series.length ; i++) {
|
||||
type = data.series[i].type;
|
||||
if (type === "bar" && !data.series[i].groupBy) type = "dbar";
|
||||
if (type === "pie" || type === "dbar" || type === "radar" || type === "gauge") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "pie" && data.series.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== "radar" && data.series.length > 1) {
|
||||
type = "multi";
|
||||
}
|
||||
|
||||
// clean up last instance
|
||||
(function () {
|
||||
var chart = element.off('adjustSize').empty().data('chart');
|
||||
if (chart && chart.tooltip && chart.tooltip.id) {
|
||||
d3.select('#' + chart.tooltip.id()).remove();
|
||||
}
|
||||
})();
|
||||
|
||||
nv.addGraph(function generate() {
|
||||
|
||||
var noData = _t('No records found.');
|
||||
if (data.dataset && data.dataset.stacktrace) {
|
||||
noData = data.dataset.message;
|
||||
data.dataset = [];
|
||||
}
|
||||
|
||||
var maker = CHARTS[type] || CHARTS.bar || function () {};
|
||||
var chart = maker(scope, element, data);
|
||||
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// series scale attribute
|
||||
var series = _.first(data.series);
|
||||
var scale = series && series.scale;
|
||||
|
||||
// format as integer if no scale is specified
|
||||
// and data has integer series values
|
||||
if (!isInteger(scale) && hasIntegerValues(data)) {
|
||||
scale = 0;
|
||||
}
|
||||
|
||||
if (isInteger(scale)) {
|
||||
var format = '.' + scale + 'f';
|
||||
chart.yAxis && chart.yAxis.tickFormat(d3.format(format));
|
||||
chart.valueFormat && chart.valueFormat(d3.format(format));
|
||||
}
|
||||
|
||||
if (chart.color) {
|
||||
chart.color(colors(config.colors, config.shades, type));
|
||||
}
|
||||
|
||||
if (chart.noData) {
|
||||
chart.noData(noData);
|
||||
}
|
||||
if(chart.controlLabels) {
|
||||
chart.controlLabels({
|
||||
grouped: _t('Grouped'),
|
||||
stacked: _t('Stacked'),
|
||||
stream: _t('Stream'),
|
||||
expanded: _t('Expanded'),
|
||||
stack_percent: _t('Stack %')
|
||||
});
|
||||
}
|
||||
|
||||
var tickFormats = {
|
||||
"date" : function (d) {
|
||||
var f = config.xFormat;
|
||||
return moment(d).format(f || 'YYYY-MM-DD');
|
||||
},
|
||||
"month" : function(d) {
|
||||
var v = "" + d;
|
||||
var f = config.xFormat;
|
||||
if (v.indexOf(".") > -1) return "";
|
||||
if (_.isString(d) && /^(\d+)$/.test(d)) {
|
||||
d = parseInt(d);
|
||||
}
|
||||
if (_.isNumber(d)) {
|
||||
return moment([moment().year(), d - 1, 1]).format(f || "MMM");
|
||||
}
|
||||
if (_.isString(d) && d.indexOf('-') > 0) {
|
||||
return moment(d).format(f || 'MMM, YYYY');
|
||||
}
|
||||
return d;
|
||||
},
|
||||
"year" : function(d) {
|
||||
return moment([moment().year(), d - 1, 1]).format("YYYY");
|
||||
},
|
||||
"number": d3.format(',f'),
|
||||
"decimal": d3.format(',.1f'),
|
||||
"text": function(d) { return d; }
|
||||
};
|
||||
|
||||
var tickFormat = tickFormats[data.xType];
|
||||
if (chart.xAxis && tickFormat) {
|
||||
chart.xAxis
|
||||
.rotateLabels(-45)
|
||||
.tickFormat(tickFormat);
|
||||
}
|
||||
|
||||
if (chart.yAxis && data.yTitle) {
|
||||
chart.yAxis.axisLabel(data.yTitle);
|
||||
}
|
||||
|
||||
var margin = data.xType === 'date' ? { 'bottom': 65 } : null;
|
||||
['top', 'left', 'bottom', 'right'].forEach(function (side) {
|
||||
var key = 'margin-' + side;
|
||||
var val = parseInt(config[key]);
|
||||
if (val) {
|
||||
(margin||(margin={}))[side] = val;
|
||||
}
|
||||
});
|
||||
if (chart.margin && margin) {
|
||||
chart.margin(margin);
|
||||
}
|
||||
|
||||
var lastWidth = 0;
|
||||
var lastHeight = 0;
|
||||
|
||||
function adjust() {
|
||||
|
||||
if (!element[0] || element.parent().is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = element[0].getBoundingClientRect();
|
||||
var w = rect.width,
|
||||
h = rect.height;
|
||||
|
||||
if (w === lastWidth && h === lastHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastWidth = w;
|
||||
lastHeight = h;
|
||||
|
||||
chart.update();
|
||||
}
|
||||
|
||||
element.data('chart', chart);
|
||||
scope.$onAdjust(adjust, 100);
|
||||
setTimeout(chart.update, 10);
|
||||
|
||||
return chart;
|
||||
});
|
||||
}
|
||||
|
||||
function hasIntegerValues(data) {
|
||||
var series = _.first(data.series);
|
||||
var dataset = _.first(data.dataset);
|
||||
return series && dataset && isInteger(dataset[series.key]);
|
||||
}
|
||||
|
||||
function isInteger(n) {
|
||||
return (n ^ 0) === n;
|
||||
}
|
||||
|
||||
var directiveFn = function(){
|
||||
return {
|
||||
controller: ChartCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var svg = element.children('svg');
|
||||
var form = element.children('.chart-controls');
|
||||
|
||||
function doExport(data) {
|
||||
var dataset = data.dataset || [];
|
||||
var header = [];
|
||||
_.each(dataset, function (item) {
|
||||
header = _.unique(_.flatten([header, _.keys(item)]));
|
||||
|
||||
});
|
||||
|
||||
var content = "data:text/csv;charset=utf-8," + header.join(';') + '\n';
|
||||
|
||||
dataset.forEach(function (item) {
|
||||
var row = header.map(function (key) {
|
||||
var val = item[key];
|
||||
if (val === undefined || val === null) {
|
||||
val = '';
|
||||
}
|
||||
return '"' + (''+val).replace(/"/g, '""') + '"';
|
||||
});
|
||||
content += row.join(';') + '\n';
|
||||
});
|
||||
|
||||
var name = (data.title || 'export').toLowerCase();
|
||||
ui.download(encodeURI(content), _.underscored(name) + '.csv');
|
||||
}
|
||||
|
||||
scope.render = function(data) {
|
||||
if (element.is(":hidden")) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
svg.height(element.height() - form.height()).width('100%');
|
||||
if (!scope.dashlet || !scope.dashlet.title) {
|
||||
scope.title = data.title;
|
||||
}
|
||||
Chart(scope, svg, data);
|
||||
var canExport = data && _.isArray(data.dataset);
|
||||
scope.canExport = function () {
|
||||
return canExport;
|
||||
};
|
||||
scope.onExport = function () {
|
||||
doExport(data);
|
||||
};
|
||||
scope.onAction = function () {
|
||||
scope.handleAction(data && data.dataset);
|
||||
};
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
function onNewOrEdit() {
|
||||
if (scope.searchInit && scope.searchFields) {
|
||||
return;
|
||||
}
|
||||
scope.onRefresh(true);
|
||||
}
|
||||
|
||||
scope.$on('on:new', onNewOrEdit);
|
||||
scope.$on('on:edit', onNewOrEdit);
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="chart-container" style="background-color: white; ">'+
|
||||
'<div ui-chart-form></div>'+
|
||||
'<svg></svg>'+
|
||||
'</div>'
|
||||
};
|
||||
};
|
||||
|
||||
ui.directive('uiChartForm', function () {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
controller: ChartFormCtrl,
|
||||
link: function (scope, element, attrs, ctrls) {
|
||||
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='chart-controls'>" +
|
||||
"<div ui-view-form x-handler='this'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
ui.directive('uiViewChart', directiveFn);
|
||||
ui.directive('uiPortletChart', directiveFn);
|
||||
|
||||
})();
|
||||
258
sophal/js/view/view.custom.js
Normal file
258
sophal/js/view/view.custom.js
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module("axelor.ui");
|
||||
|
||||
CustomViewCtrl.$inject = ['$scope', '$http', 'DataSource', 'ViewService'];
|
||||
function CustomViewCtrl($scope, $http, DataSource, ViewService) {
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
|
||||
var view = $scope._views.custom || {};
|
||||
var viewPromise = null;
|
||||
|
||||
$scope.show = function() {
|
||||
if (!viewPromise) {
|
||||
viewPromise = $scope.loadView('custom', view.name);
|
||||
viewPromise.then(function(meta) {
|
||||
var schema = meta.view;
|
||||
$scope.schema = schema;
|
||||
$scope.schema.loaded = true;
|
||||
});
|
||||
}
|
||||
$scope.onShow(viewPromise);
|
||||
};
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
// it will be refreshed by dashlet
|
||||
if ($scope.dashlet) {
|
||||
return;
|
||||
}
|
||||
viewPromise.then(function () {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'custom'
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
if (opts.mode === "custom") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "custom") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getContext = function () {
|
||||
var context = $scope._context || {};
|
||||
if ($scope.$parent.getContext) {
|
||||
context = _.extend({}, $scope.$parent.getContext(), context);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
$scope.onRefresh = function() {
|
||||
var context = $scope.getContext();
|
||||
var params = {
|
||||
data: context
|
||||
};
|
||||
return $http.post('ws/meta/custom/' + view.name, params).then(function(response) {
|
||||
var res = response.data;
|
||||
$scope.data = (res.data||{}).dataset;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var customDirective = ["$compile", function ($compile) {
|
||||
return {
|
||||
controller: CustomViewCtrl,
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
|
||||
var evalScope = axelor.$evalScope(scope);
|
||||
|
||||
function render(template) {
|
||||
var elem = $('<span>' + axelor.sanitize(template.trim()) + '</span>');
|
||||
if (elem.children().length === 1) {
|
||||
elem = elem.children().first();
|
||||
}
|
||||
if (scope.schema && scope.schema.css) {
|
||||
element.parents(".dashlet:first").addClass(scope.schema.css);
|
||||
}
|
||||
|
||||
elem = $compile(elem)(evalScope);
|
||||
element.append(elem);
|
||||
}
|
||||
|
||||
var unwatch = scope.$watch('schema.template', function customTemplateWatch(template) {
|
||||
if (template) {
|
||||
unwatch();
|
||||
render(template);
|
||||
}
|
||||
});
|
||||
|
||||
scope.showToggle = false;
|
||||
|
||||
scope.$watch('data', function customDataWatch(data) {
|
||||
evalScope.data = data;
|
||||
evalScope.first = _.first(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
ui.directive('uiCustomView', customDirective);
|
||||
ui.directive('uiPortletCustom', customDirective);
|
||||
|
||||
// helper directives
|
||||
ui.directive('reportBox', function() {
|
||||
return {
|
||||
scope: {
|
||||
value: '=',
|
||||
label: '@',
|
||||
percent: '=',
|
||||
up: '=',
|
||||
tag: '=',
|
||||
tagCss: '='
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
setTimeout(function () {
|
||||
element.parents('.dashlet:first')
|
||||
.addClass("report-box");
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='report-box'>" +
|
||||
"<h1>{{value}}</h1>" +
|
||||
"<small>{{label}}</small>" +
|
||||
"<div class='font-bold text-info pull-right' ng-show='percent'>" +
|
||||
"<span>{{percent}}</span> <i class='fa fa-level-up'></i>" +
|
||||
"</div>" +
|
||||
"<div class='report-tags' ng-if='tag'><span class='label' ng-class='tagCss'>{{tag}}</span></div>" +
|
||||
"</div>"
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('reportTable', function() {
|
||||
return {
|
||||
scope: {
|
||||
data: '=',
|
||||
columns: '@',
|
||||
sums: '@'
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var cols = [];
|
||||
var sums = (scope.sums||'').split(',');
|
||||
var fields = {};
|
||||
var schema = scope.$parent.$parent.schema;
|
||||
|
||||
function makeColumns(names) {
|
||||
cols = [];
|
||||
fields = {};
|
||||
_.each(names, function (name) {
|
||||
var field = _.findWhere(schema.items, { name: name }) || {};
|
||||
var col = _.extend({}, field, field.widgetAttrs, {
|
||||
name: name,
|
||||
title: _.humanize(name)
|
||||
});
|
||||
fields[name] = col;
|
||||
cols.push(col);
|
||||
});
|
||||
scope.cols = cols;
|
||||
}
|
||||
|
||||
if (scope.columns) {
|
||||
makeColumns((scope.columns||'').split(','));
|
||||
} else {
|
||||
var unwatch = scope.$watch('data', function reportDataWatch(data) {
|
||||
if (data) {
|
||||
unwatch();
|
||||
var first = _.first(data) || {};
|
||||
var names = _.keys(first).filter(function (name) { return name !== '$$hashKey'; });
|
||||
makeColumns(names.sort());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.sums = sums;
|
||||
|
||||
scope.format = function(value, name) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
var field = fields[name];
|
||||
if (field && field.scale) {
|
||||
var val = +(value);
|
||||
if (_.isNumber(val)) {
|
||||
return val.toFixed(field.scale);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
scope.sum = function (name) {
|
||||
if (sums.indexOf(name) === -1) {
|
||||
return "";
|
||||
}
|
||||
var res = 0.0;
|
||||
_.each(scope.data, function (row) {
|
||||
var val = +(row[name]) || 0;
|
||||
res += val;
|
||||
});
|
||||
return scope.format(res, name);
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
element.parents('.dashlet:first')
|
||||
.addClass("report-table");
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<table class='table table-striped'>" +
|
||||
"<thead>" +
|
||||
"<tr>" +
|
||||
"<th ng-repeat='col in cols'>{{col.title}}</th>" +
|
||||
"</tr>" +
|
||||
"</thead>" +
|
||||
"<tbody>" +
|
||||
"<tr ng-repeat='row in data'>" +
|
||||
"<td ng-repeat='col in cols'>{{format(row[col.name], col.name)}}</td>" +
|
||||
"</tr>" +
|
||||
"</tbody>" +
|
||||
"<tfoot ng-if='sums.length'>" +
|
||||
"<tr>" +
|
||||
"<td ng-repeat='col in cols'>{{sum(col.name)}}</td>" +
|
||||
"</tr>" +
|
||||
"</tfoot>" +
|
||||
"</table>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
335
sophal/js/view/view.dashboard.js
Normal file
335
sophal/js/view/view.dashboard.js
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
DashboardCtrl.$inject = ['$scope', '$element'];
|
||||
function DashboardCtrl($scope, $element) {
|
||||
|
||||
var view = $scope._views.dashboard;
|
||||
if (view.items) {
|
||||
$scope.$timeout(function () {
|
||||
$scope.parse(view);
|
||||
});
|
||||
} else {
|
||||
$scope.loadView('dashboard', view.name).success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function(){
|
||||
if (view.deferred)
|
||||
view.deferred.resolve($scope);
|
||||
});
|
||||
|
||||
$scope.show = function(promise) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'dashboard',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
var items = angular.copy(schema.items || []);
|
||||
var row = [];
|
||||
|
||||
items.forEach(function (item, i) {
|
||||
var span = item.colSpan || 6;
|
||||
|
||||
item.$index = i;
|
||||
item.spanCss = {};
|
||||
item.spanCss['dashlet-cs' + span] = true;
|
||||
|
||||
row.push(item);
|
||||
});
|
||||
|
||||
$scope.schema = schema;
|
||||
$scope.row = row;
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewDashboard', ['ViewService', function(ViewService) {
|
||||
|
||||
return {
|
||||
controller: DashboardCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
scope.sortableOptions = {
|
||||
handle: ".dashlet-header",
|
||||
cancel: ".dashlet-buttons",
|
||||
items: ".dashlet",
|
||||
tolerance: "pointer",
|
||||
activate: function(e, ui) {
|
||||
var height = ui.helper.height();
|
||||
ui.placeholder.height(height);
|
||||
},
|
||||
deactivate: function(event, ui) {
|
||||
axelor.$adjustSize();
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
var schema = scope.schema;
|
||||
var items = _.map(scope.row, function (item) {
|
||||
return schema.items[item.$index];
|
||||
});
|
||||
|
||||
if (angular.equals(schema.items, items)) {
|
||||
return;
|
||||
}
|
||||
|
||||
schema.items = items;
|
||||
return ViewService.save(schema);
|
||||
}
|
||||
};
|
||||
|
||||
var unwatch = scope.$watch("schema", function dashboardSchemaWatch(schema) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
unwatch();
|
||||
if (schema.css) {
|
||||
element.addClass(schema.css);
|
||||
}
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template:
|
||||
"<div ui-sortable='sortableOptions' ng-model='row'>" +
|
||||
"<div class='dashlet' ng-class='dashlet.spanCss' ng-repeat='dashlet in row' ui-view-dashlet></div>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
DashletCtrl.$inject = ['$scope', '$element', 'MenuService', 'DataSource', 'ViewService'];
|
||||
function DashletCtrl($scope, $element, MenuService, DataSource, ViewService) {
|
||||
|
||||
var self = this;
|
||||
var init = _.once(function init() {
|
||||
|
||||
ui.ViewCtrl.call(self, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.show = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.$on('on:attrs-change:refresh', function(e) {
|
||||
e.preventDefault();
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('on:tab-reload', function(e) {
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.initDashlet = function(dashlet, options) {
|
||||
|
||||
var action = dashlet.action;
|
||||
if (!action) {
|
||||
return init();
|
||||
}
|
||||
|
||||
MenuService.action(action, options).success(function(result){
|
||||
if (_.isEmpty(result.data)) {
|
||||
return;
|
||||
}
|
||||
var view = result.data[0].view;
|
||||
|
||||
$scope._viewParams = view;
|
||||
$scope._viewAction = action;
|
||||
|
||||
init();
|
||||
|
||||
$scope.title = dashlet.title || view.title;
|
||||
if ($scope.attr) {
|
||||
$scope.title = $scope.attr('title') || $scope.title;
|
||||
}
|
||||
$scope.parseDashlet(dashlet, view);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewDashlet', ['$compile', function($compile){
|
||||
return {
|
||||
scope: true,
|
||||
controller: DashletCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var lazy = true;
|
||||
(function () {
|
||||
var counter = 0;
|
||||
return function checkLoading() {
|
||||
if (counter < 10 && element.parent().is(":hidden")) {
|
||||
counter++;
|
||||
return setTimeout(checkLoading, 100);
|
||||
}
|
||||
|
||||
lazy = !element.parent().is(".dashlet-row");
|
||||
|
||||
scope.waitForActions(function () {
|
||||
var unwatch = scope.$watch(function dashletInitWatch() {
|
||||
var dashlet = scope.dashlet;
|
||||
if (!dashlet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.parent().is(":hidden")) {
|
||||
lazy = true;
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
|
||||
var ctx;
|
||||
if (scope.getContext) {
|
||||
ctx = scope.getContext();
|
||||
}
|
||||
scope.initDashlet(dashlet, {
|
||||
context: ctx
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
})()();
|
||||
|
||||
scope.parseDashlet = _.once(function(dashlet, view) {
|
||||
var body = element.find('.dashlet-body:first');
|
||||
var header = element.find('.dashlet-header:first');
|
||||
var template = $('<div ui-portlet-' + view.viewType + '></div>');
|
||||
|
||||
scope.noFilter = !dashlet.canSearch;
|
||||
|
||||
template = $compile(template)(scope);
|
||||
body.append(template);
|
||||
|
||||
if (dashlet.height) {
|
||||
setTimeout(function() {
|
||||
body.css("height", Math.max(0, dashlet.height - header.outerHeight()));
|
||||
});
|
||||
}
|
||||
if (dashlet.css) {
|
||||
element.addClass(dashlet.css);
|
||||
}
|
||||
if (view && view.viewType) {
|
||||
element.addClass(view.viewType);
|
||||
}
|
||||
|
||||
element.removeClass('hidden');
|
||||
|
||||
scope.show();
|
||||
|
||||
// if lazy, load data
|
||||
if (scope.onRefresh && lazy) {
|
||||
scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
scope.showPager = false;
|
||||
scope.showRefresh = true;
|
||||
scope.showToggle = true;
|
||||
|
||||
scope.collapsed = false;
|
||||
scope.collapsedIcon = "fa-chevron-up";
|
||||
scope.onDashletToggle = function(event) {
|
||||
var body = element.children('.dashlet-body');
|
||||
var action = scope.collapsed ? "show" : "hide";
|
||||
scope.collapsed = !scope.collapsed;
|
||||
scope.collapsedIcon = scope.collapsed ? "fa-chevron-down" : "fa-chevron-up";
|
||||
element.removeClass("collapsed");
|
||||
body[action]("blind", 200, function () {
|
||||
element.toggleClass("collapsed", !!scope.collapsed);
|
||||
if (body.css('display') !== 'none' && action === 'hide') {
|
||||
body.hide();
|
||||
}
|
||||
axelor.$adjustSize();
|
||||
});
|
||||
};
|
||||
|
||||
scope.doNext = function() {
|
||||
if (this.canNext()) this.onNext();
|
||||
};
|
||||
|
||||
scope.doPrev = function() {
|
||||
if (this.canPrev()) this.onPrev();
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
"<div class='dashlet hidden'>" +
|
||||
"<div class='dashlet-header'>" +
|
||||
"<ul class='dashlet-buttons pull-right' ng-if='showRefresh || canExport() || hasAction()'>" +
|
||||
"<li class='dropdown'>" +
|
||||
"<a href='' class='dropdown-toggle' data-toggle='dropdown'><i class='fa fa-gear'></i></a>" +
|
||||
"<ul class='dropdown-menu pull-right'>" +
|
||||
"<li ng-if='showRefresh'>" +
|
||||
"<a href='' ng-click='onRefresh()' x-translate>Refresh</a>" +
|
||||
"</li>" +
|
||||
"<li ng-if='canExport()'>" +
|
||||
"<a href='' ng-click='onExport()' x-translate>Export</a>" +
|
||||
"</li>" +
|
||||
"<li ng-if='hasAction()' class='divider'></li>" +
|
||||
"<li ng-if='hasAction()'>" +
|
||||
"<a href='' ng-click='onAction()'>{{ (actionTitle || _t('Action')) }}</a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"<li ng-if='showToggle'><a href='' ng-click='onDashletToggle()'><i class='fa' ng-class='collapsedIcon'></i></a></li>" +
|
||||
"</ul>" +
|
||||
"<div class='dashlet-pager pull-right' ng-if='showPager'>" +
|
||||
"<span class='dashlet-pager-text'>{{pagerText()}}</span>" +
|
||||
"<a href='' ng-click='doPrev()' ng-class='{disabled: !canPrev()}'><i class='fa fa-step-backward'></i></a>" +
|
||||
"<a href='' ng-click='doNext()' ng-class='{disabled: !canNext()}'><i class='fa fa-step-forward'></i></a>" +
|
||||
"</div>" +
|
||||
"<div class='dashlet-title'>{{title}}</div>" +
|
||||
"</div>" +
|
||||
"<div class='dashlet-body'></div>" +
|
||||
"</div>"
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1812
sophal/js/view/view.dms.js
Normal file
1812
sophal/js/view/view.dms.js
Normal file
File diff suppressed because it is too large
Load Diff
1507
sophal/js/view/view.form.js
Normal file
1507
sophal/js/view/view.form.js
Normal file
File diff suppressed because it is too large
Load Diff
835
sophal/js/view/view.gantt.js
Normal file
835
sophal/js/view/view.gantt.js
Normal file
@ -0,0 +1,835 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// localization
|
||||
|
||||
(function () {
|
||||
|
||||
/* global gantt: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
$(function () {
|
||||
$('<script>')
|
||||
.attr('type', 'text/javascript')
|
||||
.attr('src', 'https://export.dhtmlx.com/gantt/api.js').appendTo('head');
|
||||
});
|
||||
|
||||
var regional = {
|
||||
month_full: [
|
||||
_t('January'),
|
||||
_t('February'),
|
||||
_t('March'),
|
||||
_t('April'),
|
||||
_t('May'),
|
||||
_t('June'),
|
||||
_t('July'),
|
||||
_t('August'),
|
||||
_t('September'),
|
||||
_t('October'),
|
||||
_t('November'),
|
||||
_t('December')],
|
||||
month_short: [
|
||||
_t('Jan'),
|
||||
_t('Feb'),
|
||||
_t('Mar'),
|
||||
_t('Apr'),
|
||||
_t('May'),
|
||||
_t('Jun'),
|
||||
_t('Jul'),
|
||||
_t('Aug'),
|
||||
_t('Sep'),
|
||||
_t('Oct'),
|
||||
_t('Nov'),
|
||||
_t('Dec')],
|
||||
day_full: [
|
||||
_t('Sunday'),
|
||||
_t('Monday'),
|
||||
_t('Tuesday'),
|
||||
_t('Wednesday'),
|
||||
_t('Thursday'),
|
||||
_t('Friday'),
|
||||
_t('Saturday')],
|
||||
day_short : [_t('Sun'), _t('Mon'), _t('Tue'), _t('Wed'), _t('Thu'), _t('Fri'), _t('Sat')]
|
||||
};
|
||||
|
||||
gantt.locale = {
|
||||
date: regional,
|
||||
labels:{
|
||||
new_task: _t("New task"),
|
||||
icon_save: _t("Save"),
|
||||
icon_cancel: _t("Cancel"),
|
||||
icon_details: _t("Details"),
|
||||
icon_edit: _t("Edit"),
|
||||
icon_delete: _t("Delete"),
|
||||
confirm_closing:"",// Your changes will be lost, are your sure ?
|
||||
confirm_deleting: _t("Task will be deleted permanently, are you sure?"),
|
||||
section_description: _t("Description"),
|
||||
section_time: _t("Time period"),
|
||||
section_type: _t("Type"),
|
||||
|
||||
/* grid columns */
|
||||
|
||||
column_text : _t("Task name"),
|
||||
column_start_date : _t("Start time"),
|
||||
column_duration : _t("Duration"),
|
||||
column_add : "",
|
||||
|
||||
/* link confirmation */
|
||||
link: _t("Link"),
|
||||
confirm_link_deleting: _t("will be deleted"),
|
||||
link_start: " " + _t("(start)"),
|
||||
link_end: " " + _t("(end)"),
|
||||
|
||||
type_task: _t("Task"),
|
||||
type_project: _t("Project"),
|
||||
type_milestone: _t("Milestone"),
|
||||
|
||||
minutes: _t("Minutes"),
|
||||
hours: _t("Hours"),
|
||||
days: _t("Days"),
|
||||
weeks: _t("Week"),
|
||||
months: _t("Months"),
|
||||
years: _t("Years")
|
||||
}
|
||||
};
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
ui.controller('GanttViewCtrl', GanttViewCtrl);
|
||||
|
||||
GanttViewCtrl.$inject = ['$scope', '$element'];
|
||||
|
||||
function GanttViewCtrl($scope, $element) {
|
||||
|
||||
ui.DSViewCtrl('gantt', $scope, $element);
|
||||
var ds = $scope._dataSource;
|
||||
var view = $scope._views.gantt;
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.refresh();
|
||||
}
|
||||
|
||||
viewPromise.then(function(){
|
||||
var schema = $scope.schema;
|
||||
initialized = true;
|
||||
$scope._viewResolver.resolve(schema, $element);
|
||||
$scope.updateRoute();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.select = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.fetchItems = function(callback) {
|
||||
|
||||
var schema = $scope.schema;
|
||||
|
||||
var searchFields = _.pluck(this.fields, "name");
|
||||
searchFields.push(schema.taskStart);
|
||||
|
||||
var optionalFields = [schema.taskProgress,
|
||||
schema.taskEnd,
|
||||
schema.taskDuration,
|
||||
schema.taskParent,
|
||||
schema.taskSequence,
|
||||
schema.taskProgress,
|
||||
schema.finishToStart,
|
||||
schema.startToStart,
|
||||
schema.finishToFinish,
|
||||
schema.startToFinish,
|
||||
schema.taskUser
|
||||
];
|
||||
|
||||
_.each(optionalFields,function(optField){
|
||||
if(optField){
|
||||
searchFields.push(optField);
|
||||
}
|
||||
});
|
||||
|
||||
var opts = {
|
||||
fields: searchFields,
|
||||
filter: false,
|
||||
domain: this._domain,
|
||||
store: false
|
||||
};
|
||||
|
||||
ds.search(opts).success(function(records) {
|
||||
callback(records);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'gantt',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {};
|
||||
if (opts.mode === "gantt") {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
var params = $scope._viewParams;
|
||||
if (params.viewType !== "calendar") {
|
||||
return $scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.doSave = function(task, callback){
|
||||
var record = _.clone(task.record);
|
||||
return ds.save(record).success(function(res){
|
||||
callback(task, res);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doRemove = function(id, task){
|
||||
var record = _.clone(task.record);
|
||||
return ds.remove(record).success(function(res){
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
ui.directive('uiViewGantt', ['ViewService', 'ActionService', function(ViewService, ActionService) {
|
||||
|
||||
function link(scope, element, attrs, controller) {
|
||||
var main = element.children(".gantt-main");
|
||||
var schema = scope.schema;
|
||||
var fields = scope.fields;
|
||||
var fieldNames = _.pluck(schema.items, "name");
|
||||
var firstField = fields[fieldNames[0]];
|
||||
var mode = schema.mode || "week";
|
||||
var editor = null;
|
||||
ganttInit();
|
||||
|
||||
function byId(list, id) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].key == id)
|
||||
return list[i].label || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setScaleConfig(value){
|
||||
|
||||
switch (value) {
|
||||
case "day":
|
||||
gantt.config.scale_unit = "day";
|
||||
gantt.config.date_scale = "%d/%m/%Y";
|
||||
gantt.config.subscales = [{unit:"hour", step:1, date:"%H:%i"}];
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 50;
|
||||
break;
|
||||
case "week":
|
||||
var weekScaleTemplate = function(date){
|
||||
var dateToStr = gantt.date.date_to_str("%d/%m/%Y");
|
||||
var endDate = gantt.date.add(gantt.date.add(date, 1, "week"), -1, "day");
|
||||
return gantt.date.date_to_str("%W")(date) + "(" + dateToStr(date) + " - " + dateToStr(endDate) + ")";
|
||||
};
|
||||
gantt.config.scale_unit = "week";
|
||||
gantt.templates.date_scale = weekScaleTemplate;
|
||||
gantt.config.min_column_width = 50;
|
||||
gantt.config.subscales = [
|
||||
{unit:"day", step:1, date:"%D %d" }];
|
||||
break;
|
||||
case "month":
|
||||
gantt.config.scale_unit = "month";
|
||||
gantt.config.date_scale = "%F, %Y";
|
||||
gantt.config.subscales = [
|
||||
{unit:"week", step:1, date:"%W" }
|
||||
];
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 50;
|
||||
break;
|
||||
case "year":
|
||||
gantt.config.scale_unit = "year";
|
||||
gantt.config.date_scale = "%Y";
|
||||
gantt.templates.date_scale = null;
|
||||
gantt.config.min_column_width = 100;
|
||||
gantt.config.subscales = [
|
||||
{unit:"month", step:1, date:"%M" }
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getGanttColumns() {
|
||||
|
||||
var colHeader = '<div class="gantt_grid_head_cell gantt_grid_head_add" onclick="gantt.createTask()"></div>';
|
||||
|
||||
var colContent = function(task){
|
||||
return '<i class="fa gantt_button_grid gantt_grid_add fa-plus" onclick="gantt.createTask(null, '+task.id+')"></i>'+
|
||||
'<i class="fa gantt_button_grid gantt_grid_delete fa-times" onclick="gantt.confirm({ ' +
|
||||
'title: gantt.locale.labels.confirm_deleting_title,'+
|
||||
'text: gantt.locale.labels.confirm_deleting,'+
|
||||
'callback: function(res){ '+
|
||||
' if(res)'+
|
||||
' gantt.deleteTask('+task.id+');'+
|
||||
'}})"></i>';
|
||||
};
|
||||
|
||||
|
||||
var columns = [];
|
||||
|
||||
if (schema.taskUser) {
|
||||
columns.push({name: "users", label: fields[schema.taskUser].title, align: "center", template: function (item) {
|
||||
return byId(gantt.serverList("users"), item.user_id);
|
||||
}});
|
||||
}
|
||||
|
||||
var isTree = true;
|
||||
_.each(fieldNames, function(fname){
|
||||
var field = fields[fname];
|
||||
if (columns.length == 0) {
|
||||
columns.push({ name:"text", label:field.title, tree:isTree,
|
||||
template: function(item){
|
||||
if(moment(item[fname], moment.ISO_8601, true).isValid()) {
|
||||
return moment(item[fname], moment.ISO_8601, true).format("MM/DD/YYYY h:mm:ss");
|
||||
}
|
||||
return item.text;
|
||||
}});
|
||||
}
|
||||
else {
|
||||
columns.push({ name:field.name, label:field.title, tree:isTree,
|
||||
template: function(item){
|
||||
if (!item.label) {
|
||||
if(moment(item[fname], moment.ISO_8601, true).isValid()) {
|
||||
return moment(item[fname], moment.ISO_8601, true).format("MM/DD/YYYY h:mm:ss");
|
||||
}
|
||||
return item[fname];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
isTree = false;
|
||||
});
|
||||
columns.push({ name:"buttons", label:colHeader, width:30, template:colContent });
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function setChildTaskDisplay() {
|
||||
|
||||
function createBox(sizes, class_name){
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = [
|
||||
"height:" + sizes.height + "px",
|
||||
"line-height:" + sizes.height + "px",
|
||||
"width:" + sizes.width + "px",
|
||||
"top:" + sizes.top + 'px',
|
||||
"left:" + sizes.left + "px",
|
||||
"position:absolute"
|
||||
].join(";");
|
||||
box.className = class_name;
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
gantt.templates.grid_row_class = gantt.templates.task_class=function(start, end, task){
|
||||
var css = [];
|
||||
if(gantt.hasChild(task.id)){
|
||||
css.push("task-parent");
|
||||
}
|
||||
if (!task.$open && gantt.hasChild(task.id)) {
|
||||
css.push("task-collapsed");
|
||||
}
|
||||
|
||||
if (task.$virtual || task.type == gantt.config.types.project)
|
||||
css.push("summary-bar");
|
||||
|
||||
if(task.user_id){
|
||||
css.push("gantt_resource_task gantt_resource_" + task.user_id);
|
||||
}
|
||||
|
||||
return css.join(" ");
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function ganttInit(){
|
||||
gantt = main.dhx_gantt();
|
||||
setScaleConfig("week");
|
||||
gantt.templates.leftside_text = function(start, end, task){
|
||||
if (!task.progress){
|
||||
return "";
|
||||
}
|
||||
return "<span style='text-align:left;'>"+Math.round(task.progress*100)+ "% </span>";
|
||||
};
|
||||
gantt.config.step = 1;
|
||||
gantt.config.duration_unit = "hour";
|
||||
gantt.config.duration_step = 1;
|
||||
gantt.config.scale_height = 75;
|
||||
gantt.config.grid_width = 400;
|
||||
gantt.config.fit_tasks = true;
|
||||
gantt.config.columns = getGanttColumns();
|
||||
gantt._onTaskIdChange = null;
|
||||
gantt._onLinkIdChange = null;
|
||||
gantt.config.autosize = "x";
|
||||
gantt.config.grid_resize = true;
|
||||
gantt.config.order_branch = true;
|
||||
gantt.config.date_grid = "%d/%m/%Y %H %i";
|
||||
gantt.serverList("users", []);
|
||||
|
||||
gantt.eachSuccessor = function(callback, root){
|
||||
if(!this.isTaskExists(root))
|
||||
return;
|
||||
|
||||
// remember tasks we've already iterated in order to avoid infinite loops
|
||||
var traversedTasks = arguments[2] || {};
|
||||
if(traversedTasks[root])
|
||||
return;
|
||||
traversedTasks[root] = true;
|
||||
|
||||
var rootTask = this.getTask(root);
|
||||
var links = rootTask.$source;
|
||||
if(links){
|
||||
for(var i=0; i < links.length; i++){
|
||||
var link = this.getLink(links[i]);
|
||||
if(this.isTaskExists(link.target)){
|
||||
callback.call(this, this.getTask(link.target));
|
||||
|
||||
// iterate the whole branch, not only first-level dependencies
|
||||
this.eachSuccessor(callback, link.target, traversedTasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gantt.templates.task_class=function(start, end, task){
|
||||
if(task.$virtual)
|
||||
return "summary-bar";
|
||||
};
|
||||
ganttAttachEvents();
|
||||
setChildTaskDisplay();
|
||||
fetchRecords();
|
||||
}
|
||||
|
||||
function ganttAttachEvents(){
|
||||
|
||||
gantt.templates.rightside_text = function(start, end, task){
|
||||
return byId(gantt.serverList("users"), task.user_id);
|
||||
};
|
||||
|
||||
if (schema.taskUser) {
|
||||
gantt.attachEvent("onParse", function(){
|
||||
var styleId = "dynamicGanttStyles";
|
||||
var element = document.getElementById(styleId);
|
||||
if(!element){
|
||||
element = document.createElement("style");
|
||||
element.id = styleId;
|
||||
document.querySelector("head").appendChild(element);
|
||||
}
|
||||
var html = [".gantt_cell:nth-child(1) .gantt_tree_content{" +
|
||||
" border-radius: 16px;" +
|
||||
" width: 100%;" +
|
||||
" height: 70%;" +
|
||||
" margin: 5% 0;" +
|
||||
" line-height: 230%;}"];
|
||||
var resources = gantt.serverList("users");
|
||||
|
||||
resources.forEach(function(r){
|
||||
html.push(".gantt_task_line.gantt_resource_" + r.key + "{" +
|
||||
"background-color:"+r.backgroundColor+"; " +
|
||||
"color:"+r.textColor+";" +
|
||||
"}");
|
||||
html.push(".gantt_row.gantt_resource_" + r.key + " .gantt_cell:nth-child(1) .gantt_tree_content{" +
|
||||
"background-color:"+r.backgroundColor+"; " +
|
||||
"color:"+r.textColor+";" +
|
||||
"}");
|
||||
});
|
||||
element.innerHTML = html.join("");
|
||||
});
|
||||
}
|
||||
|
||||
gantt.attachEvent("onAfterTaskAdd", updateRecord);
|
||||
gantt.attachEvent("onAfterTaskUpdate", updateRecord);
|
||||
gantt.attachEvent("onAfterTaskDelete", scope.doRemove);
|
||||
|
||||
gantt.attachEvent("onAfterLinkAdd", updateLink);
|
||||
gantt.attachEvent("onAfterLinkUpdate", updateLink);
|
||||
gantt.attachEvent("onAfterLinkDelete", deleteLink);
|
||||
|
||||
gantt.attachEvent("onTaskCreated",function(task){
|
||||
scope.showEditor(task, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
gantt.attachEvent("onBeforeLightbox", function(id) {
|
||||
var task = gantt.getTask(id);
|
||||
scope.showEditor(task, false);
|
||||
return false;
|
||||
});
|
||||
|
||||
var diff = 0;
|
||||
|
||||
gantt.attachEvent("onBeforeTaskChanged", function(id, mode, originalTask){
|
||||
var modes = gantt.config.drag_mode;
|
||||
if(mode == modes.move ){
|
||||
var modifiedTask = gantt.getTask(id);
|
||||
diff = modifiedTask.start_date - originalTask.start_date;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
//rounds positions of the child items to scale
|
||||
gantt.attachEvent("onAfterTaskDrag", function(id, mode, e){
|
||||
var modes = gantt.config.drag_mode;
|
||||
if(mode == modes.move ){
|
||||
gantt.eachSuccessor(function(child){
|
||||
child.start_date = gantt.roundDate(new Date(child.start_date.valueOf() + diff));
|
||||
child.end_date = gantt.calculateEndDate(child.start_date, child.duration);
|
||||
gantt.updateTask(child.id);
|
||||
},id );
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function fetchRecords() {
|
||||
|
||||
scope.fetchItems(function(records) {
|
||||
var data = [];
|
||||
var links = [];
|
||||
_.each(records, function(rec) {
|
||||
addData(data, rec);
|
||||
addLinks(links, rec);
|
||||
});
|
||||
gantt.parse({ "data":data, "links":links });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateRecordItem(id,link,toRemove){
|
||||
|
||||
var linkMap = {
|
||||
"0":"finishToStart",
|
||||
"1":"startToStart",
|
||||
"2":"finishToFinish",
|
||||
"3":"startToFinish"
|
||||
};
|
||||
|
||||
var linkField = schema[linkMap[link.type]];
|
||||
var task = gantt.getTask(link.target);
|
||||
var record = task.record;
|
||||
|
||||
if(record && linkField){
|
||||
var endRecord = gantt.getTask(link.source).record;
|
||||
if(endRecord){
|
||||
var recordList = record[linkField];
|
||||
recordList = recordList.filter(function(item, idx) {
|
||||
return item.id != endRecord.id;
|
||||
});
|
||||
|
||||
if(!toRemove){
|
||||
recordList.push(endRecord);
|
||||
}
|
||||
record[linkField] = recordList;
|
||||
task.record = record;
|
||||
scope.doSave(task, updateTaskRecord);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateLink(id,link){
|
||||
updateRecordItem(id, link, false);
|
||||
}
|
||||
|
||||
function updateTaskRecord(task, rec){
|
||||
task.record = rec;
|
||||
}
|
||||
|
||||
function deleteLink(id, link){
|
||||
updateRecordItem(id, link, true);
|
||||
}
|
||||
|
||||
function updateRecord(id, item){
|
||||
|
||||
var record = item.record;
|
||||
if(!record){ record = {}; }
|
||||
|
||||
var duration = item.duration || 1;
|
||||
|
||||
record[schema.taskStart] = item.start_date.toJSON();
|
||||
record[firstField.name] = item.text;
|
||||
|
||||
if(schema.taskProgress){
|
||||
record[schema.taskProgress] = item.progress*100;
|
||||
}
|
||||
if(schema.taskSequence){
|
||||
record[schema.taskSequence] = item.order;
|
||||
}
|
||||
if(schema.taskDuration){
|
||||
record[schema.taskDuration] = duration;
|
||||
}
|
||||
if(schema.taskEnd){
|
||||
record[schema.taskEnd] = item.end_date.toJSON();
|
||||
}
|
||||
|
||||
if(schema.taskParent && item.parent && !record[schema.taskParent]){
|
||||
var parentTask = gantt.getTask(item.parent);
|
||||
var parentRecord = parentTask.record;
|
||||
if(parentRecord){
|
||||
record[schema.taskParent] = parentRecord;
|
||||
}
|
||||
}
|
||||
|
||||
return scope.doSave(item, updateTaskRecord);
|
||||
}
|
||||
|
||||
function addData(data, rec){
|
||||
|
||||
if(rec[schema.taskStart]){
|
||||
var dict = {
|
||||
id:rec.id,
|
||||
open:true,
|
||||
isNew:true
|
||||
};
|
||||
dict = updateData(dict, rec);
|
||||
dict.isNew = false;
|
||||
|
||||
if(dict.start_date){
|
||||
data.push(dict);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addLinkDict(links, targetRecordId, sourceRecords, linkType){
|
||||
|
||||
_.each(sourceRecords, function(sourceRec){
|
||||
links.push({
|
||||
id:targetRecordId+"-"+sourceRec.id,
|
||||
target:targetRecordId,
|
||||
source:sourceRec.id,
|
||||
type:linkType
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function addLinks(links,record){
|
||||
|
||||
var linkMap = {
|
||||
"finishToStart":"0",
|
||||
"startToStart":"1",
|
||||
"finishToFinish":"2",
|
||||
"startToFinish":"3"
|
||||
};
|
||||
|
||||
_.each(_.keys(linkMap), function(key) {
|
||||
if(schema[key]){
|
||||
addLinkDict(links, record.id, record[schema[key]], linkMap[key]);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateTask(task, rec){
|
||||
|
||||
task = updateData(task, rec);
|
||||
|
||||
if(!task.isNew){
|
||||
gantt.refreshTask(task.id);
|
||||
}
|
||||
task.isNew = false;
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
function updateData(task, rec){
|
||||
|
||||
task.record = rec;
|
||||
|
||||
var name = firstField.targetName ? rec[firstField.targetName] : rec[firstField.name];
|
||||
task.text = "";
|
||||
if(name){
|
||||
task.text = name;
|
||||
}
|
||||
_.each(fields,function(field){
|
||||
var val = rec[field.name];
|
||||
if(_.isObject(val) && field.targetName){
|
||||
val = val[field.targetName];
|
||||
}
|
||||
task[field.name] = val || "";
|
||||
});
|
||||
|
||||
var endDate = null;
|
||||
if(schema.taskEnd && rec[schema.taskEnd]){
|
||||
endDate = moment(rec[schema.taskEnd]);
|
||||
task.end_date = endDate.toDate();
|
||||
if(task.isNew){
|
||||
task.end_date = endDate.format("DD-MM-YYYY HH:mm:SS");
|
||||
}
|
||||
}
|
||||
|
||||
var startDate = moment(rec[schema.taskStart]);
|
||||
if(task.isNew){
|
||||
task.start_date = startDate.format("DD-MM-YYYY HH:mm:SS");
|
||||
}
|
||||
else{
|
||||
task.start_date = startDate.toDate();
|
||||
}
|
||||
|
||||
|
||||
if(schema.taskDuration && rec[schema.taskDuration]){
|
||||
task.duration = rec[schema.taskDuration];
|
||||
}
|
||||
else if(endDate){
|
||||
task.duration = gantt.calculateDuration(startDate.toDate(), endDate);
|
||||
}
|
||||
else{
|
||||
task.duration = "1";
|
||||
}
|
||||
|
||||
if(!endDate){
|
||||
task.end_date = gantt.calculateEndDate(startDate.toDate(), task.duration);
|
||||
}
|
||||
|
||||
if(schema.taskProgress){
|
||||
task.progress = rec[schema.taskProgress]/100;
|
||||
}
|
||||
|
||||
if(schema.taskParent){
|
||||
if(rec[schema.taskParent] && rec[schema.taskParent].id != task.id){
|
||||
task.parent = rec[schema.taskParent].id;
|
||||
}
|
||||
else{
|
||||
task.parent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(schema.taskSequence){
|
||||
task.sortorder = rec[schema.taskSequence];
|
||||
}
|
||||
|
||||
if(schema.taskUser && rec[schema.taskUser]) {
|
||||
task.user_id = rec[schema.taskUser].id;
|
||||
if(!byId(gantt.serverList("users"), task.user_id)) {
|
||||
gantt.serverList("users").push({key:task.user_id,
|
||||
label:rec[schema.taskUser][fields[schema.taskUser].targetName],
|
||||
backgroundColor: get_random_color(),
|
||||
textColor:"#FFF"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
}
|
||||
|
||||
function get_random_color() {
|
||||
function c() {
|
||||
var hex = Math.floor(Math.random()*256).toString(16);
|
||||
return ("0"+String(hex)).substr(-2); // pad with zero
|
||||
}
|
||||
return "#"+c()+c()+c();
|
||||
}
|
||||
|
||||
|
||||
scope.onMode = function(name) {
|
||||
setScaleConfig(name);
|
||||
mode = name;
|
||||
gantt.render();
|
||||
};
|
||||
|
||||
scope.isMode = function(name) {
|
||||
return mode === name;
|
||||
};
|
||||
|
||||
scope.onRefresh = function () {
|
||||
gantt.clearAll();
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
scope.onPrint = function () {
|
||||
gantt.exportToPDF({
|
||||
name: "Gantt.pdf",
|
||||
callback: function(result){
|
||||
window.open(result.url , '_self');
|
||||
}});
|
||||
};
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
gantt.clearAll();
|
||||
gantt.detachAllEvents();
|
||||
});
|
||||
|
||||
scope.showEditor = function(task, isNew) {
|
||||
var record = _.extend({}, task.record);
|
||||
if (!editor) {
|
||||
editor = ViewService.compile('<div ui-editor-popup></div>')(scope.$new());
|
||||
editor.data('$target', element);
|
||||
}
|
||||
|
||||
var popup = editor.isolateScope();
|
||||
popup.setEditable(true);
|
||||
|
||||
if(isNew && schema.taskParent && task.parent && !record[schema.taskParent]){
|
||||
var parentTask = gantt.getTask(task.parent);
|
||||
var parentRecord = parentTask.record;
|
||||
if(parentRecord){
|
||||
record[schema.taskParent] = parentRecord;
|
||||
}
|
||||
}
|
||||
|
||||
popup.show(record, function(result) {
|
||||
task.isNew = isNew;
|
||||
task = updateTask(task, result);
|
||||
if(isNew){
|
||||
gantt.addTask(task);
|
||||
}
|
||||
else {
|
||||
gantt.updateTask(task.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!record || !record.id) {
|
||||
popup.waitForActions(function() {
|
||||
popup.$broadcast("on:new");
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
link:function(scope, element, attrs, controller) {
|
||||
scope._viewPromise.then(function(){
|
||||
link(scope, element, attrs, controller);
|
||||
});
|
||||
},
|
||||
replace:true,
|
||||
template:
|
||||
'<div>'+
|
||||
'<div class="gantt-main"></div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
1065
sophal/js/view/view.grid.js
Normal file
1065
sophal/js/view/view.grid.js
Normal file
File diff suppressed because it is too large
Load Diff
130
sophal/js/view/view.html.js
Normal file
130
sophal/js/view/view.html.js
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.HtmlViewCtrl = HtmlViewCtrl;
|
||||
ui.HtmlViewCtrl.$inject = ['$scope', '$element', '$sce', '$interpolate'];
|
||||
|
||||
function HtmlViewCtrl($scope, $element, $sce, $interpolate) {
|
||||
|
||||
var views = $scope._views;
|
||||
var stamp = -1;
|
||||
|
||||
$scope.view = views.html;
|
||||
|
||||
$scope.getContext = function () {
|
||||
var params = $scope._viewParams || {};
|
||||
var parent = $scope.$parent;
|
||||
return _.extend({}, params.context, parent.getContext ? parent.getContext() : {});
|
||||
};
|
||||
|
||||
$scope.getURL = function getURL() {
|
||||
var view = $scope.view;
|
||||
if (view) {
|
||||
var url = view.name || view.resource;
|
||||
if (stamp > 0) {
|
||||
var q = url.lastIndexOf('?');
|
||||
if (q > -1) {
|
||||
url += "&t" + stamp;
|
||||
} else {
|
||||
url += "?t" + stamp;
|
||||
}
|
||||
}
|
||||
if (url && url.indexOf('{{') > -1) {
|
||||
url = $interpolate(url)($scope.getContext());
|
||||
}
|
||||
return $sce.trustAsResourceUrl(url);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
if (stamp > -1) {
|
||||
stamp = new Date().getTime();
|
||||
} else {
|
||||
stamp = 0;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: "html"
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
if ($scope._viewParams) {
|
||||
$scope._viewParams.$viewScope = $scope;
|
||||
$scope.show();
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function() {
|
||||
if ($scope.view.deferred) {
|
||||
$scope.view.deferred.resolve($scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var directiveFn = function(){
|
||||
return {
|
||||
controller: HtmlViewCtrl,
|
||||
replace: true,
|
||||
link: function (scope, element) {
|
||||
setTimeout(function () {
|
||||
element.parents('[ui-attach]').each(function () {
|
||||
$(this).scope().keepAttached = true;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// XXX: chrome 76 issue? See RM-20400
|
||||
if (axelor.browser.chrome) {
|
||||
scope.$on('on:nav-click', function (e, tab) {
|
||||
if (tab.$viewScope !== scope) return;
|
||||
var iframe = element.find('iframe')[0];
|
||||
var embed = iframe.contentDocument.body.firstChild;
|
||||
if (embed && embed.id === 'plugin') {
|
||||
embed.height = '101%';
|
||||
setTimeout(function () {
|
||||
embed.height = '100%';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div class="iframe-container">'+
|
||||
'<iframe ng-src="{{getURL()}}" frameborder="0" scrolling="auto"></iframe>'+
|
||||
'</div>'
|
||||
};
|
||||
};
|
||||
|
||||
ui.directive('uiViewHtml', directiveFn);
|
||||
ui.directive('uiPortletHtml', directiveFn);
|
||||
|
||||
})();
|
||||
661
sophal/js/view/view.kanban.js
Normal file
661
sophal/js/view/view.kanban.js
Normal file
@ -0,0 +1,661 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
function BaseCardsCtrl(type, $scope, $element) {
|
||||
|
||||
ui.DSViewCtrl(type, $scope, $element);
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: type,
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
|
||||
var ds = $scope._dataSource;
|
||||
var initialized = false;
|
||||
|
||||
$scope.onShow = function (viewPromise) {
|
||||
|
||||
if (initialized) {
|
||||
return $scope.onRefresh();
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
|
||||
viewPromise.then(function (meta) {
|
||||
$scope.parse(meta.fields, meta.view);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
|
||||
};
|
||||
|
||||
$scope.onNew = function () {
|
||||
ds._page.index = -1;
|
||||
$scope.switchTo('form', function (formScope) {
|
||||
formScope.edit(null);
|
||||
formScope.setEditable();
|
||||
formScope.$broadcast("on:new");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
return $scope.filter({});
|
||||
};
|
||||
|
||||
function update(records) {
|
||||
$scope.records = records;
|
||||
}
|
||||
|
||||
$scope.handleEmpty = function () {
|
||||
};
|
||||
|
||||
$scope.filter = function(options) {
|
||||
var view = $scope.schema;
|
||||
var opts = {
|
||||
fields: _.pluck($scope.fields, 'name')
|
||||
};
|
||||
var handleEmpty = $scope.handleEmpty.bind($scope);
|
||||
|
||||
if (options.criteria || options._domains) {
|
||||
opts.filter = options;
|
||||
}
|
||||
if (options.archived !== undefined) {
|
||||
opts.archived = options.archived;
|
||||
}
|
||||
if (view.orderBy) {
|
||||
opts.sortBy = view.orderBy.split(',');
|
||||
}
|
||||
|
||||
var promise = ds.search(opts);
|
||||
promise.then(handleEmpty, handleEmpty);
|
||||
return promise.success(update).then(function () {
|
||||
$scope.handleEmpty();
|
||||
return ds.fixPage();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.pagerText = function() {
|
||||
var page = ds._page;
|
||||
if (page && page.from !== undefined) {
|
||||
if (page.total === 0) return null;
|
||||
return _t("{0} to {1} of {2}", page.from + 1, page.to, page.total);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onNext = function() {
|
||||
var fields = _.pluck($scope.fields, 'name');
|
||||
return ds.next(fields).success(update);
|
||||
};
|
||||
|
||||
$scope.onPrev = function() {
|
||||
var fields = _.pluck($scope.fields, 'name');
|
||||
return ds.prev(fields).success(update);
|
||||
};
|
||||
|
||||
$scope.getActionData = function(context) {
|
||||
return _.extend({
|
||||
_domain: ds._lastDomain,
|
||||
_domainContext: _.extend({}, ds._lastContext, context),
|
||||
_archived: ds._showArchived
|
||||
}, ds._filter);
|
||||
};
|
||||
}
|
||||
|
||||
ui.controller("CardsCtrl", ['$scope', '$element', function CardsCtrl($scope, $element) {
|
||||
|
||||
BaseCardsCtrl.call(this, 'cards', $scope, $element);
|
||||
|
||||
$scope.viewItems = {};
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
var viewItems = {};
|
||||
_.each(view.items, function (item) {
|
||||
if (item.name) {
|
||||
viewItems[item.name] = _.extend({}, item, fields[item.name], item.widgetAttrs);
|
||||
}
|
||||
});
|
||||
$scope.viewItems = viewItems;
|
||||
$scope.onRefresh();
|
||||
$scope.waitForActions(axelor.$adjustSize);
|
||||
};
|
||||
|
||||
$scope.onExport = function (full) {
|
||||
var fields = full ? [] : _.pluck($scope.viewItems, 'name');
|
||||
return $scope._dataSource.export_(fields).success(function(res) {
|
||||
var fileName = res.fileName;
|
||||
var filePath = 'ws/rest/' + $scope._model + '/export/' + fileName;
|
||||
ui.download(filePath, fileName);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.controller("KanbanCtrl", ['$scope', '$element', 'ActionService', function KanbanCtrl($scope, $element, ActionService) {
|
||||
|
||||
BaseCardsCtrl.call(this, 'kanban', $scope, $element);
|
||||
|
||||
$scope.parse = function (fields, view) {
|
||||
var params = $scope._viewParams.params || {};
|
||||
var hideCols = (params['kanban-hide-columns'] || '').split(',');
|
||||
var columnBy = fields[view.columnBy] || {};
|
||||
var columns = _.filter(columnBy.selectionList, function (item) {
|
||||
return hideCols.indexOf(item.value) === -1;
|
||||
});
|
||||
|
||||
var first = _.first(columns);
|
||||
if (view.onNew) {
|
||||
first.canCreate = true;
|
||||
}
|
||||
|
||||
var sequenceBy = fields[view.sequenceBy] || {};
|
||||
if (["integer", "long"].indexOf(sequenceBy.type) === -1 || ["id", "version"].indexOf(sequenceBy.name) > -1) {
|
||||
throw new Error("Invalid sequenceBy field in view: " + view.name);
|
||||
}
|
||||
|
||||
$scope.sortableOptions.disabled = !view.draggable || !$scope.hasPermission('write');
|
||||
$scope.columns = columns;
|
||||
$scope.colWidth = params['kanban-column-width'];
|
||||
};
|
||||
|
||||
$scope.move = function (record, to, next, prev) {
|
||||
if(!record) {
|
||||
return;
|
||||
}
|
||||
var ds = $scope._dataSource._new($scope._model);
|
||||
var view = $scope.schema;
|
||||
|
||||
var rec = _.pick(record, "id", "version", view.sequenceBy);
|
||||
var prv = prev ? _.pick(prev, "id", "version", view.sequenceBy) : null;
|
||||
var nxt = next ? _.pick(next, "id", "version", view.sequenceBy) : null;
|
||||
|
||||
// update columnBy
|
||||
rec[view.columnBy] = to;
|
||||
|
||||
// update sequenceBy
|
||||
var all = _.compact([prv, rec, nxt]);
|
||||
var offset = _.min(_.pluck(all, view.sequenceBy)) || 0;
|
||||
|
||||
_.each(all, function (item, i) {
|
||||
item[view.sequenceBy] = offset + i;
|
||||
});
|
||||
|
||||
function doSave() {
|
||||
return ds.saveAll(all).success(function (records) {
|
||||
_.each(_.compact([prev, rec, next]), function (item) {
|
||||
_.extend(item, _.pick(ds.get(item.id), "version", view.sequenceBy));
|
||||
});
|
||||
_.extend(record, rec);
|
||||
}).error(function () {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
if (view.onMove) {
|
||||
var actScope = $scope.$new();
|
||||
actScope.record = rec;
|
||||
actScope.getContext = function () {
|
||||
return _.extend({}, $scope._context, rec);
|
||||
};
|
||||
return ActionService.handler(actScope, $(), { action: view.onMove }).handle().then(function () {
|
||||
return doSave();
|
||||
}, function (err) {
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
return doSave();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function () {
|
||||
$scope.$broadcast("on:refresh");
|
||||
};
|
||||
|
||||
$scope.filter = function(searchFilter) {
|
||||
var options = {};
|
||||
if (searchFilter.criteria || searchFilter._domains) {
|
||||
options = {
|
||||
filter: searchFilter
|
||||
};
|
||||
if (searchFilter.archived !== undefined) {
|
||||
options.archived = searchFilter.archived;
|
||||
}
|
||||
$scope.$broadcast("on:filter", options);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sortableOptions = {
|
||||
connectWith: ".kanban-card-list",
|
||||
items: ".kanban-card",
|
||||
tolerance: "pointer",
|
||||
helper: "clone",
|
||||
stop: function (event, ui) {
|
||||
$scope.$broadcast('on:re-attach-click');
|
||||
var item = ui.item;
|
||||
var sortable = item.sortable;
|
||||
var source = sortable.source.scope();
|
||||
var target = (sortable.droptarget || $(this)).scope();
|
||||
|
||||
var next = item.next().scope();
|
||||
var prev = item.prev().scope();
|
||||
if (next) next = next.record;
|
||||
if (prev) prev = prev.record;
|
||||
|
||||
var index = sortable.dropindex;
|
||||
if (source === target && sortable.index === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.move(target.records[index], target.column.value, next, prev);
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiKanbanColumn', ["ActionService", function (ActionService) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var ds = scope._dataSource._new(scope._model);
|
||||
var view = scope.schema;
|
||||
var elemMore = element.children(".kanban-more");
|
||||
|
||||
ds._context = _.extend({}, scope._dataSource._context);
|
||||
ds._context[view.columnBy] = scope.column.value;
|
||||
ds._page.limit = view.limit || 20;
|
||||
|
||||
var domain = "self." + view.columnBy + " = :" + view.columnBy;
|
||||
ds._domain = scope._dataSource._domain ? scope._dataSource._domain + " AND " + domain : domain;
|
||||
|
||||
scope.records = [];
|
||||
|
||||
function handleEmpty() {
|
||||
element.toggleClass('empty', scope.isEmpty());
|
||||
}
|
||||
|
||||
function fetch(options) {
|
||||
var opts = _.extend({
|
||||
offset: 0,
|
||||
sortBy: [view.sequenceBy],
|
||||
fields: _.pluck(scope.fields, 'name')
|
||||
}, options);
|
||||
elemMore.hide();
|
||||
var promise = ds.search(opts);
|
||||
promise.success(function (records) {
|
||||
scope.records = scope.records.concat(records);
|
||||
elemMore.fadeIn('slow');
|
||||
});
|
||||
return promise.then(handleEmpty, handleEmpty);
|
||||
}
|
||||
|
||||
scope.$watch('records.length', handleEmpty);
|
||||
|
||||
scope.hasMore = function () {
|
||||
var page = ds._page;
|
||||
var next = page.from + page.limit;
|
||||
return next < page.total;
|
||||
};
|
||||
|
||||
scope.isEmpty = function () {
|
||||
return scope.records.length == 0;
|
||||
};
|
||||
|
||||
scope.onMore = function () {
|
||||
var page = ds._page;
|
||||
var next = scope.records.length;
|
||||
if (next < page.total) {
|
||||
return fetch({
|
||||
offset: next
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var onNew = null;
|
||||
|
||||
scope.getContext = function () {
|
||||
var ctx = _.extend({}, scope._context);
|
||||
ctx._value = scope.newItem;
|
||||
return ctx;
|
||||
};
|
||||
|
||||
scope.newItem = null;
|
||||
scope.onCreate = function () {
|
||||
|
||||
var rec = scope.record = {};
|
||||
var view = scope.schema;
|
||||
|
||||
rec[view.columnBy] = scope.column.value;
|
||||
|
||||
if (onNew === null) {
|
||||
onNew = ActionService.handler(scope, element, {
|
||||
action: view.onNew
|
||||
});
|
||||
}
|
||||
|
||||
var ds = scope._dataSource;
|
||||
var promise = onNew.handle();
|
||||
promise.then(function () {
|
||||
ds.save(scope.record).success(function (rec) {
|
||||
scope.newItem = null;
|
||||
scope.records.unshift(rec);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.onEdit = function (record, readonly) {
|
||||
scope.switchTo('form', function (formScope) {
|
||||
formScope.edit(record);
|
||||
formScope.setEditable(!readonly && scope.hasPermission('write') && formScope.canEdit());
|
||||
});
|
||||
};
|
||||
|
||||
scope.onDelete = function (record) {
|
||||
axelor.dialogs.confirm(_t("Do you really want to delete the selected record?"),
|
||||
function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
ds.removeAll([record]).success(function(records, page) {
|
||||
var index = scope.records.indexOf(record);
|
||||
scope.records.splice(index, 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.$on("on:refresh", function (e) {
|
||||
scope.newItem = null;
|
||||
scope.records.length = 0;
|
||||
fetch();
|
||||
});
|
||||
|
||||
scope.$on("on:filter", function (e, options) {
|
||||
scope.newItem = null;
|
||||
scope.records.length = 0;
|
||||
return fetch(options);
|
||||
});
|
||||
|
||||
element.on("click", ".kanban-card", function (e) {
|
||||
var elem = $(e.target);
|
||||
var selector = '[ng-click],[ui-action-click],button,a,.iswitch,.ibox,.kanban-card-menu';
|
||||
if (elem.is(selector) || element.find(selector).has(elem).length) {
|
||||
return;
|
||||
}
|
||||
var record = $(this).scope().record;
|
||||
scope.onEdit(record, true);
|
||||
scope.$applyAsync();
|
||||
});
|
||||
|
||||
if (scope.colWidth) {
|
||||
element.width(scope.colWidth);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
element.find('[ui-sortable]').sortable("option", "appendTo", element.parent());
|
||||
});
|
||||
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiCards', function () {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
var onRefresh = scope.onRefresh;
|
||||
scope.onRefresh = function () {
|
||||
scope.records = null;
|
||||
return onRefresh.apply(scope, arguments);
|
||||
};
|
||||
|
||||
scope.onEdit = function (record, readonly) {
|
||||
var ds = scope._dataSource;
|
||||
var page = ds._page;
|
||||
page.index = record ? ds._data.indexOf(record) : -1;
|
||||
scope.switchTo('form', function (formScope) {
|
||||
formScope.setEditable(!readonly && scope.hasPermission('write') && formScope.canEdit());
|
||||
});
|
||||
};
|
||||
|
||||
scope.onDelete = function (record) {
|
||||
axelor.dialogs.confirm(_t("Do you really want to delete the selected record?"),
|
||||
function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
var ds = scope._dataSource;
|
||||
ds.removeAll([record]).success(function() {
|
||||
scope.onRefresh();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.isEmpty = function () {
|
||||
return (scope.records||[]).length == 0;
|
||||
};
|
||||
|
||||
scope.handleEmpty = function () {
|
||||
element.toggleClass('empty', scope.isEmpty());
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiCard', ["$compile", function ($compile) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
var body = element.find(".kanban-card-body");
|
||||
var record = scope.record;
|
||||
var evalScope = axelor.$evalScope(scope);
|
||||
|
||||
evalScope.record = record;
|
||||
evalScope.getContext = scope.getContext = function () {
|
||||
var ctx = _.extend({}, scope._context, scope.record);
|
||||
ctx._model = scope._model;
|
||||
return ctx;
|
||||
};
|
||||
|
||||
if (!record.$processed) {
|
||||
element.hide();
|
||||
}
|
||||
|
||||
function process(record) {
|
||||
if (record.$processed) {
|
||||
return record;
|
||||
}
|
||||
record.$processed = true;
|
||||
for (var name in record) {
|
||||
if (!record.hasOwnProperty(name) || name.indexOf('.') === -1) {
|
||||
continue;
|
||||
}
|
||||
var nested = record;
|
||||
var names = name.split('.');
|
||||
var head = _.first(names, names.length - 1);
|
||||
var last = _.last(names);
|
||||
var i, n;
|
||||
for (i = 0; i < head.length; i++) {
|
||||
n = head[i];
|
||||
nested = nested[n] || (nested[n] = {});
|
||||
}
|
||||
nested[last] = record[name];
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
evalScope.$watch("record", function cardRecordWatch(record) {
|
||||
_.extend(evalScope, process(record));
|
||||
}, true);
|
||||
|
||||
evalScope.$image = function (fieldName, imageName) {
|
||||
return ui.formatters.$image(scope, fieldName, imageName);
|
||||
};
|
||||
|
||||
evalScope.$fmt = function (fieldName) {
|
||||
return ui.formatters.$fmt(scope, fieldName, evalScope[fieldName]);
|
||||
};
|
||||
|
||||
var template = (scope.schema.template || "<span></span>").trim();
|
||||
if (template.indexOf('<') !== 0) {
|
||||
template = "<span>" + template + "</span>";
|
||||
}
|
||||
|
||||
scope.hilite = null;
|
||||
|
||||
$compile(template)(evalScope).appendTo(body);
|
||||
|
||||
var hilites = scope.schema.hilites || [];
|
||||
for (var i = 0; i < hilites.length; i++) {
|
||||
var hilite = hilites[i];
|
||||
if (axelor.$eval(evalScope, hilite.condition, scope.record)) {
|
||||
scope.hilite = hilite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.schema.width) {
|
||||
element.parent().css("width", scope.schema.width);
|
||||
}
|
||||
if (scope.schema.minWidth) {
|
||||
element.parent().css("min-width", scope.schema.minWidth);
|
||||
}
|
||||
if (scope.schema.maxWidth) {
|
||||
element.parent().css("max-width", scope.schema.maxWidth);
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
var elem = $(e.target);
|
||||
var selector = '[ng-click],[ui-action-click],button,a,.iswitch,.ibox,.kanban-card-menu';
|
||||
if (elem.is(selector) || element.find(selector).has(elem).length) {
|
||||
return;
|
||||
}
|
||||
var record = $(this).scope().record;
|
||||
scope.onEdit(record, true);
|
||||
scope.$applyAsync();
|
||||
}
|
||||
|
||||
function attachClick() {
|
||||
element.on('click', onClick);
|
||||
}
|
||||
|
||||
attachClick();
|
||||
|
||||
scope.$on('on:re-attach-click', function () {
|
||||
element.off('click', onClick);
|
||||
setTimeout(attachClick, 100);
|
||||
});
|
||||
|
||||
element.fadeIn("slow");
|
||||
|
||||
var summaryHandler;
|
||||
var summaryPlacement;
|
||||
var summary = body.find('.card-summary.popover');
|
||||
|
||||
var configureSummary = _.once(function configureSummary() {
|
||||
element.popover({
|
||||
placement: function (tip, el) {
|
||||
summaryPlacement = setTimeout(function () {
|
||||
$(tip).css('visibility', 'hidden').css('max-width', 400).position({
|
||||
my: 'left',
|
||||
at: 'right',
|
||||
of: el,
|
||||
using: function (pos, feedback) {
|
||||
$(feedback.element.element)
|
||||
.css(pos)
|
||||
.css('visibility', '')
|
||||
.removeClass('left right')
|
||||
.addClass(feedback.horizontal === 'left' ? 'right' : 'left');
|
||||
summaryPlacement = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
container: 'body',
|
||||
trigger: 'manual',
|
||||
title: summary.attr('title'),
|
||||
content: summary.html(),
|
||||
html: true
|
||||
});
|
||||
});
|
||||
|
||||
function showSummary() {
|
||||
configureSummary();
|
||||
if (summaryPlacement) {
|
||||
clearTimeout(summaryPlacement);
|
||||
summaryPlacement = null;
|
||||
}
|
||||
summaryHandler = setTimeout(function () {
|
||||
summaryHandler = null;
|
||||
element.popover('show');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function hideSummary() {
|
||||
if (summaryPlacement) {
|
||||
clearTimeout(summaryPlacement);
|
||||
summaryPlacement = null;
|
||||
}
|
||||
if (summaryHandler) {
|
||||
clearTimeout(summaryHandler);
|
||||
summaryHandler = null;
|
||||
}
|
||||
element.popover('hide');
|
||||
}
|
||||
|
||||
if (summary.length > 0) {
|
||||
element.on('mouseenter.summary', showSummary);
|
||||
element.on('mouseleave.summary', hideSummary);
|
||||
element.on('mousedown.summary', hideSummary);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (summaryHandler) {
|
||||
clearTimeout(summaryHandler);
|
||||
summaryHandler = null;
|
||||
}
|
||||
if (element) {
|
||||
element.off('mouseenter.summary');
|
||||
element.off('mouseleave.summary');
|
||||
element.off('mousedown.summary');
|
||||
element.popover('destroy');
|
||||
element = null;
|
||||
}
|
||||
}
|
||||
|
||||
element.on('$destroy', destroy);
|
||||
scope.$on('$destroy', destroy);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
||||
511
sophal/js/view/view.popup.js
Normal file
511
sophal/js/view/view.popup.js
Normal file
@ -0,0 +1,511 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module("axelor.ui");
|
||||
|
||||
EditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService', '$q'];
|
||||
|
||||
function EditorCtrl($scope, $element, DataSource, ViewService, $q) {
|
||||
|
||||
var parent = $scope.$parent;
|
||||
|
||||
$scope._viewParams = parent._viewParams;
|
||||
$scope.editorCanSave = parent.editorCanSave;
|
||||
$scope.editorCanReload = parent.editorCanReload;
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
|
||||
var closeCallback = null;
|
||||
var originalEdit = $scope.edit;
|
||||
var originalShow = $scope.show;
|
||||
|
||||
var recordVersion = -1;
|
||||
var canClose = false;
|
||||
var isClosed = true;
|
||||
|
||||
$scope.show = function(record, callback) {
|
||||
originalShow();
|
||||
if (_.isFunction(record)) {
|
||||
callback = record;
|
||||
record = null;
|
||||
}
|
||||
closeCallback = callback;
|
||||
isClosed = false;
|
||||
recordVersion = record ? record.version : -1;
|
||||
if (recordVersion === undefined && record) {
|
||||
recordVersion = record.$version;
|
||||
}
|
||||
this.edit(record);
|
||||
};
|
||||
|
||||
function doEdit(record, fireOnLoad) {
|
||||
if (record && record.id > 0 && (!_.isNumber(record.version) || !record.$fetched)) {
|
||||
$scope.doRead(record.id).success(function(rec) {
|
||||
if (record.$dirty) {
|
||||
rec = _.extend({}, rec, record);
|
||||
}
|
||||
originalEdit(rec, fireOnLoad);
|
||||
});
|
||||
} else {
|
||||
originalEdit(record, fireOnLoad);
|
||||
}
|
||||
canClose = false;
|
||||
}
|
||||
|
||||
var parentCanEditTarget = null;
|
||||
|
||||
$scope.canEditTarget = function () {
|
||||
if (parentCanEditTarget === null) {
|
||||
var parent = $scope.$parent;
|
||||
var func = parent.canEditTarget;
|
||||
while (parent && func === $scope.canEditTarget) {
|
||||
parent = parent.$parent;
|
||||
func = parent.canEditTarget;
|
||||
}
|
||||
parentCanEditTarget = func || angular.noop;
|
||||
}
|
||||
return parentCanEditTarget() !== false;
|
||||
};
|
||||
|
||||
var isEditable = $scope.isEditable;
|
||||
$scope.isEditable = function () {
|
||||
var id = ($scope.record || {}).id;
|
||||
var perm = id > 0 ? 'write' : 'create';
|
||||
if (parent.isReadonly && parent.isReadonly()) return false;
|
||||
return $scope.hasPermission(perm)
|
||||
&& (id > 0 ? $scope.canEditTarget() : true)
|
||||
&& isEditable.call($scope);
|
||||
};
|
||||
|
||||
var canEdit = $scope.canEdit;
|
||||
$scope.canEdit = function() {
|
||||
return $scope.canEditTarget() && canEdit.call($scope);
|
||||
};
|
||||
|
||||
$scope.edit = function(record, fireOnLoad) {
|
||||
if (isClosed) return;
|
||||
$scope._viewPromise.then(function(){
|
||||
doEdit(record, fireOnLoad);
|
||||
$scope.setEditable(!$scope.$parent.$$readonly);
|
||||
});
|
||||
};
|
||||
|
||||
function isChanged() {
|
||||
if ($scope.isDirty()) return true;
|
||||
var record = $scope.record || {};
|
||||
var version = record.version;
|
||||
return recordVersion !== version || record.$forceDirty;
|
||||
}
|
||||
|
||||
function canOK() {
|
||||
if (isClosed) return false;
|
||||
return isChanged();
|
||||
}
|
||||
|
||||
function onOK() {
|
||||
|
||||
var record = $scope.record;
|
||||
|
||||
function close(value, forceSelect) {
|
||||
if (value && (forceSelect || canOK())) {
|
||||
value.$fetched = true;
|
||||
value.selected = true;
|
||||
$scope.$parent.select(value);
|
||||
}
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
if ($scope.editorCanReload) {
|
||||
$scope.$parent.parentReload();
|
||||
}
|
||||
if (closeCallback && value) {
|
||||
closeCallback(value);
|
||||
}
|
||||
closeCallback = null;
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
var event = $scope.$broadcast('on:before-save', record);
|
||||
if (event.defaultPrevented) {
|
||||
if (event.error) {
|
||||
axelor.dialogs.error(event.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for onChange actions
|
||||
$scope.waitForActions(function() {
|
||||
if ($scope.editorCanSave && isChanged()) {
|
||||
if (record.id < 0)
|
||||
record.id = null;
|
||||
return $scope.onSave({force: true}).then(function(record, page) {
|
||||
// wait for onSave actions
|
||||
$scope.waitForActions(function(){
|
||||
close(record, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
if ($scope.isValid()) {
|
||||
close(record);
|
||||
} else if ($scope.showErrorNotice) {
|
||||
$scope.showErrorNotice();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$scope.onOK = function() {
|
||||
$scope.$timeout(onOK, 10);
|
||||
};
|
||||
|
||||
$scope.onBeforeClose = function(event, ui) {
|
||||
|
||||
if (canClose || !$scope.isDirty()) {
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.edit(null, false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
$scope.confirmDirty(function(){
|
||||
canClose = true;
|
||||
$element.dialog('close');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onHotKey = function (e, action) {
|
||||
|
||||
if (action === "save") {
|
||||
$(e.target).blur().focus();
|
||||
$scope.onOK();
|
||||
}
|
||||
|
||||
$scope.$applyAsync();
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
SelectorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
|
||||
function SelectorCtrl($scope, $element, DataSource, ViewService) {
|
||||
|
||||
var parent = $scope.$parent;
|
||||
|
||||
$scope._viewParams = parent._viewParams;
|
||||
$scope.getDomain = parent.getDomain;
|
||||
|
||||
ui.ViewCtrl.call(this, $scope, DataSource, ViewService);
|
||||
ui.GridViewCtrl.call(this, $scope, $element);
|
||||
|
||||
var searchLimit = (parent.field||{}).searchLimit || 0;
|
||||
if (searchLimit > 0) {
|
||||
$scope._dataSource._page.limit = searchLimit;
|
||||
}
|
||||
|
||||
function doFilter() {
|
||||
$scope.filter($scope.getDomain());
|
||||
}
|
||||
|
||||
var initialized = false;
|
||||
var origShow = $scope.show;
|
||||
$scope.show = function() {
|
||||
origShow();
|
||||
if (initialized) {
|
||||
doFilter();
|
||||
}
|
||||
};
|
||||
|
||||
var _getContext = $scope.getContext;
|
||||
$scope.getContext = function() {
|
||||
// selector popup should return parent's context
|
||||
if ($scope.$parent && $scope.$parent.getContext) {
|
||||
return $scope.$parent.getContext();
|
||||
}
|
||||
return _getContext();
|
||||
};
|
||||
|
||||
$scope.onItemClick = function(e, args) {
|
||||
$scope.$applyAsync($scope.onOK.bind($scope));
|
||||
};
|
||||
|
||||
var origOnShow = $scope.onShow;
|
||||
$scope.onShow = function(viewPromise) {
|
||||
|
||||
viewPromise.then(function(){
|
||||
var view = $scope.schema;
|
||||
var field = $scope.field || $scope.$parent.field;
|
||||
if (field) {
|
||||
view.orderBy = field.orderBy || view.orderBy;
|
||||
}
|
||||
$element.dialog('open');
|
||||
initialized = true;
|
||||
origOnShow(viewPromise);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onOK = function() {
|
||||
|
||||
var selection = _.map($scope.selection, function(index){
|
||||
return $scope.dataView.getItem(index);
|
||||
});
|
||||
|
||||
if (!_.isEmpty(selection)) {
|
||||
$scope.$applyAsync(function () {
|
||||
$scope.$parent.select(selection);
|
||||
$scope.selection = [];
|
||||
});
|
||||
}
|
||||
|
||||
$element.dialog('close');
|
||||
};
|
||||
|
||||
$scope.onCreate = function () {
|
||||
$element.dialog('close');
|
||||
$scope.$parent.onNew();
|
||||
};
|
||||
|
||||
$scope.canNew = function () {
|
||||
return $scope.hasPermission('create') && $scope.$parent.canNew();
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiDialogSize', function() {
|
||||
|
||||
return function (scope, element, attrs) {
|
||||
|
||||
// use only with dialogs
|
||||
if (attrs.uiDialog === undefined && !element.hasClass('ui-dialog-content')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var loaded = false;
|
||||
var addMaximizeButton = _.once(function () {
|
||||
var elemDialog = element.parent();
|
||||
var elemTitle = elemDialog.find('.ui-dialog-title');
|
||||
var elemButton = $('<a href="#" class="ui-dialog-titlebar-max"><i class="fa fa-expand"></i></a>')
|
||||
.click(function (e) {
|
||||
$(this).children('i').toggleClass('fa-expand fa-compress');
|
||||
elemDialog.toggleClass('maximized');
|
||||
axelor.$adjustSize();
|
||||
setTimeout(function () {
|
||||
scope.$broadcast('grid:adjust-columns');
|
||||
}, 350);
|
||||
return false;
|
||||
}).insertAfter(elemTitle);
|
||||
|
||||
// remove maximized state on close
|
||||
element.on('dialogclose', function(e, ui) {
|
||||
elemTitle.parent().find('i.fa-compress').toggleClass('fa-expand fa-compress');
|
||||
elemDialog.removeClass('maximized');
|
||||
});
|
||||
|
||||
var params = (scope._viewParams || {}).params || {};
|
||||
if (params['popup.maximized']) {
|
||||
elemButton.click();
|
||||
}
|
||||
});
|
||||
var addCollapseButton = _.once(function () {
|
||||
var elemDialog = element.parent();
|
||||
var elemTitle = elemDialog.find('.ui-dialog-title');
|
||||
$('<a href="#" class="ui-dialog-titlebar-collapse"><i class="fa fa-chevron-up"></i></a>')
|
||||
.click(function (e) {
|
||||
$(this).children('i').toggleClass('fa-chevron-up fa-chevron-down');
|
||||
elemDialog.toggleClass('collapsed');
|
||||
axelor.$adjustSize();
|
||||
return false;
|
||||
}).insertAfter(elemTitle);
|
||||
|
||||
// remove maximized and collapsed states on close
|
||||
element.on('dialogclose', function(e, ui) {
|
||||
elemTitle.parent().find('i.fa-compress').toggleClass('fa-expand fa-compress');
|
||||
elemTitle.parent().find('i.fa-chevron-down').toggleClass('fa-chevron-down fa-chevron-up');
|
||||
elemDialog.removeClass('maximized collapsed');
|
||||
});
|
||||
});
|
||||
|
||||
function doAdjust() {
|
||||
element.dialog('open');
|
||||
element.scrollTop(0);
|
||||
setTimeout(doFocus);
|
||||
if (scope._afterPopupShow) {
|
||||
scope._afterPopupShow();
|
||||
}
|
||||
}
|
||||
|
||||
function doShow() {
|
||||
addMaximizeButton();
|
||||
addCollapseButton();
|
||||
if (loaded) {
|
||||
return setTimeout(doAdjust);
|
||||
}
|
||||
loaded = true;
|
||||
scope.waitForActions(doAdjust);
|
||||
}
|
||||
|
||||
function doFocus() {
|
||||
var container = element.is('[ui-selector-popup]')
|
||||
? element.find('.slick-headerrow')
|
||||
: element;
|
||||
var focusElem = container.find('input:tabbable');
|
||||
if (focusElem.length == 0) {
|
||||
focusElem = element.parent().find('.ui-dialog-buttonset').find(':tabbable');
|
||||
}
|
||||
if (focusElem[0]) {
|
||||
focusElem[0].focus();
|
||||
}
|
||||
|
||||
//XXX: ui-dialog issue
|
||||
element.find('.slick-headerrow-column,.slickgrid,[ui-embedded-editor]').zIndex(element.zIndex());
|
||||
element.find('.record-toolbar .btn').zIndex(element.zIndex()+1);
|
||||
}
|
||||
|
||||
// a flag used by evalScope to detect popup (see form.base.js)
|
||||
scope._isPopup = true;
|
||||
scope._doShow = function(viewPromise) {
|
||||
if (viewPromise && viewPromise.then) {
|
||||
viewPromise.then(doShow);
|
||||
} else {
|
||||
doShow();
|
||||
}
|
||||
};
|
||||
|
||||
scope._setTitle = function (title) {
|
||||
if (title) {
|
||||
element.closest('.ui-dialog').find('.ui-dialog-title').text(title);
|
||||
}
|
||||
};
|
||||
|
||||
scope.adjustSize = function() {
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiEditorPopup', function() {
|
||||
|
||||
return {
|
||||
restrict: 'EA',
|
||||
controller: EditorCtrl,
|
||||
scope: {},
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
scope.onShow = function(viewPromise) {
|
||||
scope._doShow(viewPromise);
|
||||
};
|
||||
|
||||
scope.$watch('schema.title', function popupTitleWatch(title) {
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
element.scroll(function (e) {
|
||||
$(document).trigger('adjust:scroll', element);
|
||||
});
|
||||
|
||||
var onNewHandler = scope.onNewHandler;
|
||||
scope.onNewHandler = function (event) {
|
||||
if (scope.isPopupOpen) {
|
||||
return onNewHandler.apply(scope, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
scope.isPopupOpen = true;
|
||||
setTimeout(function () {
|
||||
var isOpen = false;
|
||||
element.on('dialogclose', function (e) {
|
||||
isOpen = false;
|
||||
scope.waitForActions(function () {
|
||||
scope.isPopupOpen = isOpen;
|
||||
scope.$$popupStack.pop(1);
|
||||
}, 2000); // delay couple of seconds to that popup can cleanup
|
||||
});
|
||||
element.on('dialogopen', function (e) {
|
||||
scope.isPopupOpen = isOpen = true;
|
||||
scope.$$popupStack.push(1);
|
||||
scope.$applyAsync();
|
||||
});
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-on-ok="onOK" x-on-before-close="onBeforeClose" ui-watch-if="isPopupOpen">'+
|
||||
'<div ui-view-form x-handler="this"></div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
ui.directive('uiSelectorPopup', function(){
|
||||
|
||||
return {
|
||||
restrict: 'EA',
|
||||
controller: SelectorCtrl,
|
||||
scope: {
|
||||
selectMode: "@"
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var onShow = scope.onShow;
|
||||
scope.onShow = function (viewPromise) {
|
||||
if (scope.clearFilters) {
|
||||
scope.clearFilters();
|
||||
scope.selection = [];
|
||||
}
|
||||
onShow(viewPromise);
|
||||
scope._doShow(viewPromise);
|
||||
};
|
||||
|
||||
scope.$watch('schema.title', function popupTitleWatch(title){
|
||||
scope._setTitle(title);
|
||||
});
|
||||
|
||||
var btnOK = null;
|
||||
|
||||
function buttonState(count) {
|
||||
if (btnOK === null) {
|
||||
btnOK = element.siblings('.ui-dialog-buttonpane').find('.btn:last');
|
||||
}
|
||||
return btnOK.attr('disabled', !count || count <= 0);
|
||||
}
|
||||
|
||||
scope.$watch('selection.length', buttonState);
|
||||
|
||||
setTimeout(function(){
|
||||
var footer = element.closest('.ui-dialog').find('.ui-dialog-buttonpane'),
|
||||
header = element.closest('.ui-dialog').find('.ui-dialog-titlebar'),
|
||||
pager = element.find('.record-pager'),
|
||||
buttons = element.find('.ui-dialog-buttonset-left');
|
||||
header.find('.ui-dialog-title').after(pager);
|
||||
footer.prepend(buttons);
|
||||
footer.find('.button-ok').html(_t("Select"));
|
||||
});
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div ui-dialog ui-dialog-size x-on-ok="onOK">'+
|
||||
'<div ui-view-grid x-view="schema" x-data-view="dataView" x-handler="this" x-editable="false" x-selector="{{selectMode}}"></div>'+
|
||||
'<div ui-record-pager></div>'+
|
||||
'<div class="ui-dialog-buttonset-left pull-left" ng-show="canNew()">'+
|
||||
'<button class="btn" ng-click="onCreate()" x-translate>Create</button>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
391
sophal/js/view/view.portal.js
Normal file
391
sophal/js/view/view.portal.js
Normal file
@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
PortalCtrl.$inject = ['$scope', '$element'];
|
||||
function PortalCtrl($scope, $element) {
|
||||
|
||||
var view = $scope._views.portal;
|
||||
if (view.items) {
|
||||
$scope.$timeout(function () {
|
||||
$scope.parse(view);
|
||||
});
|
||||
} else {
|
||||
$scope.loadView('portal', view.name).success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$applyAsync(function(){
|
||||
if (view.deferred)
|
||||
view.deferred.resolve($scope);
|
||||
});
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
};
|
||||
|
||||
$scope.show = function(promise) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.getContext = function() {
|
||||
return _.extend({}, $scope._context);
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: 'portal',
|
||||
args: []
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
if (!$scope.isNested) {
|
||||
$scope.updateRoute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var tmplPortlet =
|
||||
'<div ui-view-portlet '+
|
||||
'x-action="{{p.action}}" '+
|
||||
'x-can-search="{{p.canSearch}}" '+
|
||||
'x-col-span="{{p.colSpan}}" '+
|
||||
'x-row-span="{{p.rowSpan}}" ' +
|
||||
'x-height="{{p.height}}"></div>';
|
||||
|
||||
var tmplTabs =
|
||||
"<div ui-portal-tabs x-schema='p'></div>";
|
||||
|
||||
ui.directive('uiViewPortal', ['$compile', function($compile) {
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
controller: PortalCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
function init() {
|
||||
element.sortable({
|
||||
handle: ".portlet-header",
|
||||
items: "> .portlet, > .portal-tabs",
|
||||
forceHelperSize: true,
|
||||
forcePlaceholderSizeType: true,
|
||||
activate2: function(event, ui) {
|
||||
var width = ui.placeholder.width();
|
||||
var height = ui.placeholder.height();
|
||||
|
||||
ui.placeholder.width(width - 4);
|
||||
ui.placeholder.height(height - 4);
|
||||
|
||||
ui.placeholder.css({
|
||||
'left': '2px',
|
||||
'top': '2px',
|
||||
'margin-right': '4px'
|
||||
});
|
||||
},
|
||||
deactivate: function(event, ui) {
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.parse = function(schema) {
|
||||
scope.portletCols = schema.cols || 2;
|
||||
scope.portlets = schema.items;
|
||||
|
||||
_.each(scope.portlets, function (item) {
|
||||
var tmpl = item.type === 'tabs' ? tmplTabs : tmplPortlet;
|
||||
var child = scope.$new();
|
||||
child.p = item;
|
||||
|
||||
var elem = $compile(tmpl)(child);
|
||||
|
||||
element.append(elem);
|
||||
});
|
||||
|
||||
setTimeout(init);
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: '<div class="portal" ng-transclude></div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
PortletCtrl.$inject = ['$scope', '$element', 'MenuService', 'DataSource', 'ViewService'];
|
||||
function PortletCtrl($scope, $element, MenuService, DataSource, ViewService) {
|
||||
|
||||
var self = this;
|
||||
|
||||
function init() {
|
||||
|
||||
ui.ViewCtrl.call(self, $scope, DataSource, ViewService);
|
||||
|
||||
$scope.show = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
$scope.initPortlet = function(action, options) {
|
||||
|
||||
MenuService.action(action, options).success(function(result){
|
||||
if (_.isEmpty(result.data)) {
|
||||
return;
|
||||
}
|
||||
var view = result.data[0].view;
|
||||
|
||||
$scope._viewParams = view;
|
||||
$scope._viewAction = action;
|
||||
|
||||
init();
|
||||
|
||||
$scope.title = view.title;
|
||||
$scope.parsePortlet(view);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('on:attrs-change:refresh', function(e) {
|
||||
e.preventDefault();
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('on:tab-reload', function(e) {
|
||||
if ($scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setPortletSize(scope, element, attrs) {
|
||||
var cols = scope.portletCols;
|
||||
var colSpan = +attrs.colSpan || 1;
|
||||
var rowSpan = +attrs.rowSpan || 1;
|
||||
|
||||
var width = 100;
|
||||
var height = (+attrs.height || 250) * rowSpan;
|
||||
|
||||
width = (width / cols) * colSpan;
|
||||
|
||||
element.width(width + '%').height(height);
|
||||
}
|
||||
|
||||
ui.directive('uiViewPortlet', ['$compile', function($compile){
|
||||
return {
|
||||
scope: true,
|
||||
controller: PortletCtrl,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var lazy = true;
|
||||
(function () {
|
||||
var counter = 0;
|
||||
return function checkLoading() {
|
||||
scope.waitForActions(function () {
|
||||
if (counter < 10 && element.parent().is(":hidden")) {
|
||||
counter++;
|
||||
return setTimeout(checkLoading, 100);
|
||||
}
|
||||
|
||||
lazy = !element.parent().is(".portal");
|
||||
|
||||
var unwatch = scope.$watch(function portalVisibleWatch() {
|
||||
var action = attrs.action;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.parent().is(":hidden")) {
|
||||
lazy = true;
|
||||
return;
|
||||
}
|
||||
|
||||
unwatch();
|
||||
unwatch = null;
|
||||
|
||||
var ctx;
|
||||
if (scope.getContext) {
|
||||
ctx = scope.getContext();
|
||||
}
|
||||
scope.initPortlet(action, {
|
||||
context: ctx
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
})()();
|
||||
|
||||
scope.parsePortlet = _.once(function(view) {
|
||||
|
||||
scope.noFilter = attrs.canSearch != "true";
|
||||
|
||||
var template = $compile($('<div ui-portlet-' + view.viewType + '></div>'))(scope);
|
||||
element.find('.portlet-content:first').append(template);
|
||||
|
||||
scope.show();
|
||||
|
||||
if (scope.portletCols) {
|
||||
setPortletSize(scope, element, attrs);
|
||||
}
|
||||
|
||||
// if lazy, load data
|
||||
if (scope.onRefresh && lazy) {
|
||||
scope.onRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
scope.onPortletToggle = function(event) {
|
||||
var e = $(event.target);
|
||||
e.toggleClass('fa-chevron-up fa-chevron-down');
|
||||
element.toggleClass('portlet-minimized');
|
||||
if (e.hasClass('fa-chevron-up')) {
|
||||
axelor.$adjustSize();
|
||||
}
|
||||
};
|
||||
|
||||
scope.doNext = function() {
|
||||
if (this.canNext()) this.onNext();
|
||||
};
|
||||
|
||||
scope.doPrev = function() {
|
||||
if (this.canPrev()) this.onPrev();
|
||||
};
|
||||
},
|
||||
replace: true,
|
||||
template:
|
||||
'<div class="portlet">'+
|
||||
'<div class="portlet-body stackbar">'+
|
||||
'<div class="portlet-header navbar">'+
|
||||
'<div class="navbar-inner">'+
|
||||
'<div class="container-fluid">'+
|
||||
'<span class="brand" ng-bind-html="title"></span>'+
|
||||
'<ul class="nav pull-right">'+
|
||||
'<li class="portlet-pager" ng-show="showPager">'+
|
||||
'<span class="portlet-pager-text">{{pagerText()}}</span>'+
|
||||
'<span class="icons-bar">'+
|
||||
'<i ng-click="doPrev()" ng-class="{disabled: !canPrev()}" class="fa fa-step-backward"></i>'+
|
||||
'<i ng-click="doNext()" ng-class="{disabled: !canNext()}" class="fa fa-step-forward"></i>'+
|
||||
'</span>'+
|
||||
'</li>'+
|
||||
'<li class="divider-vertical"></li>'+
|
||||
'<li>'+
|
||||
'<span class="icons-bar">'+
|
||||
'<i title="{{\'Refresh\' | t}}" ng-click="onRefresh()" class="fa fa-refresh"></i>'+
|
||||
'<i title="{{\'Toggle\' | t}}" ng-click="onPortletToggle($event)" class="fa fa-chevron-up"></i>'+
|
||||
'</span>'+
|
||||
'</li>'+
|
||||
'</ul>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<div class="portlet-content"></div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
ui.directive('uiPortalTabs', function() {
|
||||
return {
|
||||
scope: {
|
||||
schema: '='
|
||||
},
|
||||
replace: true,
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var schema = scope.schema;
|
||||
|
||||
var first = _.first(schema.tabs);
|
||||
if (first) {
|
||||
first.active = true;
|
||||
}
|
||||
|
||||
scope.tabClick = function (tab) {
|
||||
_.each(schema.tabs, function (item) {
|
||||
item.active = false;
|
||||
});
|
||||
tab.active = true;
|
||||
axelor.$adjustSize();
|
||||
};
|
||||
|
||||
scope.tabs = schema.tabs;
|
||||
scope.portletCols = scope.$parent.portletCols;
|
||||
|
||||
setPortletSize(scope, element, {
|
||||
colSpan: schema.colSpan,
|
||||
rowSpan: schema.rowSpan,
|
||||
height: schema.height
|
||||
});
|
||||
|
||||
element.height('auto');
|
||||
},
|
||||
template:
|
||||
"<div class='tabbable-tabs portal-tabs'>" +
|
||||
"<ul class='nav nav-tabs nav-tabs-scrollable'>" +
|
||||
"<li ng-repeat='tab in tabs' ng-class='{active: tab.active}'>" +
|
||||
"<a href='' ng-click='tabClick(tab)' >{{tab.title}}</a>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"<div class='tab-content portal-tab-content'>" +
|
||||
"<div ng-repeat='tab in tabs' ng-class='{active: tab.active}' class='tab-pane'>" +
|
||||
"<div ui-portal-tab x-schema='tab'></div>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
ui.directive('uiPortalTab', function() {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
schema: '='
|
||||
},
|
||||
controller: ['$scope', 'DataSource', 'ViewService', function ($scope, DataSource, ViewService) {
|
||||
|
||||
var view = $scope.schema;
|
||||
var params = {
|
||||
viewType: 'portal',
|
||||
views: [ view ]
|
||||
};
|
||||
|
||||
view.type = 'portal';
|
||||
|
||||
$scope._viewParams = params;
|
||||
$scope.isNested = true;
|
||||
$scope._model = null;
|
||||
|
||||
ui.ViewCtrl.apply(this, arguments);
|
||||
}],
|
||||
template: "<div ui-view-portal></div>"
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
856
sophal/js/view/view.search.js
Normal file
856
sophal/js/view/view.search.js
Normal file
@ -0,0 +1,856 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.controller('SearchViewCtrl', SearchViewCtrl);
|
||||
|
||||
SearchViewCtrl.$inject = ['$scope', '$element', '$http', 'DataSource', 'ViewService', 'MenuService'];
|
||||
function SearchViewCtrl($scope, $element, $http, DataSource, ViewService, MenuService) {
|
||||
|
||||
var view = $scope._views.search || {};
|
||||
|
||||
$scope._dataSource = DataSource.create('multi-search');
|
||||
|
||||
$scope.$applyAsync(function(){
|
||||
if (view.deferred)
|
||||
view.deferred.resolve($scope);
|
||||
});
|
||||
|
||||
function fixFields(fields) {
|
||||
_.each(fields, function(field){
|
||||
if (field.type == 'reference') {
|
||||
field.type = 'MANY_TO_ONE';
|
||||
field.canNew = false;
|
||||
field.canEdit = false;
|
||||
}
|
||||
|
||||
if (field.type)
|
||||
field.type = field.type.toUpperCase();
|
||||
else
|
||||
field.type = 'STRING';
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
$scope.show = function(viewPromise) {
|
||||
if (!viewPromise) {
|
||||
viewPromise = $scope.loadView('search', view.name);
|
||||
viewPromise.success(function(fields, schema){
|
||||
$scope.initView(schema);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.onShow(viewPromise);
|
||||
};
|
||||
|
||||
$scope.onShow = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.initView = function(schema) {
|
||||
|
||||
var params = $scope._viewParams;
|
||||
|
||||
$scope._searchFields = fixFields(schema.searchFields);
|
||||
$scope._resultFields = fixFields(schema.resultFields);
|
||||
|
||||
$scope._searchView = schema;
|
||||
$scope._showSingle = params.params && params.params.showSingle;
|
||||
$scope._forceEdit = params.params && params.params.forceEdit;
|
||||
$scope._hideActions = params.params && params.params.hideActions;
|
||||
|
||||
$scope.updateRoute();
|
||||
|
||||
if (params.options && params.options.mode == "search") {
|
||||
$scope.setRouteOptions(params.options);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
var args = [],
|
||||
query = $scope._routeSearch;
|
||||
|
||||
return {
|
||||
mode: 'search',
|
||||
args: args,
|
||||
query: query
|
||||
};
|
||||
};
|
||||
|
||||
$scope._routeSearch = null;
|
||||
var onNewCalled = false;
|
||||
$scope.setRouteOptions = function(options) {
|
||||
var opts = options || {},
|
||||
fields = $scope._searchFields || [],
|
||||
search = opts.search,
|
||||
record = {};
|
||||
|
||||
var changed = !angular.equals($scope._routeSearch, search);
|
||||
|
||||
$scope._routeSearch = search;
|
||||
|
||||
if (!onNewCalled && _.isEmpty(search)) {
|
||||
onNewCalled = true;
|
||||
scopes.form.$broadcast('on:new');
|
||||
}
|
||||
if (!search || _.isEmpty(search) || !changed) {
|
||||
return $scope.updateRoute();
|
||||
}
|
||||
|
||||
_.each(fields, function(field) {
|
||||
var value = search[field.name];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (field.target) {
|
||||
if (value) {
|
||||
record[field.name] = {id: +value};
|
||||
}
|
||||
} else {
|
||||
record[field.name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (search.objects) {
|
||||
scopes.toolbar.editRecord({
|
||||
objectSelect: search.objects
|
||||
});
|
||||
}
|
||||
|
||||
scopes.form.editSearch(record, fields);
|
||||
|
||||
function _doSearch() {
|
||||
var promise = $scope.doSearch();
|
||||
if (promise && promise.then && $scope._showSingle) {
|
||||
promise.then(function () {
|
||||
var items = scopes.grid.getItems();
|
||||
if (items && items.length === 1) {
|
||||
scopes.grid.selection = [0];
|
||||
scopes.grid.onEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var promise = scopes.toolbar._viewPromise;
|
||||
if (promise && promise.then) {
|
||||
promise.then(function() {
|
||||
$scope.$timeout(_doSearch);
|
||||
});
|
||||
} else {
|
||||
_doSearch();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var scopes = {};
|
||||
$scope._register = function(key, scope) {
|
||||
scopes[key] = scope;
|
||||
};
|
||||
|
||||
$scope.doSearch = function() {
|
||||
var params = _.extend({}, scopes.form.record),
|
||||
empty = _.chain(params).values().compact().isEmpty().value();
|
||||
if (empty)
|
||||
return $scope.doClear();
|
||||
|
||||
var selected = (scopes.toolbar.record || {}).objectSelect;
|
||||
|
||||
_.extend(params,{
|
||||
__name: view.name,
|
||||
__selected: _.isEmpty(selected) ? null : selected.split(/,\s*/)
|
||||
});
|
||||
|
||||
var promise = $http.post('ws/search', {
|
||||
limit: view.limit || 80,
|
||||
data: params
|
||||
});
|
||||
|
||||
return promise.then(function(response){
|
||||
var res = response.data,
|
||||
records = res.data || [];
|
||||
|
||||
// slickgrid expects unique `id` so generate them and store original one
|
||||
_.each(records, function(rec, i){
|
||||
rec._id = rec.id;
|
||||
rec.id = i + 1;
|
||||
});
|
||||
|
||||
scopes.grid.setItems(records);
|
||||
|
||||
if (scopes.form.$events.onLoad) {
|
||||
scopes.form.record._count = records.length;
|
||||
scopes.form.record._countByModels = _.countBy(records, function(rec) {
|
||||
return rec._model;
|
||||
});
|
||||
scopes.form.$events.onLoad();
|
||||
}
|
||||
|
||||
if (_.isEmpty(records)) {
|
||||
axelor.notify.info(_t("No records found."));
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.doClear = function(all) {
|
||||
scopes.form.edit(null);
|
||||
scopes.form.$broadcast('on:new');
|
||||
scopes.grid.setItems([]);
|
||||
if (all) {
|
||||
scopes.toolbar.edit(null);
|
||||
scopes.toolbar.doReset();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.doAction = function() {
|
||||
|
||||
var action = scopes.toolbar.getMenuAction();
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
var grid = scopes.grid,
|
||||
index = _.first(grid.selection),
|
||||
record = grid.getItem(index);
|
||||
|
||||
action = action.action;
|
||||
record = _.extend({
|
||||
_action: action
|
||||
}, record);
|
||||
|
||||
record.id = record._id;
|
||||
|
||||
MenuService.action(action).success(function(result){
|
||||
|
||||
if (!result.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
var view = result.data[0].view;
|
||||
var tab = view;
|
||||
|
||||
tab.action = _.uniqueId('$act');
|
||||
tab.viewType = 'form';
|
||||
|
||||
tab.context = _.extend({}, tab.context, {
|
||||
_ref : record
|
||||
});
|
||||
|
||||
$scope.openTab(tab);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
ui.controller('SearchFormCtrl', SearchFormCtrl);
|
||||
|
||||
SearchFormCtrl.$inject = ['$scope', '$element', 'ViewService'];
|
||||
function SearchFormCtrl($scope, $element, ViewService) {
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
$scope._register('form', $scope);
|
||||
$scope.setEditable();
|
||||
|
||||
// prevent requesting defaults
|
||||
$scope.defaultValues = {};
|
||||
|
||||
$scope.$watch('_searchView', function searchSchemaWatch(schema) {
|
||||
if (!schema) return;
|
||||
var form = {
|
||||
title: 'Search',
|
||||
type: 'form',
|
||||
cols: 1,
|
||||
items: [{
|
||||
type: 'panel',
|
||||
title: schema.title,
|
||||
items: schema.searchFields
|
||||
}]
|
||||
};
|
||||
|
||||
var meta = { fields: schema.searchFields };
|
||||
ViewService.process(meta, schema.searchForm);
|
||||
|
||||
function process(item) {
|
||||
if (item.items || item.pages) {
|
||||
return _.each(item.items || item.pages, process);
|
||||
}
|
||||
switch (item.widget) {
|
||||
case 'ManyToOne':
|
||||
case 'OneToOne':
|
||||
case 'SuggestBox':
|
||||
item.canNew = false;
|
||||
item.canEdit = false;
|
||||
break;
|
||||
case 'OneToMany':
|
||||
case 'ManyToMany':
|
||||
case 'MasterDetail':
|
||||
item.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.searchForm && schema.searchForm.items) {
|
||||
_.each(schema.searchForm.items, process);
|
||||
}
|
||||
|
||||
$scope.fields = meta.fields;
|
||||
$scope.schema = schema.searchForm || form;
|
||||
$scope.schema.loaded = true;
|
||||
});
|
||||
|
||||
var model = null;
|
||||
var getContext = $scope.getContext;
|
||||
|
||||
$scope.getContext = function() {
|
||||
var view = $scope._searchView || {};
|
||||
if (model === null && view.selects) {
|
||||
model = (_.first(view.selects) || {}).model;
|
||||
}
|
||||
|
||||
var ctx = getContext.apply(this, arguments) || {};
|
||||
ctx._model = model;
|
||||
return ctx;
|
||||
};
|
||||
|
||||
$scope.editSearch = function (record, fields) {
|
||||
$scope.editRecord(record);
|
||||
setTimeout(function () {
|
||||
_.each(fields, function (field) {
|
||||
if (!field.target || !$scope.record) return;
|
||||
var item = $element.find('[x-field=' + field.name + ']');
|
||||
var itemScope = item.data('$scope');
|
||||
var value = itemScope.getValue();
|
||||
if (value && itemScope && !itemScope.text && itemScope.select) {
|
||||
itemScope.select(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
ui.controller('SearchGridCtrl', SearchGridCtrl);
|
||||
|
||||
SearchGridCtrl.$inject = ['$scope', '$element', 'ViewService', '$interpolate'];
|
||||
function SearchGridCtrl($scope, $element, ViewService, $interpolate) {
|
||||
|
||||
ui.GridViewCtrl.call(this, $scope, $element);
|
||||
$scope._register('grid', $scope);
|
||||
|
||||
var viewTitles = {};
|
||||
|
||||
$scope.$watch('_searchView', function searchSchemaWatch(schema) {
|
||||
if (!schema) return;
|
||||
var view = {
|
||||
title: 'Search',
|
||||
type: 'grid',
|
||||
editIcon: true,
|
||||
items: []
|
||||
};
|
||||
|
||||
var objItem = _.findWhere(schema.resultFields, {name: 'object'});
|
||||
if (!objItem) {
|
||||
view.items.push(objItem = {});
|
||||
}
|
||||
|
||||
objItem = _.extend(objItem, { name : '_modelTitle', title: _t('Object') });
|
||||
view.items = view.items.concat(schema.resultFields);
|
||||
|
||||
if (+(objItem.width) === 0) {
|
||||
objItem.hidden = true;
|
||||
}
|
||||
|
||||
var meta = { fields: schema.resultFields };
|
||||
ViewService.process(meta);
|
||||
|
||||
_.each(schema.selects, function (select) {
|
||||
viewTitles[select.model] = select.viewTitle;
|
||||
});
|
||||
|
||||
_.each(view.items, function (item) {
|
||||
if (item.width) {
|
||||
objItem.width = objItem.width || 220;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.fields = meta.fields;
|
||||
$scope.schema = view;
|
||||
$scope.schema.loaded = true;
|
||||
});
|
||||
|
||||
$scope.onEdit = function(force) {
|
||||
|
||||
var index = _.first(this.selection),
|
||||
records = this.getItems(),
|
||||
record = this.getItem(index),
|
||||
ids, domain, views;
|
||||
|
||||
ids = _.chain(records).filter(function(rec){
|
||||
return rec._model == record._model;
|
||||
}).pluck('_id').value();
|
||||
|
||||
domain = "self.id IN (" + ids.join(',') + ")";
|
||||
|
||||
views = _.map(['form', 'grid'], function(type){
|
||||
var view = { type : type };
|
||||
var name = record["_" + type];
|
||||
if (name) view.name = name;
|
||||
return view;
|
||||
});
|
||||
|
||||
if (force === undefined) {
|
||||
force = $scope._forceEdit;
|
||||
}
|
||||
|
||||
var title = viewTitles[record._model];
|
||||
if (title) {
|
||||
title = $interpolate(title)(record);
|
||||
}
|
||||
|
||||
var tab = {
|
||||
action: _.uniqueId('$act'),
|
||||
model: record._model,
|
||||
title: title || record._modelTitle,
|
||||
forceTitle: true,
|
||||
domain: domain,
|
||||
recordId: record._id,
|
||||
forceEdit: force,
|
||||
viewType: 'form',
|
||||
views: views
|
||||
};
|
||||
|
||||
this.openTab(tab);
|
||||
};
|
||||
|
||||
$scope.onSort = function(event, args) {
|
||||
var grid = args.grid;
|
||||
var data = grid.getData();
|
||||
var sortCols = args.sortCols;
|
||||
|
||||
var types = {};
|
||||
|
||||
_.each($scope.fields, function (field) {
|
||||
types[field.name] = field.type;
|
||||
});
|
||||
|
||||
data.sort(function(dataRow1, dataRow2) {
|
||||
for (var i = 0, l = sortCols.length; i < l; i++) {
|
||||
var name = sortCols[i].sortCol.field;
|
||||
var sign = sortCols[i].sortAsc ? 1 : -1;
|
||||
var value1 = dataRow1[name], value2 = dataRow2[name];
|
||||
|
||||
switch (types[name]) {
|
||||
case "integer":
|
||||
case "long":
|
||||
value1 = value1 || 0;
|
||||
value2 = value2 || 0;
|
||||
break;
|
||||
default:
|
||||
value1 = value1 || "";
|
||||
value2 = value2 || "";
|
||||
}
|
||||
|
||||
var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign;
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
grid.invalidate();
|
||||
grid.render();
|
||||
};
|
||||
}
|
||||
|
||||
ui.controller('SearchToolbarCtrl', SearchToolbarCtrl);
|
||||
|
||||
SearchToolbarCtrl.$inject = ['$scope', '$element', '$http'];
|
||||
function SearchToolbarCtrl($scope, $element, $http) {
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
$scope._register('toolbar', $scope);
|
||||
$scope.setEditable();
|
||||
|
||||
var menus = {};
|
||||
|
||||
function fetch(key, parent, request, response) {
|
||||
|
||||
if (menus[key]) {
|
||||
return response(menus[key]);
|
||||
}
|
||||
|
||||
var promise = $http.get('ws/search/menu', {
|
||||
params: {
|
||||
parent: parent
|
||||
}
|
||||
});
|
||||
promise.then(function(res){
|
||||
var data = res.data.data;
|
||||
data = _.map(data, function(item){
|
||||
return {
|
||||
value: item.name,
|
||||
action: item.action,
|
||||
label: item.title
|
||||
};
|
||||
});
|
||||
menus[key] = data;
|
||||
response(data);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.fetchRootMenus = function(request, response) {
|
||||
fetch('menuRoot', null, request, response);
|
||||
};
|
||||
|
||||
$scope.fetchSubMenus = function(request, response) {
|
||||
fetch('menuSub', $scope.record.menuRoot, request, response);
|
||||
};
|
||||
|
||||
$scope.fetchItemMenus = function(request, response) {
|
||||
fetch('menuItem', $scope.record.menuSub, request, response);
|
||||
};
|
||||
|
||||
$scope.resetSelector = function(a, b) {
|
||||
_.each(arguments, function(name){
|
||||
if (_.isString(name)) {
|
||||
$scope.record[name] = null;
|
||||
menus[name] = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getMenuAction = function() {
|
||||
return _.find(menus.menuItem, function(item){
|
||||
return item.value === $scope.record.menuItem;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('_searchView', function searchSchemaWatch(schema) {
|
||||
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = [];
|
||||
|
||||
$scope.fields = {
|
||||
'objectSelect' : {
|
||||
type : 'string',
|
||||
placeholder: _t('Search Objects'),
|
||||
multiple : true,
|
||||
selectionList : _.map(schema.selects, function(x) {
|
||||
if (x.selected) {
|
||||
selected.push(x.model);
|
||||
}
|
||||
return {
|
||||
value : x.model,
|
||||
title : x.title
|
||||
};
|
||||
})
|
||||
},
|
||||
'menuRoot' : {
|
||||
type : 'string',
|
||||
placeholder: _t('Action Category'),
|
||||
widget: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchRootMenus',
|
||||
'ng-change': 'resetSelector("menuSub", "menuItem")'
|
||||
}
|
||||
},
|
||||
'menuSub' : {
|
||||
placeholder: _t('Action Sub-Category'),
|
||||
widget: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchSubMenus',
|
||||
'ng-change': 'resetSelector($event, "menuItem")'
|
||||
}
|
||||
},
|
||||
'menuItem' : {
|
||||
placeholder: _t('Action'),
|
||||
widget: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchItemMenus'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var items1 = [{
|
||||
name : 'objectSelect',
|
||||
showTitle : false,
|
||||
colSpan: 8
|
||||
}, {
|
||||
type : 'button',
|
||||
title : _t('Search'),
|
||||
colSpan: 2,
|
||||
attrs: {
|
||||
'ng-click': 'doSearch()'
|
||||
}
|
||||
}, {
|
||||
type : 'button',
|
||||
title : _t('Clear'),
|
||||
colSpan: 2,
|
||||
attrs: {
|
||||
'ng-click': 'doClear(true)'
|
||||
}
|
||||
}];
|
||||
|
||||
var items2 = [{
|
||||
name : 'menuRoot',
|
||||
showTitle : false,
|
||||
colSpan: 3
|
||||
}, {
|
||||
name : 'menuSub',
|
||||
showTitle : false,
|
||||
colSpan: 3
|
||||
}, {
|
||||
name : 'menuItem',
|
||||
showTitle : false,
|
||||
colSpan: 4
|
||||
}, {
|
||||
type : 'button',
|
||||
title : _t('Go'),
|
||||
colSpan: 2,
|
||||
attrs: {
|
||||
'ng-click': 'doAction()'
|
||||
}
|
||||
}];
|
||||
|
||||
var item1 = {
|
||||
type: "panel",
|
||||
colSpan: $scope._hideActions ? 12 : 6,
|
||||
items: items1
|
||||
};
|
||||
|
||||
var item2 = {
|
||||
type: "panel",
|
||||
colSpan: 6,
|
||||
items: items2
|
||||
};
|
||||
|
||||
schema = {
|
||||
type : 'form',
|
||||
items: [{
|
||||
type: 'panel',
|
||||
items: $scope._hideActions ? [item1] : [item1, item2]
|
||||
}]
|
||||
};
|
||||
|
||||
$scope.schema = schema;
|
||||
$scope.schema.loaded = true;
|
||||
|
||||
$scope.doReset = function () {
|
||||
var record = $scope.record || {};
|
||||
if (selected.length > 0 && _.isEmpty(record.objectSelect)) {
|
||||
record.objectSelect = selected.join(', ');
|
||||
$scope.edit(record);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$timeout($scope.doReset);
|
||||
});
|
||||
}
|
||||
|
||||
angular.module('axelor.ui').directive('uiViewSearch', function(){
|
||||
return {
|
||||
controller: SearchViewCtrl,
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
element.on('keypress', '.search-view-form form:first', function(event){
|
||||
if (event.keyCode == 13 && $(event.target).is('input')){
|
||||
scope.doSearch();
|
||||
}
|
||||
});
|
||||
|
||||
var grid = element.children('.search-view-grid');
|
||||
scope.$onAdjust(function(){
|
||||
if (!element.is(':visible'))
|
||||
return;
|
||||
grid.height(element.height() - grid.position().top);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ActionSelector (TODO: re-use search view toolbar)
|
||||
|
||||
ActionSelectorCtrl.$inject = ['$scope', '$element', '$attrs', '$http', 'MenuService'];
|
||||
function ActionSelectorCtrl($scope, $element, $attrs, $http, MenuService) {
|
||||
|
||||
ui.FormViewCtrl.call(this, $scope, $element);
|
||||
var menus = {},
|
||||
category = $attrs.category;
|
||||
|
||||
function fetch(key, request, response, params) {
|
||||
|
||||
if (menus[key]) {
|
||||
return response(menus[key]);
|
||||
}
|
||||
|
||||
var promise = $http.get('ws/search/menu', {
|
||||
params: params
|
||||
});
|
||||
promise.then(function(res){
|
||||
var data = res.data.data;
|
||||
data = _.map(data, function(item){
|
||||
return {
|
||||
value: item.name,
|
||||
action: item.action,
|
||||
label: item.title
|
||||
};
|
||||
});
|
||||
menus[key] = data;
|
||||
response(data);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.fetchRootMenus = function(request, response) {
|
||||
fetch('$menuRoot', request, response, {
|
||||
parent: '',
|
||||
category: category
|
||||
});
|
||||
};
|
||||
|
||||
$scope.fetchSubMenus = function(request, response) {
|
||||
if (!$scope.record.$menuRoot) return;
|
||||
fetch('$menuSub', request, response, {
|
||||
parent: $scope.record.$menuRoot
|
||||
});
|
||||
};
|
||||
|
||||
$scope.fetchItemMenus = function(request, response) {
|
||||
if (!$scope.record.$menuSub) return;
|
||||
fetch('$menuItem', request, response, {
|
||||
parent: $scope.record.$menuSub
|
||||
});
|
||||
};
|
||||
|
||||
$scope.resetSelector = function(a, b) {
|
||||
_.each(arguments, function(name){
|
||||
if (_.isString(name)) {
|
||||
$scope.record[name] = null;
|
||||
menus[name] = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getMenuAction = function() {
|
||||
return _.find(menus.$menuItem, function(item){
|
||||
return item.value === $scope.record.$menuItem;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doAction = function() {
|
||||
|
||||
var action = $scope.getMenuAction();
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
var context = $scope.$parent.getContext(),
|
||||
record;
|
||||
|
||||
action = action.action;
|
||||
record = {
|
||||
id : context.id,
|
||||
_action: action,
|
||||
_model: context._model
|
||||
};
|
||||
|
||||
MenuService.action(action).success(function(result){
|
||||
|
||||
if (!result.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
var view = result.data[0].view;
|
||||
var tab = view;
|
||||
|
||||
tab.action = _.uniqueId('$act');
|
||||
tab.viewType = 'form';
|
||||
|
||||
tab.context = _.extend({}, tab.context, {
|
||||
_ref : record
|
||||
});
|
||||
|
||||
$scope.openTab(tab);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.fields = {
|
||||
'$menuRoot' : {
|
||||
type : 'string',
|
||||
placeholder: _t('Action Category'),
|
||||
widget: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchRootMenus',
|
||||
'ng-change': 'resetSelector("$menuSub", "$menuItem")'
|
||||
}
|
||||
},
|
||||
'$menuSub' : {
|
||||
placeholder: _t('Action Sub-Category'),
|
||||
type: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchSubMenus',
|
||||
'ng-change': 'resetSelector($event, "$menuItem")'
|
||||
}
|
||||
},
|
||||
'$menuItem' : {
|
||||
placeholder: _t('Action'),
|
||||
widget: 'select-query',
|
||||
attrs: {
|
||||
query: 'fetchItemMenus'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.schema = {
|
||||
cols : 4,
|
||||
colWidths : '30%,30%,30%,10%',
|
||||
type : 'form',
|
||||
items : [ {
|
||||
name : '$menuRoot',
|
||||
showTitle : false
|
||||
}, {
|
||||
name : '$menuSub',
|
||||
showTitle : false
|
||||
}, {
|
||||
name : '$menuItem',
|
||||
showTitle : false
|
||||
}, {
|
||||
type : 'button',
|
||||
title : _t('Go'),
|
||||
attrs: {
|
||||
'ng-click': 'doAction()'
|
||||
}
|
||||
} ]
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('axelor.ui').directive('uiActionSelector', function(){
|
||||
return {
|
||||
scope: true,
|
||||
controller: ActionSelectorCtrl,
|
||||
template: '<div ui-view-form x-handler="this"></div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
795
sophal/js/view/view.tree.js
Normal file
795
sophal/js/view/view.tree.js
Normal file
@ -0,0 +1,795 @@
|
||||
/*
|
||||
* Axelor Business Solutions
|
||||
*
|
||||
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
var ui = angular.module('axelor.ui');
|
||||
|
||||
ui.controller('TreeViewCtrl', TreeViewCtrl);
|
||||
|
||||
TreeViewCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
|
||||
function TreeViewCtrl($scope, $element, DataSource, ActionService) {
|
||||
|
||||
var view = $scope._views.tree;
|
||||
var viewPromise = $scope.loadView('tree', view.name);
|
||||
|
||||
$scope.$applyAsync(function() {
|
||||
if (view.deferred) {
|
||||
view.deferred.resolve($scope);
|
||||
}
|
||||
});
|
||||
|
||||
viewPromise.success(function(fields, schema){
|
||||
$scope.parse(schema);
|
||||
});
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onShow = function(promise) {
|
||||
|
||||
};
|
||||
|
||||
$scope.getRouteOptions = function() {
|
||||
return {
|
||||
mode: "tree"
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setRouteOptions = function(options) {
|
||||
$scope.updateRoute();
|
||||
};
|
||||
|
||||
$scope.onRefresh = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.onSort = function(column) {
|
||||
if (column) {
|
||||
column.sort = true;
|
||||
column.desc = column.desc !== undefined && !column.desc;
|
||||
column.sortCss = column.desc ? "slick-sort-indicator-desc" : "slick-sort-indicator-asc";
|
||||
}
|
||||
$scope.onRefresh();
|
||||
};
|
||||
|
||||
var first = null;
|
||||
|
||||
$scope.parse = function(schema) {
|
||||
|
||||
var columns = _.map(schema.columns, function(col) {
|
||||
return new Column($scope, col);
|
||||
});
|
||||
|
||||
var last = null;
|
||||
var draggable = false;
|
||||
|
||||
var loaders = _.map(schema.nodes, function(node) {
|
||||
var loader = new Loader($scope, node, DataSource);
|
||||
if (last) {
|
||||
last.child = loader;
|
||||
}
|
||||
if (loader.draggable) {
|
||||
draggable = true;
|
||||
}
|
||||
return last = loader;
|
||||
});
|
||||
|
||||
$scope.viewTitle = schema.title;
|
||||
|
||||
$scope.columns = columns;
|
||||
$scope.loaders = loaders;
|
||||
$scope.draggable = draggable;
|
||||
|
||||
first = _.first(loaders);
|
||||
|
||||
first.domain = $scope._domain;
|
||||
first.context = $scope._context;
|
||||
|
||||
// recursive tree (parent -> child on same object)
|
||||
if (loaders.length === 2 && first.model === last.model) {
|
||||
last.child = last;
|
||||
$scope._countOn = _.last(schema.nodes).parent;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onNext = function() {
|
||||
return first && first.onNext();
|
||||
};
|
||||
|
||||
$scope.onPrev = function() {
|
||||
return first && first.onPrev();
|
||||
};
|
||||
|
||||
$scope.canNext = function() {
|
||||
return first && first.canNext();
|
||||
};
|
||||
|
||||
$scope.canPrev = function() {
|
||||
return first && first.canPrev();
|
||||
};
|
||||
|
||||
$scope.pagerText = function() {
|
||||
return first ? first.pagerText() : "";
|
||||
};
|
||||
|
||||
$scope.resetPager = function() {
|
||||
if (first) {
|
||||
first.resetPager();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onClick = function(e, options) {
|
||||
|
||||
var loader = options.loader,
|
||||
record = options.record;
|
||||
|
||||
var target = $(e.target);
|
||||
if (target.is('img,i')) {
|
||||
target = target.parent();
|
||||
}
|
||||
if (e.type === 'click' && !target.is('.tree-button')) {
|
||||
return;
|
||||
}
|
||||
var action = target.attr('x-action') || loader.action;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $handler = ActionService.handler($scope.$new(), $(e.currentTarget), {
|
||||
action: action
|
||||
});
|
||||
|
||||
var model = loader.model;
|
||||
var context = record.$record;
|
||||
|
||||
$handler.scope.record = context;
|
||||
$handler.scope.getContext = function() {
|
||||
return _.extend({
|
||||
_model: model
|
||||
}, context);
|
||||
};
|
||||
|
||||
$handler.onClick().then(function(res){
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('on:tab-reload', function(e, tab) {
|
||||
if ($scope === e.targetScope && $scope.onRefresh) {
|
||||
$scope.onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Column controller.
|
||||
*
|
||||
*/
|
||||
function Column(scope, col) {
|
||||
|
||||
this.css = col.type || 'string';
|
||||
this.name = col.name;
|
||||
this.title = col.title || col.autoTitle;
|
||||
|
||||
if (this.title === null || this.title === undefined) {
|
||||
this.title = _.humanize(col.name);
|
||||
}
|
||||
if (col.type == 'button') {
|
||||
this.title = null;
|
||||
}
|
||||
|
||||
this.cellCss = function(record) {
|
||||
return this.css;
|
||||
};
|
||||
|
||||
this.cellText = function(record) {
|
||||
|
||||
if (col.type === 'button') {
|
||||
var template = "---";
|
||||
var item = _.findWhere(record.$node.items, { type: 'button', name: col.name });
|
||||
if (item) {
|
||||
template = "<a href='javascript:' class='tree-button' x-action='"+ item.onClick +"'>";
|
||||
if (item.icon) {
|
||||
if (item.icon.indexOf('fa') === 0) {
|
||||
template += "<i class='fa " + item.icon + "'></i>";
|
||||
} else {
|
||||
template += "<img width='16px' src='"+ item.icon +"'>";
|
||||
}
|
||||
}
|
||||
if (item.title) {
|
||||
template += item.title;
|
||||
}
|
||||
template += "</a>";
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
var value = record[this.name];
|
||||
if (value === undefined || value === null) {
|
||||
return '---';
|
||||
}
|
||||
|
||||
var selection = (record.$selection || {})[this.name];
|
||||
if (selection) {
|
||||
var cmp = col.type === "integer" ? function(a, b) { return a == b ; } : _.isEqual;
|
||||
var res = _.find(selection, function(item){
|
||||
return cmp(item.value, value);
|
||||
}) || {};
|
||||
|
||||
if (col.widget === 'ImageSelect' && res.icon) {
|
||||
var image = "<img style='max-height: 24px;' src='" + (res.icon || res.value) + "'>";
|
||||
if (col.labels === false) {
|
||||
return image;
|
||||
}
|
||||
return image + " " + res.title;
|
||||
}
|
||||
|
||||
return res.title;
|
||||
}
|
||||
var type = col.type;
|
||||
if (type === 'reference') {
|
||||
type = 'many-to-one';
|
||||
}
|
||||
var item = _.findWhere(record.$node.items, { type: 'field', as: col.name });
|
||||
var attrs = _.extend({}, item, col);
|
||||
var fn = ui.formatters[type];
|
||||
if (fn) {
|
||||
value = fn(attrs, value, record);
|
||||
}
|
||||
return value === undefined || value === null ? '---' : value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node loader.
|
||||
*
|
||||
*/
|
||||
function Loader(scope, node, DataSource) {
|
||||
|
||||
var ds = DataSource.create(node.model);
|
||||
var names = _.pluck(node.items, 'name');
|
||||
var domain = null;
|
||||
|
||||
if (node.parent) {
|
||||
domain = "self." + node.parent + ".id = :parentId";
|
||||
ds._page.limit = -1;
|
||||
}
|
||||
|
||||
if (node.domain) {
|
||||
if (domain) {
|
||||
domain = '(' + domain + ') AND (' + node.domain + ')';
|
||||
} else {
|
||||
domain = node.domain;
|
||||
}
|
||||
}
|
||||
|
||||
this.node = node;
|
||||
|
||||
this.child = null;
|
||||
|
||||
this.model = node.model;
|
||||
|
||||
this.action = node.onClick;
|
||||
|
||||
this.draggable = node.draggable;
|
||||
|
||||
this.getDomain = function(context) {
|
||||
var _domain = domain,
|
||||
_context = context;
|
||||
|
||||
if (_domain && this.domain) {
|
||||
_domain = "(" + this.domain + ") AND (" + domain + ")";
|
||||
}
|
||||
|
||||
_domain = _domain || this.domain;
|
||||
_context = _.extend({}, this.context, context);
|
||||
|
||||
return {
|
||||
domain: _domain,
|
||||
context: _context
|
||||
};
|
||||
};
|
||||
|
||||
this.resetPager = function () {
|
||||
ds._page.from = 0;
|
||||
};
|
||||
|
||||
this.load = function(item, callback) {
|
||||
|
||||
var context = _.extend({}, scope._context),
|
||||
current = item && item.$record;
|
||||
|
||||
var sortOn = _.filter(scope.columns, function (col) { return col.sort; });
|
||||
var sortBy = _.map(sortOn, function (col) {
|
||||
var field = _.findWhere(node.items, { as: col.name });
|
||||
if (field) {
|
||||
return col.desc ? '-' + field.name : field.name;
|
||||
}
|
||||
});
|
||||
|
||||
sortBy = _.compact(sortBy).join(',') || node.orderBy;
|
||||
|
||||
if (scope.getContext) {
|
||||
context = _.extend(context, scope.getContext());
|
||||
}
|
||||
if (current) {
|
||||
context.parentId = current.id;
|
||||
}
|
||||
|
||||
if (scope._countOn) {
|
||||
context._countOn = scope._countOn;
|
||||
} else if (this.child) {
|
||||
var child = this.child.node;
|
||||
context._childOn = {
|
||||
model: child.model,
|
||||
parent: child.parent
|
||||
};
|
||||
}
|
||||
|
||||
var opts = _.extend(this.getDomain(context), {
|
||||
fields: names,
|
||||
action: scope._viewAction
|
||||
});
|
||||
|
||||
if (sortBy) {
|
||||
opts.sortBy = sortBy.split(',');
|
||||
}
|
||||
|
||||
var promise = ds.search(opts);
|
||||
|
||||
promise.success(function(records) {
|
||||
if (callback) {
|
||||
callback(accept(item, records));
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
this.move = function(item, callback) {
|
||||
|
||||
var record = item.$record,
|
||||
parent = { id: item.$parentId };
|
||||
|
||||
record[node.parent || scope._countOn] = parent;
|
||||
|
||||
return ds.save(record).success(function(rec) {
|
||||
record.version = rec.version;
|
||||
if (callback) {
|
||||
callback(rec);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var that = this;
|
||||
|
||||
function accept(current, records) {
|
||||
|
||||
var fields = node.items,
|
||||
parent = current && current.$record,
|
||||
child = that.child;
|
||||
|
||||
return _.map(records, function(record) {
|
||||
|
||||
var $id = _.uniqueId('row');
|
||||
var $parent = current ? current.$id : null;
|
||||
|
||||
var item = {
|
||||
'$id': $id,
|
||||
'$model': node.model,
|
||||
'$node': node,
|
||||
'$record': record,
|
||||
'$selection': {},
|
||||
'$parent': $parent,
|
||||
'$parentId': parent && parent.id,
|
||||
'$parentModel': current && current.$model,
|
||||
'$draggable': node.draggable,
|
||||
'$folder': child && (record._children === undefined || record._children > 0)
|
||||
};
|
||||
|
||||
item.$expand = function(callback) {
|
||||
if (child) {
|
||||
return child.load(this, callback);
|
||||
}
|
||||
};
|
||||
|
||||
item.$move = function(callback) {
|
||||
return that.move(this, callback);
|
||||
};
|
||||
|
||||
item.$click = function(e) {
|
||||
if (node.onClick) {
|
||||
scope.onClick(e, {
|
||||
loader: that,
|
||||
record: item,
|
||||
parent: parent
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_.each(fields, function(field) {
|
||||
var name = field.as || field.name;
|
||||
item[name] = record[field.name];
|
||||
item.$selection[name] = field.selectionList;
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
var page = {};
|
||||
|
||||
ds.on('change', function(e, _records, _page) {
|
||||
page = _page;
|
||||
});
|
||||
|
||||
this.canNext = function() {
|
||||
return ds.canNext();
|
||||
};
|
||||
|
||||
this.canPrev = function() {
|
||||
return ds.canPrev();
|
||||
};
|
||||
|
||||
this.onNext = function() {
|
||||
ds.next(names).success(function(records){
|
||||
scope.setRootNodes(accept(null, records));
|
||||
});
|
||||
};
|
||||
|
||||
this.onPrev = function() {
|
||||
ds.prev(names).success(function(records){
|
||||
scope.setRootNodes(accept(null, records));
|
||||
});
|
||||
};
|
||||
|
||||
this.pagerText = function() {
|
||||
if (page && page.from !== undefined) {
|
||||
if (page.total === 0) return null;
|
||||
return _t("{0} to {1} of {2}", page.from + 1, page.to, page.total);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ui.directive('uiViewTree', function(){
|
||||
|
||||
return {
|
||||
|
||||
replace: true,
|
||||
|
||||
link: function(scope, element, attrs) {
|
||||
|
||||
var table = element.find('.tree-table > table');
|
||||
|
||||
table.treetable({
|
||||
|
||||
indent: 16,
|
||||
|
||||
expandable: true,
|
||||
|
||||
clickableNodeNames: true,
|
||||
|
||||
nodeIdAttr: "id",
|
||||
|
||||
parentIdAttr: "parent",
|
||||
|
||||
branchAttr: "folder",
|
||||
|
||||
onNodeCollapse: function onNodeCollapse() {
|
||||
var node = this,
|
||||
row = node.row;
|
||||
|
||||
if (node._state === "collapsed") {
|
||||
return;
|
||||
}
|
||||
node._state = "collapsed";
|
||||
|
||||
table.treetable("collapseNode", row.data("id"));
|
||||
adjustCols();
|
||||
},
|
||||
|
||||
onNodeExpand: function onNodeExpand() {
|
||||
|
||||
var node = this,
|
||||
row = this.row,
|
||||
record = row.data('$record');
|
||||
|
||||
if (node._loading || node._state === "expanded") {
|
||||
return;
|
||||
}
|
||||
|
||||
node._state = "expanded";
|
||||
|
||||
if (node._loaded) {
|
||||
table.treetable("expandNode", row.data("id"));
|
||||
return adjustCols();
|
||||
}
|
||||
|
||||
node._loading = true;
|
||||
|
||||
if (record.$expand) {
|
||||
record.$expand(function(records) {
|
||||
acceptNodes(records, node);
|
||||
node._loading = false;
|
||||
node._loaded = true;
|
||||
adjustCols();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function acceptNodes(records, after) {
|
||||
var rows = _.map(records, makeRow);
|
||||
table.treetable("loadBranch", after, rows);
|
||||
}
|
||||
|
||||
function makeRow(record) {
|
||||
|
||||
var tr = $('<tr>')
|
||||
.attr('data-id', record.$id)
|
||||
.attr('data-parent', record.$parent)
|
||||
.attr('data-folder', record.$folder);
|
||||
|
||||
tr.data('$record', record);
|
||||
|
||||
_.each(scope.columns, function(col) {
|
||||
$('<td>').html(col.cellText(record)).appendTo(tr);
|
||||
});
|
||||
|
||||
if (scope.draggable && (record.$folder || scope._countOn || !record.$parent)) {
|
||||
makeDroppable(tr);
|
||||
}
|
||||
if (record.$draggable || (scope.draggable && scope._countOn)) {
|
||||
makeDraggable(tr);
|
||||
}
|
||||
|
||||
tr.on('click dblclick taphold', function(e) {
|
||||
record.$click(e);
|
||||
});
|
||||
|
||||
return tr[0];
|
||||
}
|
||||
|
||||
function onDrop(e, ui) {
|
||||
/* jshint validthis: true */
|
||||
var row = ui.draggable,
|
||||
record = row.data('$record'),
|
||||
current = $(this).data('$record'),
|
||||
node = table.treetable("node", row.data("id")),
|
||||
nodeParent = node.parentNode();
|
||||
|
||||
table.treetable("move", node.id, $(this).data("id"));
|
||||
|
||||
// make sure to remove expander icon if no children left
|
||||
if (nodeParent && nodeParent.children.length === 0) {
|
||||
nodeParent.row.removeClass('expanded');
|
||||
nodeParent.row.removeClass('branch');
|
||||
nodeParent.row.addClass('leaf');
|
||||
|
||||
nodeParent.treeCell.off('click.treetable');
|
||||
nodeParent.treeCell.off('keydown.treetable');
|
||||
nodeParent.indenter.empty();
|
||||
}
|
||||
|
||||
record.$parentId = current.$record.id;
|
||||
record.$move(function(result) {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function isParent(source, target) {
|
||||
var parent = target.parent().find('[data-id=' + target.data('parent') + ']');
|
||||
if (parent.data('id') === source.data('id')) {
|
||||
return true;
|
||||
}
|
||||
if (parent.length) {
|
||||
return isParent(source, parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeDroppable(row) {
|
||||
|
||||
row.droppable({
|
||||
accept: function(draggable, x) {
|
||||
var source = draggable.data('$record'),
|
||||
target = row.data('$record');
|
||||
|
||||
// don't allow moving parent to child
|
||||
if (scope._countOn) {
|
||||
return !isParent(draggable, $(this));
|
||||
}
|
||||
|
||||
return source && target && target.$model === source.$parentModel;
|
||||
},
|
||||
hoverClass: "accept",
|
||||
drop: onDrop,
|
||||
over: function(e, ui) {
|
||||
var row = ui.draggable;
|
||||
if(this != row[0] && !$(this).is(".expanded")) {
|
||||
table.treetable("expandNode", $(this).data("id"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeDraggable(row) {
|
||||
|
||||
var record = row.data('$record');
|
||||
if (!record.$draggable && !scope._countOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.draggable({
|
||||
helper: function() {
|
||||
return $('<span></span>').append(row.children('td:first').clone());
|
||||
},
|
||||
opacity: 0.75,
|
||||
containment: 'document',
|
||||
refreshPositions: true,
|
||||
revert: "invalid",
|
||||
revertDuration: 300,
|
||||
delay: 300,
|
||||
scroll: true
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
||||
var tree = table.data('treetable');
|
||||
if (tree === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(tree.roots, function(node) {
|
||||
tree.unloadBranch(node);
|
||||
node.row.remove();
|
||||
delete tree.tree[node.id];
|
||||
});
|
||||
|
||||
tree.nodes.length = 0;
|
||||
tree.roots.length = 0;
|
||||
}
|
||||
|
||||
scope.onRefresh = function() {
|
||||
var root = _.first(scope.loaders);
|
||||
if (root) {
|
||||
root.load(null, function(nodes) {
|
||||
scope.setRootNodes(nodes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
scope.setRootNodes = function(nodes) {
|
||||
clear();
|
||||
acceptNodes(nodes);
|
||||
};
|
||||
|
||||
scope.onHeaderClick = function (event, column) {
|
||||
if (!event.shiftKey) {
|
||||
_.each(scope.columns, function (col) {
|
||||
if (col !== column) {
|
||||
col.sort = false;
|
||||
col.desc = undefined;
|
||||
col.sortCss = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
scope.onSort(column);
|
||||
};
|
||||
|
||||
var watcher = scope.$watch('loaders', function treeLoadersWatch(loaders) {
|
||||
|
||||
if (loaders === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
watcher();
|
||||
|
||||
var root = _.first(loaders);
|
||||
if (root) {
|
||||
root.load(null, acceptNodes).then(adjustCols);
|
||||
}
|
||||
});
|
||||
|
||||
var adjustCounter = 0;
|
||||
|
||||
function adjustCols() {
|
||||
|
||||
if (element.is(':hidden')) {
|
||||
if (adjustCounter++ < 10) {
|
||||
_.delay(adjustCols, 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
adjustCounter = 0;
|
||||
|
||||
var tds = table.find('tr:first').find('td');
|
||||
var ths = element.find('.tree-header').find('th');
|
||||
var widths = [];
|
||||
|
||||
if (tds.length !== ths.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
tds.each(function() {
|
||||
widths.push($(this).outerWidth());
|
||||
});
|
||||
|
||||
ths.each(function(i) {
|
||||
$(this).width(widths[i] - 12);
|
||||
});
|
||||
}
|
||||
|
||||
scope.$onAdjust(adjustCols, 100);
|
||||
|
||||
table.on('mousedown.treeview', 'tbody tr', function(e) {
|
||||
table.find('tr.selected').removeClass('selected');
|
||||
$(this).addClass("selected");
|
||||
});
|
||||
},
|
||||
template:
|
||||
'<div class="tree-view-container" ui-attach-scroll="> .tree-table">'+
|
||||
'<table class="tree-header">'+
|
||||
'<thead>'+
|
||||
'<tr>'+
|
||||
'<th ng-repeat="column in columns" ng-class="column.css" ng-click="onHeaderClick($event, column)">' +
|
||||
'<span>{{column.title}}</span>'+
|
||||
'<span ng-if="column.sort" class="slick-sort-indicator" ng-class="column.sortCss"></span>'+
|
||||
'</th>'+
|
||||
'</tr>'+
|
||||
'</thead>'+
|
||||
'</table>'+
|
||||
'<div class="tree-table">'+
|
||||
'<table>'+
|
||||
'<tbody></tbody>'+
|
||||
'</table>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
TreePortletCtrl.$inject = ['$scope', '$element', 'DataSource', 'ActionService'];
|
||||
function TreePortletCtrl($scope, $element, DataSource, ActionService) {
|
||||
|
||||
TreeViewCtrl.call(this, $scope, $element, DataSource, ActionService);
|
||||
|
||||
$scope.showPager = true;
|
||||
|
||||
$scope.$on("on:new", function(e) {
|
||||
$scope.resetPager();
|
||||
$scope.onRefresh();
|
||||
});
|
||||
$scope.$on("on:edit", function(e) {
|
||||
$scope.resetPager();
|
||||
$scope.onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
ui.directive('uiPortletTree', function(){
|
||||
|
||||
return {
|
||||
controller: TreePortletCtrl,
|
||||
template: '<div ui-view-tree></div>'
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user