663 lines
27 KiB
JavaScript
663 lines
27 KiB
JavaScript
/**
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; under version 2
|
|
* of the License (non-upgradable).
|
|
*
|
|
* 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* Copyright (c) 2014-2017 Open Assessment Technologies SA;
|
|
*/
|
|
|
|
/**
|
|
* @author Bertrand Chevrier <bertrand@taotesting.com>
|
|
*/
|
|
define([
|
|
'jquery',
|
|
'i18n',
|
|
'lodash',
|
|
'core/promise',
|
|
'core/request',
|
|
'layout/section',
|
|
'layout/actions/binder',
|
|
'layout/permissions',
|
|
'provider/resources',
|
|
'ui/destination/selector',
|
|
'uri',
|
|
'ui/feedback',
|
|
'ui/dialog/confirm',
|
|
], function($, __, _, Promise, request, section, binder, permissionsManager, resourceProviderFactory, destinationSelectorFactory, uri, feedback, confirmDialog) {
|
|
'use strict';
|
|
|
|
var messages = {
|
|
confirmMove: __('The properties of the source class will be replaced by those of the destination class. This might result in a loss of metadata. Continue anyway?')
|
|
};
|
|
|
|
/**
|
|
* Cleans up the main panel and creates a container
|
|
* @returns {jQuery}
|
|
*/
|
|
var emptyPanel = function cleanupPanel() {
|
|
section.current().updateContentBlock('<div class="main-container flex-container-form-main"></div>');
|
|
return $(section.selected.panel).find('.main-container');
|
|
};
|
|
|
|
/**
|
|
* Register common actions.
|
|
*
|
|
* TODO this common actions may be re-structured, split in different files or moved in a more obvious location.
|
|
*
|
|
* @exports layout/actions/common
|
|
*/
|
|
var commonActions = function commonActions(){
|
|
|
|
/**
|
|
* Register the load action: load the url and into the content container
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} [actionContext.uri]
|
|
* @param {String} [actionContext.classUri]
|
|
*/
|
|
binder.register('load', function load(actionContext){
|
|
section.current().loadContentBlock(this.url, _.pick(actionContext, ['uri', 'classUri', 'id']));
|
|
});
|
|
|
|
/**
|
|
* Register the load class action: load the url into the content container
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} actionContext.classUri - the URI of the parent class
|
|
*/
|
|
binder.register('loadClass', function load(actionContext){
|
|
section.current().loadContentBlock(this.url, {classUri: actionContext.classUri, id: uri.decode(actionContext.classUri)});
|
|
});
|
|
|
|
/**
|
|
* Register the subClass action: creates a sub class
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} actionContext.classUri - the URI of the parent class
|
|
* @returns {Promise<Object>} resolves with the new class data
|
|
*
|
|
* @fires layout/tree#addnode.taotree
|
|
*/
|
|
binder.register('subClass', function subClass(actionContext){
|
|
var classUri = uri.decode(actionContext.classUri);
|
|
var signature = actionContext.signature;
|
|
var self = this;
|
|
if (actionContext.type !== 'class') {
|
|
signature = actionContext.classSignature;
|
|
}
|
|
|
|
const currentSection = section.current();
|
|
if (currentSection.clearContentBlock) {
|
|
currentSection.clearContentBlock();
|
|
}
|
|
|
|
return request({
|
|
url: self.url,
|
|
method: "POST",
|
|
data: {id: classUri, type: 'class', signature: signature},
|
|
dataType: 'json',
|
|
})
|
|
.then(function(response) {
|
|
if (response.success && response.uri) {
|
|
if (actionContext.tree) {
|
|
$(actionContext.tree).trigger('addnode.taotree', [{
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
parent : uri.decode(actionContext.classUri),
|
|
cssClass : 'node-class'
|
|
}]);
|
|
}
|
|
|
|
//return format (resourceSelector)
|
|
return {
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
classUri : uri.decode(actionContext.classUri),
|
|
type : 'class'
|
|
};
|
|
|
|
} else {
|
|
throw new Error(__('Adding the new class has failed'));
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the instanciate action: creates a new instance from a class
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} actionContext.classUri - the URI of the class' instance
|
|
* @returns {Promise<Object>} resolves with the new instance data
|
|
*
|
|
* @fires layout/tree#addnode.taotree
|
|
*/
|
|
binder.register('instanciate', function instanciate(actionContext){
|
|
var self = this;
|
|
var classUri = uri.decode(actionContext.classUri);
|
|
var signature = actionContext.signature;
|
|
if (actionContext.type !== 'class') {
|
|
signature = actionContext.classSignature;
|
|
}
|
|
return request({
|
|
url: self.url,
|
|
method: "POST",
|
|
data: {id: classUri, type: 'instance', signature: signature},
|
|
dataType: 'json'
|
|
})
|
|
.then(function(response) {
|
|
if (response.success && response.uri) {
|
|
//backward compat format for jstree
|
|
if(actionContext.tree){
|
|
$(actionContext.tree).trigger('addnode.taotree', [{
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
parent : uri.decode(actionContext.classUri),
|
|
cssClass : 'node-instance'
|
|
}]);
|
|
}
|
|
|
|
//return format (resourceSelector)
|
|
return {
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
classUri : uri.decode(actionContext.classUri),
|
|
type : 'instance'
|
|
};
|
|
|
|
} else {
|
|
throw new Error(__('Adding the new resource has failed'));
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the duplicateNode action: creates a clone of a node.
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} actionContext.uri - the URI of the base instance
|
|
* @param {String} actionContext.classUri - the URI of the class' instance
|
|
* @returns {Promise<Object>} resolves with the new instance data
|
|
*
|
|
* @fires layout/tree#addnode.taotree
|
|
*/
|
|
binder.register('duplicateNode', function duplicateNode(actionContext){
|
|
var self = this;
|
|
return request({
|
|
url: self.url,
|
|
method: "POST",
|
|
data: {
|
|
uri: actionContext.id,
|
|
classUri: uri.decode(actionContext.classUri),
|
|
signature: actionContext.signature
|
|
},
|
|
dataType: 'json',
|
|
})
|
|
.then(function(response) {
|
|
if (response.success && response.uri) {
|
|
//backward compat format for jstree
|
|
if(actionContext.tree){
|
|
$(actionContext.tree).trigger('addnode.taotree', [{
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
parent : uri.decode(actionContext.classUri),
|
|
cssClass : 'node-instance'
|
|
}]);
|
|
}
|
|
|
|
//return format (resourceSelector)
|
|
return {
|
|
uri : uri.decode(response.uri),
|
|
label : response.label,
|
|
classUri : uri.decode(actionContext.classUri),
|
|
type : 'instance'
|
|
};
|
|
|
|
} else {
|
|
throw new Error(__('Node duplication has failed'));
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the removeNode action: removes a resource.
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} [actionContext.uri]
|
|
* @param {String} [actionContext.classUri]
|
|
*
|
|
* @fires layout/tree#removenode.taotree
|
|
*/
|
|
binder.register('removeNode', function remove(actionContext){
|
|
var self = this;
|
|
var data = {};
|
|
|
|
data.uri = uri.decode(actionContext.uri);
|
|
data.classUri = uri.decode(actionContext.classUri);
|
|
data.id = actionContext.id;
|
|
data.signature = actionContext.signature;
|
|
|
|
return new Promise( function (resolve, reject){
|
|
confirmDialog(__("Please confirm deletion"), function accept(){
|
|
request({
|
|
url: self.url,
|
|
method: "POST",
|
|
data: data,
|
|
dataType: 'json',
|
|
})
|
|
.then(function(response) {
|
|
if (response.success && response.deleted) {
|
|
feedback().success(response.message || __('Resource deleted'));
|
|
|
|
if (actionContext.tree){
|
|
$(actionContext.tree).trigger('removenode.taotree', [{
|
|
id : actionContext.uri || actionContext.classUri
|
|
}]);
|
|
}
|
|
return resolve({
|
|
uri : actionContext.uri || actionContext.classUri
|
|
});
|
|
|
|
} else {
|
|
reject(response.msg || __("Unable to delete the selected resource"));
|
|
}
|
|
});
|
|
}, function cancel(){
|
|
reject({ cancel : true });
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the removeNodes action: removes multiple resources
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object[]|Object} actionContexts - single or multiple action contexts
|
|
* @returns {Promise<String[]>} with the list of deleted ids/uris
|
|
*/
|
|
binder.register('removeNodes', function removeNodes(actionContexts){
|
|
var self = this;
|
|
var confirmMessage = '';
|
|
var data = {};
|
|
var classes;
|
|
var instances;
|
|
|
|
if(!_.isArray(actionContexts)){
|
|
actionContexts = [actionContexts];
|
|
}
|
|
|
|
classes = _.filter(actionContexts, { type : 'class' });
|
|
instances = _.filter(actionContexts, { type : 'instance' });
|
|
|
|
data.ids = _.map(actionContexts, function (elem) {
|
|
return {id: elem.id, signature: elem.signature};
|
|
});
|
|
|
|
if(actionContexts.length === 1){
|
|
confirmMessage = __('Please confirm deletion');
|
|
} else if(actionContexts.length > 1){
|
|
if(instances.length){
|
|
if(instances.length === 1){
|
|
confirmMessage = __('an instance');
|
|
} else {
|
|
confirmMessage = __('%s instances', instances.length);
|
|
}
|
|
}
|
|
if(classes.length){
|
|
if(confirmMessage){
|
|
confirmMessage += __(' and ');
|
|
}
|
|
if(classes.length === 1){
|
|
confirmMessage = __('a class');
|
|
} else {
|
|
confirmMessage += __('%s classes', classes.length);
|
|
}
|
|
}
|
|
confirmMessage = __('Please confirm deletion of %s.', confirmMessage);
|
|
}
|
|
|
|
return new Promise( function (resolve, reject){
|
|
|
|
confirmDialog(confirmMessage, function accept(){
|
|
request({
|
|
url: self.url,
|
|
method: "POST",
|
|
data: data,
|
|
dataType: 'json',
|
|
})
|
|
.then(function(response) {
|
|
if (response.success && response.deleted) {
|
|
resolve(response.deleted);
|
|
} else {
|
|
reject(new Error(response.message || __("Unable to delete the selected resources")));
|
|
}
|
|
});
|
|
}, function cancel(){
|
|
reject({ cancel : true });
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the moveNode action: moves a resource.
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} [actionContext.uri]
|
|
* @param {String} [actionContext.classUri]
|
|
*/
|
|
binder.register('moveNode', function remove(actionContext){
|
|
var data = _.pick(actionContext, ['id', 'uri', 'destinationClassUri', 'confirmed', 'signature']);
|
|
|
|
//wrap into a private function for recusion calls
|
|
var _moveNode = function _moveNode(url){
|
|
request({
|
|
url: url,
|
|
method: "POST",
|
|
data: data,
|
|
dataType: 'json',
|
|
})
|
|
.then(function(response) {
|
|
var message;
|
|
var i;
|
|
|
|
if (response && response.status === true) {
|
|
return;
|
|
} else if (response && response.status === 'diff') {
|
|
message = __("Moving this element will replace the properties of the previous class by those of the destination class :");
|
|
message += "\n";
|
|
for (i = 0; i < response.data.length; i++) {
|
|
if (response.data[i].label) {
|
|
message += `- ${response.data[i].label}\n`;
|
|
}
|
|
}
|
|
message += `${__("Please confirm this operation.")}\n`;
|
|
|
|
// eslint-disable-next-line no-alert
|
|
if (window.confirm(message)) {
|
|
data.confirmed = true;
|
|
return _moveNode(url, data);
|
|
}
|
|
}
|
|
|
|
//ask to rollback the tree
|
|
$(actionContext.tree).trigger('rollback.taotree');
|
|
});
|
|
};
|
|
_moveNode(this.url, data);
|
|
});
|
|
|
|
/**
|
|
* Register the launchEditor action.
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object} actionContext - the current actionContext
|
|
* @param {String} [actionContext.uri]
|
|
* @param {String} [actionContext.classUri]
|
|
*
|
|
* @fires layout/tree#removenode.taotree
|
|
*/
|
|
binder.register('launchEditor', function launchEditor(actionContext){
|
|
|
|
var data = _.pick(actionContext, ['id']);
|
|
var wideDifferenciator = '[data-content-target="wide"]';
|
|
|
|
$.ajax({
|
|
url: this.url,
|
|
type: "GET",
|
|
data: data,
|
|
dataType: 'html',
|
|
success: function(response){
|
|
var $response = $($.parseHTML(response, document, true));
|
|
//check if the editor should be displayed widely or in the content area
|
|
if($response.is(wideDifferenciator) || $response.find(wideDifferenciator).length){
|
|
section.create({
|
|
id : 'authoring',
|
|
name : __('Authoring'),
|
|
url : this.url,
|
|
content : $response,
|
|
visible : false
|
|
})
|
|
.show();
|
|
} else {
|
|
section.updateContentBlock($response);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the copyTo action: select a destination class to copy a resource
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object[]|Object} actionContext - single or multiple action contexts
|
|
* @returns {Promise<String>} with the new resource URI
|
|
*/
|
|
binder.register('copyTo', function copyTo (actionContext){
|
|
//create the container manually...
|
|
var $container = emptyPanel();
|
|
|
|
//get the resource provider configured with the action URL
|
|
var resourceProvider = resourceProviderFactory({
|
|
copyTo : {
|
|
url : this.url
|
|
}
|
|
});
|
|
|
|
return new Promise( function (resolve, reject){
|
|
|
|
//set up a destination selector
|
|
destinationSelectorFactory($container, {
|
|
classUri: actionContext.rootClassUri,
|
|
preventSelection : function preventSelection(nodeUri, node, $node){
|
|
//prevent selection on nodes without WRITE permissions
|
|
if( $node.length && $node.data('access') === 'partial' || $node.data('access') === 'denied'){
|
|
if(! permissionsManager.hasPermission(nodeUri, 'WRITE') ) {
|
|
feedback().warning(__('You are not allowed to write in the class %s', node.label));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
})
|
|
.on('query', function(params) {
|
|
var self = this;
|
|
|
|
//asks only classes
|
|
params.classOnly = true;
|
|
resourceProvider
|
|
.getResources(params, true)
|
|
.then(function(resources){
|
|
//ask the server the resources from the component query
|
|
self.update(resources, params);
|
|
})
|
|
.catch(function(err){
|
|
self.trigger('error', err);
|
|
});
|
|
})
|
|
.on('select', function(destinationClassUri){
|
|
var self = this;
|
|
if(!_.isEmpty(destinationClassUri)){
|
|
this.disable();
|
|
|
|
resourceProvider
|
|
.copyTo(actionContext.id, destinationClassUri, actionContext.signature)
|
|
.then(function(result){
|
|
if(result && result.uri){
|
|
|
|
feedback().success(__('Resource copied'));
|
|
|
|
//backward compatible for jstree
|
|
if(actionContext.tree){
|
|
$(actionContext.tree).trigger('refresh.taotree', [result]);
|
|
}
|
|
return resolve(result);
|
|
}
|
|
return reject(new Error(__('Unable to copy the resource')));
|
|
})
|
|
.catch(function(err){
|
|
self.trigger('error', err);
|
|
});
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Register the moveTo action: select a destination class to move resources
|
|
*
|
|
* @this the action (once register it is bound to an action object)
|
|
*
|
|
* @param {Object|Object[]} actionContext - multiple action contexts
|
|
* @returns {Promise<String>} with the destination class URI
|
|
*/
|
|
binder.register('moveTo', function moveTo(actionContext) {
|
|
//create the container manually...
|
|
var $container = emptyPanel();
|
|
|
|
//backward compatible for jstree
|
|
var tree = actionContext.tree;
|
|
|
|
//get the resource provider configured with the action URL
|
|
var resourceProvider = resourceProviderFactory({
|
|
moveTo: {
|
|
url: this.url
|
|
}
|
|
});
|
|
|
|
if (!_.isArray(actionContext)) {
|
|
actionContext = [actionContext];
|
|
}
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
var rootClassUri = _.pluck(actionContext, 'rootClassUri').pop();
|
|
var selectedUri = _.pluck(actionContext, 'id');
|
|
var selectedData = _.map(actionContext, function (a) {
|
|
return {id: a.id, signature: a.signature};
|
|
});
|
|
|
|
//set up a destination selector
|
|
destinationSelectorFactory($container, {
|
|
title: __('Move to'),
|
|
actionName: __('Move'),
|
|
icon: 'move-item',
|
|
classUri: rootClassUri,
|
|
confirm: messages.confirmMove,
|
|
preventSelection: function preventSelection(nodeUri, node, $node) {
|
|
var uriList = [];
|
|
|
|
//prevent selection on nodes without WRITE permissions
|
|
if ($node.length && $node.data('access') === 'partial' || $node.data('access') === 'denied') {
|
|
if (!permissionsManager.hasPermission(nodeUri, 'WRITE')) {
|
|
feedback().warning(__('You are not allowed to write in the class %s', node.label));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
uriList = [nodeUri];
|
|
$node.parents('.class').each(function() {
|
|
if (this.dataset.uri !== rootClassUri) {
|
|
uriList.push(this.dataset.uri);
|
|
}
|
|
});
|
|
|
|
//prevent selection on nodes that are already the containers of the resources or the resources themselves
|
|
if (_.intersection(selectedUri, uriList).length) {
|
|
feedback().warning(__('You cannot move the selected resources in the class %s', node.label));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
})
|
|
.on('query', function (params) {
|
|
var self = this;
|
|
|
|
//asks only classes
|
|
params.classOnly = true;
|
|
resourceProvider
|
|
.getResources(params, true)
|
|
.then(function (resources) {
|
|
//ask the server the resources from the component query
|
|
self.update(resources, params);
|
|
})
|
|
.catch(function (err) {
|
|
self.trigger('error', err);
|
|
});
|
|
})
|
|
.on('select', function (destinationClassUri) {
|
|
var self = this;
|
|
|
|
if (!_.isEmpty(destinationClassUri)) {
|
|
this.disable();
|
|
|
|
resourceProvider
|
|
.moveTo(selectedData, destinationClassUri)
|
|
.then(function (results) {
|
|
var failed = [];
|
|
var success = [];
|
|
|
|
_.forEach(results, function (result, resUri) {
|
|
var resource = _.find(actionContext, {uri: resUri});
|
|
if (result.success) {
|
|
success.push(resource);
|
|
} else {
|
|
failed.push(result.message);
|
|
}
|
|
});
|
|
|
|
if (!success.length) {
|
|
feedback().error(__('Unable to move the resources'));
|
|
} else if (failed.length) {
|
|
feedback().warning(__('Some resources have not been moved: %s', failed.join(', ')));
|
|
} else {
|
|
feedback().success(__('Resources moved'));
|
|
}
|
|
|
|
//backward compatible for jstree
|
|
if (tree) {
|
|
$(tree).trigger('refresh.taotree', [destinationClassUri]);
|
|
}
|
|
return resolve(destinationClassUri);
|
|
})
|
|
.catch(function (err) {
|
|
self.trigger('error', err);
|
|
});
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
});
|
|
};
|
|
|
|
return commonActions;
|
|
});
|
|
|
|
|