mirror of
https://github.com/nasa/openmct.git
synced 2025-02-21 09:52:04 +00:00
Merge pull request #249 from nasa/open127
[UI] Progress indicator for pending operations (e.g. duplicate)
This commit is contained in:
commit
96a7c12d69
@ -181,7 +181,7 @@ define(
|
|||||||
* @typedef DialogOption
|
* @typedef DialogOption
|
||||||
* @property {string} label a label to be displayed as the button
|
* @property {string} label a label to be displayed as the button
|
||||||
* text for this action
|
* text for this action
|
||||||
* @property {function} action a function to be called when the
|
* @property {function} callback a function to be called when the
|
||||||
* button is clicked
|
* button is clicked
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -72,20 +72,6 @@ $colorInputBg: $colorGenBg;
|
|||||||
$colorInputFg: $colorBodyFg;
|
$colorInputFg: $colorBodyFg;
|
||||||
$colorFormText: pushBack($colorBodyFg, 10%);
|
$colorFormText: pushBack($colorBodyFg, 10%);
|
||||||
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
||||||
|
|
||||||
// Status colors, mainly used for messaging and item ancillary symbols
|
|
||||||
$colorStatusFg: #fff;
|
|
||||||
$colorStatusDefault: #ccc;
|
|
||||||
$colorStatusInfo: #60ba7b;
|
|
||||||
$colorStatusAlert: #ffb66c;
|
|
||||||
$colorStatusError: #c96b68;
|
|
||||||
$colorProgressBarOuter: rgba(#000, 0.1);
|
|
||||||
$colorProgressBarAmt: #0a0;
|
|
||||||
$progressBarHOverlay: 15px;
|
|
||||||
$progressBarStripeW: 20px;
|
|
||||||
$shdwStatusIc: rgba(white, 0.8) 0 0px 5px;
|
|
||||||
|
|
||||||
// Selects
|
|
||||||
$colorSelectBg: #ddd;
|
$colorSelectBg: #ddd;
|
||||||
$colorSelectFg: $colorBodyFg;
|
$colorSelectFg: $colorBodyFg;
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
"glyph": "+",
|
"glyph": "+",
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"implementation": "actions/CopyAction.js",
|
"implementation": "actions/CopyAction.js",
|
||||||
"depends": ["locationService", "copyService"]
|
"depends": ["$log", "locationService", "copyService",
|
||||||
|
"dialogService", "notificationService"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "link",
|
"key": "link",
|
||||||
@ -84,7 +85,8 @@
|
|||||||
"name": "Copy Service",
|
"name": "Copy Service",
|
||||||
"description": "Provides a service for copying objects",
|
"description": "Provides a service for copying objects",
|
||||||
"implementation": "services/CopyService.js",
|
"implementation": "services/CopyService.js",
|
||||||
"depends": ["$q", "creationService", "policyService"]
|
"depends": ["$q", "creationService", "policyService",
|
||||||
|
"persistenceService", "now"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "locationService",
|
"key": "locationService",
|
||||||
|
@ -34,16 +34,103 @@ define(
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @memberof platform/entanglement
|
* @memberof platform/entanglement
|
||||||
*/
|
*/
|
||||||
function CopyAction(locationService, copyService, context) {
|
function CopyAction($log, locationService, copyService, dialogService,
|
||||||
return new AbstractComposeAction(
|
notificationService, context) {
|
||||||
locationService,
|
this.dialog = undefined;
|
||||||
copyService,
|
this.notification = undefined;
|
||||||
context,
|
this.dialogService = dialogService;
|
||||||
"Duplicate",
|
this.notificationService = notificationService;
|
||||||
"to a location"
|
this.$log = $log;
|
||||||
);
|
//Extend the behaviour of the Abstract Compose Action
|
||||||
|
AbstractComposeAction.call(this, locationService, copyService,
|
||||||
|
context, "Duplicate", "to a location");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user about progress of copy. Should not be invoked by
|
||||||
|
* client code under any circumstances.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param phase
|
||||||
|
* @param totalObjects
|
||||||
|
* @param processed
|
||||||
|
*/
|
||||||
|
CopyAction.prototype.progress = function(phase, totalObjects, processed){
|
||||||
|
/*
|
||||||
|
Copy has two distinct phases. In the first phase a copy plan is
|
||||||
|
made in memory. During this phase of execution, the user is
|
||||||
|
shown a blocking 'modal' dialog.
|
||||||
|
|
||||||
|
In the second phase, the copying is taking place, and the user
|
||||||
|
is shown non-invasive banner notifications at the bottom of the screen.
|
||||||
|
*/
|
||||||
|
if (phase.toLowerCase() === 'preparing' && !this.dialog){
|
||||||
|
this.dialog = this.dialogService.showBlockingMessage({
|
||||||
|
title: "Preparing to copy objects",
|
||||||
|
unknownProgress: true,
|
||||||
|
severity: "info"
|
||||||
|
});
|
||||||
|
} else if (phase.toLowerCase() === "copying") {
|
||||||
|
this.dialogService.dismiss();
|
||||||
|
if (!this.notification) {
|
||||||
|
this.notification = this.notificationService
|
||||||
|
.notify({
|
||||||
|
title: "Copying objects",
|
||||||
|
unknownProgress: false,
|
||||||
|
severity: "info"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.notification.model.progress = (processed / totalObjects) * 100;
|
||||||
|
this.notification.model.title = ["Copied ", processed, "of ",
|
||||||
|
totalObjects, "objects"].join(" ");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the CopyAction. The CopyAction uses the default behaviour of
|
||||||
|
* the AbstractComposeAction, but extends it to support notification
|
||||||
|
* updates of progress on copy.
|
||||||
|
*/
|
||||||
|
CopyAction.prototype.perform = function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function success(){
|
||||||
|
self.notification.dismiss();
|
||||||
|
self.notificationService.info("Copying complete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(errorDetails){
|
||||||
|
var errorMessage = {
|
||||||
|
title: "Error copying objects.",
|
||||||
|
severity: "error",
|
||||||
|
hint: errorDetails.message,
|
||||||
|
minimized: true, // want the notification to be minimized initially (don't show banner)
|
||||||
|
options: [{
|
||||||
|
label: "OK",
|
||||||
|
callback: function() {
|
||||||
|
self.dialogService.dismiss();
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
self.dialogService.dismiss();
|
||||||
|
if (self.notification) {
|
||||||
|
self.notification.dismiss(); // Clear the progress notification
|
||||||
|
}
|
||||||
|
self.$log.error("Error copying objects. ", errorDetails);
|
||||||
|
//Show a minimized notification of error for posterity
|
||||||
|
self.notificationService.notify(errorMessage);
|
||||||
|
//Display a blocking message
|
||||||
|
self.dialogService.showBlockingMessage(errorMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
function notification(details){
|
||||||
|
self.progress(details.phase, details.totalObjects, details.processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AbstractComposeAction.prototype.perform.call(this)
|
||||||
|
.then(success, error, notification);
|
||||||
|
};
|
||||||
return CopyAction;
|
return CopyAction;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,11 @@
|
|||||||
/*global define */
|
/*global define */
|
||||||
|
|
||||||
define(
|
define(
|
||||||
function () {
|
[
|
||||||
|
"uuid",
|
||||||
|
"./CopyTask"
|
||||||
|
],
|
||||||
|
function (uuid, CopyTask) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,10 +38,12 @@ define(
|
|||||||
* @memberof platform/entanglement
|
* @memberof platform/entanglement
|
||||||
* @implements {platform/entanglement.AbstractComposeService}
|
* @implements {platform/entanglement.AbstractComposeService}
|
||||||
*/
|
*/
|
||||||
function CopyService($q, creationService, policyService) {
|
function CopyService($q, creationService, policyService, persistenceService, now) {
|
||||||
this.$q = $q;
|
this.$q = $q;
|
||||||
this.creationService = creationService;
|
this.creationService = creationService;
|
||||||
this.policyService = policyService;
|
this.policyService = policyService;
|
||||||
|
this.persistenceService = persistenceService;
|
||||||
|
this.now = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyService.prototype.validate = function (object, parentCandidate) {
|
CopyService.prototype.validate = function (object, parentCandidate) {
|
||||||
@ -54,45 +60,25 @@ define(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a duplicate of the object tree starting at domainObject to
|
||||||
|
* the new parent specified.
|
||||||
|
* @param domainObject
|
||||||
|
* @param parent
|
||||||
|
* @param progress
|
||||||
|
* @returns a promise that will be completed with the clone of
|
||||||
|
* domainObject when the duplication is successful.
|
||||||
|
*/
|
||||||
CopyService.prototype.perform = function (domainObject, parent) {
|
CopyService.prototype.perform = function (domainObject, parent) {
|
||||||
var model = JSON.parse(JSON.stringify(domainObject.getModel())),
|
var $q = this.$q,
|
||||||
$q = this.$q,
|
copyTask = new CopyTask(domainObject, parent, this.persistenceService, this.$q, this.now);
|
||||||
self = this;
|
if (this.validate(domainObject, parent)) {
|
||||||
|
return copyTask.perform();
|
||||||
// Wrapper for the recursive step
|
} else {
|
||||||
function duplicateObject(domainObject, parent) {
|
|
||||||
return self.perform(domainObject, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.validate(domainObject, parent)) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Tried to copy objects without validating first."
|
"Tried to copy objects without validating first."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domainObject.hasCapability('composition')) {
|
|
||||||
model.composition = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.creationService
|
|
||||||
.createObject(model, parent)
|
|
||||||
.then(function (newObject) {
|
|
||||||
if (!domainObject.hasCapability('composition')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return domainObject
|
|
||||||
.useCapability('composition')
|
|
||||||
.then(function (composees) {
|
|
||||||
// Duplicate composition serially to prevent
|
|
||||||
// write conflicts.
|
|
||||||
return composees.reduce(function (promise, composee) {
|
|
||||||
return promise.then(function () {
|
|
||||||
return duplicateObject(composee, newObject);
|
|
||||||
});
|
|
||||||
}, $q.when(undefined));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return CopyService;
|
return CopyService;
|
||||||
|
198
platform/entanglement/src/services/CopyTask.js
Normal file
198
platform/entanglement/src/services/CopyTask.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT Web includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/*global define */
|
||||||
|
|
||||||
|
define(
|
||||||
|
["uuid"],
|
||||||
|
function (uuid) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class encapsulates the process of copying a domain object
|
||||||
|
* and all of its children.
|
||||||
|
*
|
||||||
|
* @param domainObject The object to copy
|
||||||
|
* @param parent The new location of the cloned object tree
|
||||||
|
* @param persistenceService
|
||||||
|
* @param $q
|
||||||
|
* @param now
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function CopyTask (domainObject, parent, persistenceService, $q, now){
|
||||||
|
this.domainObject = domainObject;
|
||||||
|
this.parent = parent;
|
||||||
|
this.$q = $q;
|
||||||
|
this.deferred = undefined;
|
||||||
|
this.persistenceService = persistenceService;
|
||||||
|
this.persisted = 0;
|
||||||
|
this.now = now;
|
||||||
|
this.clones = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeChild(child, parent) {
|
||||||
|
//Once copied, associate each cloned
|
||||||
|
// composee with its parent clone
|
||||||
|
child.model.location = parent.id;
|
||||||
|
parent.model.composition = parent.model.composition || [];
|
||||||
|
return parent.model.composition.push(child.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneObjectModel(objectModel) {
|
||||||
|
var clone = JSON.parse(JSON.stringify(objectModel));
|
||||||
|
|
||||||
|
delete clone.composition;
|
||||||
|
delete clone.persisted;
|
||||||
|
delete clone.modified;
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will persist a list of {@link objectClones}. It will persist all
|
||||||
|
* simultaneously, irrespective of order in the list. This may
|
||||||
|
* result in automatic request batching by the browser.
|
||||||
|
*/
|
||||||
|
function persistObjects(self) {
|
||||||
|
|
||||||
|
return self.$q.all(self.clones.map(function(clone){
|
||||||
|
clone.model.persisted = self.now();
|
||||||
|
return self.persistenceService.createObject(clone.persistenceSpace, clone.id, clone.model)
|
||||||
|
.then(function(){
|
||||||
|
self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted});
|
||||||
|
});
|
||||||
|
})).then(function(){
|
||||||
|
return self;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a list of clones to the specified parent's composition
|
||||||
|
*/
|
||||||
|
function addClonesToParent(self) {
|
||||||
|
var parentClone = self.clones[self.clones.length-1];
|
||||||
|
|
||||||
|
if (!self.parent.hasCapability('composition')){
|
||||||
|
return self.$q.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.persistenceService
|
||||||
|
.updateObject(parentClone.persistenceSpace, parentClone.id, parentClone.model)
|
||||||
|
.then(function(){return self.parent.getCapability("composition").add(parentClone.id);})
|
||||||
|
.then(function(){return self.parent.getCapability("persistence").persist();})
|
||||||
|
.then(function(){return parentClone;});
|
||||||
|
// Ensure the clone of the original domainObject is returned
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of objects composed by a parent, clone them, then
|
||||||
|
* add them to the parent.
|
||||||
|
* @private
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
CopyTask.prototype.copyComposees = function(composees, clonedParent, originalParent){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return (composees || []).reduce(function(promise, composee){
|
||||||
|
//If the composee is composed of other
|
||||||
|
// objects, chain a promise..
|
||||||
|
return promise.then(function(){
|
||||||
|
// ...to recursively copy it (and its children)
|
||||||
|
return self.copy(composee, originalParent).then(function(composee){
|
||||||
|
composeChild(composee, clonedParent);
|
||||||
|
});
|
||||||
|
});}, self.$q.when(undefined)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recursive function that will perform a bottom-up copy of
|
||||||
|
* the object tree with originalObject at the root. Recurses to
|
||||||
|
* the farthest leaf, then works its way back up again,
|
||||||
|
* cloning objects, and composing them with their child clones
|
||||||
|
* as it goes
|
||||||
|
* @private
|
||||||
|
* @param originalObject
|
||||||
|
* @param originalParent
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
CopyTask.prototype.copy = function(originalObject, originalParent) {
|
||||||
|
var self = this,
|
||||||
|
modelClone = {
|
||||||
|
id: uuid(),
|
||||||
|
model: cloneObjectModel(originalObject.getModel()),
|
||||||
|
persistenceSpace: originalParent.hasCapability('persistence') && originalParent.getCapability('persistence').getSpace()
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.$q.when(originalObject.useCapability('composition')).then(function(composees){
|
||||||
|
self.deferred.notify({phase: "preparing"});
|
||||||
|
//Duplicate the object's children, and their children, and
|
||||||
|
// so on down to the leaf nodes of the tree.
|
||||||
|
return self.copyComposees(composees, modelClone, originalObject).then(function (){
|
||||||
|
//Add the clone to the list of clones that will
|
||||||
|
//be returned by this function
|
||||||
|
self.clones.push(modelClone);
|
||||||
|
return modelClone;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will build a graph of an object and all of its child objects in
|
||||||
|
* memory
|
||||||
|
* @private
|
||||||
|
* @param domainObject The original object to be copied
|
||||||
|
* @param parent The parent of the original object to be copied
|
||||||
|
* @returns {Promise} resolved with an array of clones of the models
|
||||||
|
* of the object tree being copied. Copying is done in a bottom-up
|
||||||
|
* fashion, so that the last member in the array is a clone of the model
|
||||||
|
* object being copied. The clones are all full composed with
|
||||||
|
* references to their own children.
|
||||||
|
*/
|
||||||
|
CopyTask.prototype.buildCopyPlan = function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return this.copy(self.domainObject, self.parent).then(function(domainObjectClone){
|
||||||
|
domainObjectClone.model.location = self.parent.getId();
|
||||||
|
return self;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the copy task with the objects provided in the constructor.
|
||||||
|
* @returns {promise} Which will resolve with a clone of the object
|
||||||
|
* once complete.
|
||||||
|
*/
|
||||||
|
CopyTask.prototype.perform = function(){
|
||||||
|
this.deferred = this.$q.defer();
|
||||||
|
|
||||||
|
this.buildCopyPlan()
|
||||||
|
.then(persistObjects)
|
||||||
|
.then(addClonesToParent)
|
||||||
|
.then(this.deferred.resolve, this.deferred.reject);
|
||||||
|
|
||||||
|
return this.deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return CopyTask;
|
||||||
|
}
|
||||||
|
);
|
@ -20,7 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
/*global define,describe,beforeEach,it,jasmine,expect */
|
/*global define,describe,beforeEach,it,jasmine,expect,spyOn */
|
||||||
|
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
@ -41,7 +41,13 @@ define(
|
|||||||
selectedObject,
|
selectedObject,
|
||||||
selectedObjectContextCapability,
|
selectedObjectContextCapability,
|
||||||
currentParent,
|
currentParent,
|
||||||
newParent;
|
newParent,
|
||||||
|
notificationService,
|
||||||
|
notification,
|
||||||
|
dialogService,
|
||||||
|
mockLog,
|
||||||
|
abstractComposePromise,
|
||||||
|
progress = {phase: "copying", totalObjects: 10, processed: 1};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
selectedObjectContextCapability = jasmine.createSpyObj(
|
||||||
@ -87,10 +93,43 @@ define(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
abstractComposePromise = jasmine.createSpyObj(
|
||||||
|
'abstractComposePromise',
|
||||||
|
[
|
||||||
|
'then'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
abstractComposePromise.then.andCallFake(function(success, error, notify){
|
||||||
|
notify(progress);
|
||||||
|
success();
|
||||||
|
});
|
||||||
|
|
||||||
|
locationServicePromise.then.andCallFake(function(callback){
|
||||||
|
callback(newParent);
|
||||||
|
return abstractComposePromise;
|
||||||
|
});
|
||||||
|
|
||||||
locationService
|
locationService
|
||||||
.getLocationFromUser
|
.getLocationFromUser
|
||||||
.andReturn(locationServicePromise);
|
.andReturn(locationServicePromise);
|
||||||
|
|
||||||
|
dialogService = jasmine.createSpyObj('dialogService',
|
||||||
|
['showBlockingMessage', 'dismiss']
|
||||||
|
);
|
||||||
|
|
||||||
|
notification = jasmine.createSpyObj('notification',
|
||||||
|
['dismiss', 'model']
|
||||||
|
);
|
||||||
|
|
||||||
|
notificationService = jasmine.createSpyObj('notificationService',
|
||||||
|
['notify', 'info']
|
||||||
|
);
|
||||||
|
|
||||||
|
notificationService.notify.andReturn(notification);
|
||||||
|
|
||||||
|
mockLog = jasmine.createSpyObj('log', ['error']);
|
||||||
|
|
||||||
copyService = new MockCopyService();
|
copyService = new MockCopyService();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,8 +141,11 @@ define(
|
|||||||
};
|
};
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
copyAction = new CopyAction(
|
||||||
|
mockLog,
|
||||||
locationService,
|
locationService,
|
||||||
copyService,
|
copyService,
|
||||||
|
dialogService,
|
||||||
|
notificationService,
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -114,6 +156,7 @@ define(
|
|||||||
|
|
||||||
describe("when performed it", function () {
|
describe("when performed it", function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
spyOn(copyAction, 'progress').andCallThrough();
|
||||||
copyAction.perform();
|
copyAction.perform();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,7 +175,7 @@ define(
|
|||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copys object to selected location", function () {
|
it("copies object to selected location", function () {
|
||||||
locationServicePromise
|
locationServicePromise
|
||||||
.then
|
.then
|
||||||
.mostRecentCall
|
.mostRecentCall
|
||||||
@ -141,6 +184,11 @@ define(
|
|||||||
expect(copyService.perform)
|
expect(copyService.perform)
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("notifies the user of progress", function(){
|
||||||
|
expect(notificationService.info).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -152,8 +200,11 @@ define(
|
|||||||
};
|
};
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
copyAction = new CopyAction(
|
||||||
|
mockLog,
|
||||||
locationService,
|
locationService,
|
||||||
copyService,
|
copyService,
|
||||||
|
dialogService,
|
||||||
|
notificationService,
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,10 @@ define(
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function synchronousPromise(value) {
|
function synchronousPromise(value) {
|
||||||
|
if (value && value.then) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
var promise = {
|
var promise = {
|
||||||
then: function (callback) {
|
then: function (callback) {
|
||||||
return synchronousPromise(callback(value));
|
return synchronousPromise(callback(value));
|
||||||
@ -122,13 +126,19 @@ define(
|
|||||||
describe("perform", function () {
|
describe("perform", function () {
|
||||||
|
|
||||||
var mockQ,
|
var mockQ,
|
||||||
|
mockDeferred,
|
||||||
creationService,
|
creationService,
|
||||||
createObjectPromise,
|
createObjectPromise,
|
||||||
copyService,
|
copyService,
|
||||||
|
mockPersistenceService,
|
||||||
|
mockNow,
|
||||||
object,
|
object,
|
||||||
newParent,
|
newParent,
|
||||||
copyResult,
|
copyResult,
|
||||||
copyFinished;
|
copyFinished,
|
||||||
|
persistObjectPromise,
|
||||||
|
parentPersistenceCapability,
|
||||||
|
resolvedValue;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
creationService = jasmine.createSpyObj(
|
creationService = jasmine.createSpyObj(
|
||||||
@ -138,44 +148,93 @@ define(
|
|||||||
createObjectPromise = synchronousPromise(undefined);
|
createObjectPromise = synchronousPromise(undefined);
|
||||||
creationService.createObject.andReturn(createObjectPromise);
|
creationService.createObject.andReturn(createObjectPromise);
|
||||||
policyService.allow.andReturn(true);
|
policyService.allow.andReturn(true);
|
||||||
|
|
||||||
|
mockPersistenceService = jasmine.createSpyObj(
|
||||||
|
'persistenceService',
|
||||||
|
['createObject', 'updateObject']
|
||||||
|
);
|
||||||
|
persistObjectPromise = synchronousPromise(undefined);
|
||||||
|
mockPersistenceService.createObject.andReturn(persistObjectPromise);
|
||||||
|
mockPersistenceService.updateObject.andReturn(persistObjectPromise);
|
||||||
|
|
||||||
|
parentPersistenceCapability = jasmine.createSpyObj(
|
||||||
|
"persistence",
|
||||||
|
[ "persist", "getSpace" ]
|
||||||
|
);
|
||||||
|
|
||||||
|
parentPersistenceCapability.persist.andReturn(persistObjectPromise);
|
||||||
|
parentPersistenceCapability.getSpace.andReturn("testSpace");
|
||||||
|
|
||||||
|
mockNow = jasmine.createSpyObj("mockNow", ["now"]);
|
||||||
|
mockNow.now.andCallFake(function(){
|
||||||
|
return 1234;
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDeferred = jasmine.createSpyObj('mockDeferred', ['notify', 'resolve']);
|
||||||
|
mockDeferred.notify.andCallFake(function(notification){});
|
||||||
|
mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;});
|
||||||
|
mockDeferred.promise = {
|
||||||
|
then: function(callback){
|
||||||
|
return synchronousPromise(callback(resolvedValue));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mockQ = jasmine.createSpyObj('mockQ', ['when', 'all', 'reject', 'defer']);
|
||||||
|
mockQ.when.andCallFake(synchronousPromise);
|
||||||
|
mockQ.all.andCallFake(function (promises) {
|
||||||
|
var result = {};
|
||||||
|
Object.keys(promises).forEach(function (k) {
|
||||||
|
promises[k].then(function (v) { result[k] = v; });
|
||||||
|
});
|
||||||
|
return synchronousPromise(result);
|
||||||
|
});
|
||||||
|
mockQ.defer.andReturn(mockDeferred);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("on domain object without composition", function () {
|
describe("on domain object without composition", function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
object = domainObjectFactory({
|
|
||||||
name: 'object',
|
|
||||||
id: 'abc',
|
|
||||||
model: {
|
|
||||||
name: 'some object'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
newParent = domainObjectFactory({
|
newParent = domainObjectFactory({
|
||||||
name: 'newParent',
|
name: 'newParent',
|
||||||
id: '456',
|
id: '456',
|
||||||
model: {
|
model: {
|
||||||
composition: []
|
composition: []
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
persistence: parentPersistenceCapability
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
copyService = new CopyService(null, creationService, policyService);
|
|
||||||
|
object = domainObjectFactory({
|
||||||
|
name: 'object',
|
||||||
|
id: 'abc',
|
||||||
|
model: {
|
||||||
|
name: 'some object',
|
||||||
|
location: newParent.id,
|
||||||
|
persisted: mockNow.now()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||||
copyResult = copyService.perform(object, newParent);
|
copyResult = copyService.perform(object, newParent);
|
||||||
copyFinished = jasmine.createSpy('copyFinished');
|
copyFinished = jasmine.createSpy('copyFinished');
|
||||||
copyResult.then(copyFinished);
|
copyResult.then(copyFinished);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses creation service", function () {
|
it("uses persistence service", function () {
|
||||||
expect(creationService.createObject)
|
expect(mockPersistenceService.createObject)
|
||||||
.toHaveBeenCalledWith(jasmine.any(Object), newParent);
|
.toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel());
|
||||||
|
|
||||||
expect(createObjectPromise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(persistObjectPromise.then)
|
||||||
|
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
it("deep clones object model", function () {
|
it("deep clones object model", function () {
|
||||||
var newModel = creationService
|
//var newModel = creationService
|
||||||
|
var newModel = mockPersistenceService
|
||||||
.createObject
|
.createObject
|
||||||
.mostRecentCall
|
.mostRecentCall
|
||||||
.args[0];
|
.args[2];
|
||||||
|
|
||||||
expect(newModel).toEqual(object.model);
|
expect(newModel).toEqual(object.model);
|
||||||
expect(newModel).not.toBe(object.model);
|
expect(newModel).not.toBe(object.model);
|
||||||
});
|
});
|
||||||
@ -191,11 +250,15 @@ define(
|
|||||||
var newObject,
|
var newObject,
|
||||||
childObject,
|
childObject,
|
||||||
compositionCapability,
|
compositionCapability,
|
||||||
|
locationCapability,
|
||||||
compositionPromise;
|
compositionPromise;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockQ = jasmine.createSpyObj('mockQ', ['when']);
|
|
||||||
mockQ.when.andCallFake(synchronousPromise);
|
|
||||||
|
locationCapability = jasmine.createSpyObj('locationCapability', ['isLink']);
|
||||||
|
locationCapability.isLink.andReturn(true);
|
||||||
|
|
||||||
childObject = domainObjectFactory({
|
childObject = domainObjectFactory({
|
||||||
name: 'childObject',
|
name: 'childObject',
|
||||||
id: 'def',
|
id: 'def',
|
||||||
@ -205,24 +268,28 @@ define(
|
|||||||
});
|
});
|
||||||
compositionCapability = jasmine.createSpyObj(
|
compositionCapability = jasmine.createSpyObj(
|
||||||
'compositionCapability',
|
'compositionCapability',
|
||||||
['invoke']
|
['invoke', 'add']
|
||||||
);
|
);
|
||||||
compositionPromise = jasmine.createSpyObj(
|
compositionPromise = jasmine.createSpyObj(
|
||||||
'compositionPromise',
|
'compositionPromise',
|
||||||
['then']
|
['then']
|
||||||
);
|
);
|
||||||
|
|
||||||
compositionCapability
|
compositionCapability
|
||||||
.invoke
|
.invoke
|
||||||
.andReturn(compositionPromise);
|
.andReturn(synchronousPromise([childObject]));
|
||||||
|
|
||||||
object = domainObjectFactory({
|
object = domainObjectFactory({
|
||||||
name: 'object',
|
name: 'object',
|
||||||
id: 'abc',
|
id: 'abc',
|
||||||
model: {
|
model: {
|
||||||
name: 'some object',
|
name: 'some object',
|
||||||
composition: ['def']
|
composition: ['def'],
|
||||||
|
location: 'testLocation'
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
composition: compositionCapability
|
composition: compositionCapability,
|
||||||
|
location: locationCapability
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
newObject = domainObjectFactory({
|
newObject = domainObjectFactory({
|
||||||
@ -241,45 +308,45 @@ define(
|
|||||||
id: '456',
|
id: '456',
|
||||||
model: {
|
model: {
|
||||||
composition: []
|
composition: []
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
composition: compositionCapability,
|
||||||
|
persistence: parentPersistenceCapability
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createObjectPromise = synchronousPromise(newObject);
|
createObjectPromise = synchronousPromise(newObject);
|
||||||
creationService.createObject.andReturn(createObjectPromise);
|
creationService.createObject.andReturn(createObjectPromise);
|
||||||
copyService = new CopyService(mockQ, creationService, policyService);
|
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||||
copyResult = copyService.perform(object, newParent);
|
|
||||||
copyFinished = jasmine.createSpy('copyFinished');
|
|
||||||
copyResult.then(copyFinished);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses creation service", function () {
|
describe("the cloning process", function(){
|
||||||
expect(creationService.createObject)
|
beforeEach(function() {
|
||||||
.toHaveBeenCalledWith(jasmine.any(Object), newParent);
|
copyResult = copyService.perform(object, newParent);
|
||||||
|
copyFinished = jasmine.createSpy('copyFinished');
|
||||||
|
copyResult.then(copyFinished);
|
||||||
|
});
|
||||||
|
|
||||||
expect(createObjectPromise.then)
|
it("copies object and children in a bottom-up" +
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
" fashion", function () {
|
||||||
});
|
expect(mockPersistenceService.createObject.calls[0].args[2].name).toEqual(childObject.model.name);
|
||||||
|
expect(mockPersistenceService.createObject.calls[1].args[2].name).toEqual(object.model.name);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears model composition", function () {
|
it("returns a promise", function () {
|
||||||
var newModel = creationService
|
expect(copyResult.then).toBeDefined();
|
||||||
.createObject
|
expect(copyFinished).toHaveBeenCalled();
|
||||||
.mostRecentCall
|
});
|
||||||
.args[0];
|
|
||||||
|
|
||||||
expect(newModel.composition.length).toBe(0);
|
it("clears modified and sets persisted", function () {
|
||||||
expect(newModel.name).toBe('some object');
|
expect(copyFinished.mostRecentCall.args[0].model.modified).toBeUndefined();
|
||||||
});
|
expect(copyFinished.mostRecentCall.args[0].model.persisted).toBe(mockNow.now());
|
||||||
|
});
|
||||||
|
|
||||||
it("recursively clones it's children", function () {
|
it ("correctly locates cloned objects", function() {
|
||||||
expect(creationService.createObject.calls.length).toBe(1);
|
expect(mockPersistenceService.createObject.calls[0].args[2].location).toEqual(mockPersistenceService.createObject.calls[1].args[1]);
|
||||||
expect(compositionCapability.invoke).toHaveBeenCalled();
|
});
|
||||||
compositionPromise.then.mostRecentCall.args[0]([childObject]);
|
|
||||||
expect(creationService.createObject.calls.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise", function () {
|
|
||||||
expect(copyResult.then).toBeDefined();
|
|
||||||
expect(copyFinished).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -301,7 +368,7 @@ define(
|
|||||||
|
|
||||||
it("throws an error", function () {
|
it("throws an error", function () {
|
||||||
var copyService =
|
var copyService =
|
||||||
new CopyService(mockQ, creationService, policyService);
|
new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||||
|
|
||||||
function perform() {
|
function perform() {
|
||||||
copyService.perform(object, newParent);
|
copyService.perform(object, newParent);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user