mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 09:26:45 +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
|
||||
* @property {string} label a label to be displayed as the button
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
@ -72,20 +72,6 @@ $colorInputBg: $colorGenBg;
|
||||
$colorInputFg: $colorBodyFg;
|
||||
$colorFormText: pushBack($colorBodyFg, 10%);
|
||||
$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;
|
||||
$colorSelectFg: $colorBodyFg;
|
||||
|
||||
|
@ -20,7 +20,8 @@
|
||||
"glyph": "+",
|
||||
"category": "contextual",
|
||||
"implementation": "actions/CopyAction.js",
|
||||
"depends": ["locationService", "copyService"]
|
||||
"depends": ["$log", "locationService", "copyService",
|
||||
"dialogService", "notificationService"]
|
||||
},
|
||||
{
|
||||
"key": "link",
|
||||
@ -84,7 +85,8 @@
|
||||
"name": "Copy Service",
|
||||
"description": "Provides a service for copying objects",
|
||||
"implementation": "services/CopyService.js",
|
||||
"depends": ["$q", "creationService", "policyService"]
|
||||
"depends": ["$q", "creationService", "policyService",
|
||||
"persistenceService", "now"]
|
||||
},
|
||||
{
|
||||
"key": "locationService",
|
||||
|
@ -34,16 +34,103 @@ define(
|
||||
* @constructor
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function CopyAction(locationService, copyService, context) {
|
||||
return new AbstractComposeAction(
|
||||
locationService,
|
||||
copyService,
|
||||
context,
|
||||
"Duplicate",
|
||||
"to a location"
|
||||
);
|
||||
function CopyAction($log, locationService, copyService, dialogService,
|
||||
notificationService, context) {
|
||||
this.dialog = undefined;
|
||||
this.notification = undefined;
|
||||
this.dialogService = dialogService;
|
||||
this.notificationService = notificationService;
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
@ -23,7 +23,11 @@
|
||||
/*global define */
|
||||
|
||||
define(
|
||||
function () {
|
||||
[
|
||||
"uuid",
|
||||
"./CopyTask"
|
||||
],
|
||||
function (uuid, CopyTask) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
@ -34,10 +38,12 @@ define(
|
||||
* @memberof platform/entanglement
|
||||
* @implements {platform/entanglement.AbstractComposeService}
|
||||
*/
|
||||
function CopyService($q, creationService, policyService) {
|
||||
function CopyService($q, creationService, policyService, persistenceService, now) {
|
||||
this.$q = $q;
|
||||
this.creationService = creationService;
|
||||
this.policyService = policyService;
|
||||
this.persistenceService = persistenceService;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
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) {
|
||||
var model = JSON.parse(JSON.stringify(domainObject.getModel())),
|
||||
$q = this.$q,
|
||||
self = this;
|
||||
|
||||
// Wrapper for the recursive step
|
||||
function duplicateObject(domainObject, parent) {
|
||||
return self.perform(domainObject, parent);
|
||||
}
|
||||
|
||||
if (!this.validate(domainObject, parent)) {
|
||||
var $q = this.$q,
|
||||
copyTask = new CopyTask(domainObject, parent, this.persistenceService, this.$q, this.now);
|
||||
if (this.validate(domainObject, parent)) {
|
||||
return copyTask.perform();
|
||||
} else {
|
||||
throw new Error(
|
||||
"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;
|
||||
|
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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*global define,describe,beforeEach,it,jasmine,expect */
|
||||
/*global define,describe,beforeEach,it,jasmine,expect,spyOn */
|
||||
|
||||
define(
|
||||
[
|
||||
@ -41,7 +41,13 @@ define(
|
||||
selectedObject,
|
||||
selectedObjectContextCapability,
|
||||
currentParent,
|
||||
newParent;
|
||||
newParent,
|
||||
notificationService,
|
||||
notification,
|
||||
dialogService,
|
||||
mockLog,
|
||||
abstractComposePromise,
|
||||
progress = {phase: "copying", totalObjects: 10, processed: 1};
|
||||
|
||||
beforeEach(function () {
|
||||
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
|
||||
.getLocationFromUser
|
||||
.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();
|
||||
});
|
||||
|
||||
@ -102,8 +141,11 @@ define(
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
@ -114,6 +156,7 @@ define(
|
||||
|
||||
describe("when performed it", function () {
|
||||
beforeEach(function () {
|
||||
spyOn(copyAction, 'progress').andCallThrough();
|
||||
copyAction.perform();
|
||||
});
|
||||
|
||||
@ -132,7 +175,7 @@ define(
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("copys object to selected location", function () {
|
||||
it("copies object to selected location", function () {
|
||||
locationServicePromise
|
||||
.then
|
||||
.mostRecentCall
|
||||
@ -141,6 +184,11 @@ define(
|
||||
expect(copyService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
|
||||
it("notifies the user of progress", function(){
|
||||
expect(notificationService.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -152,8 +200,11 @@ define(
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
@ -31,6 +31,10 @@ define(
|
||||
"use strict";
|
||||
|
||||
function synchronousPromise(value) {
|
||||
if (value && value.then) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var promise = {
|
||||
then: function (callback) {
|
||||
return synchronousPromise(callback(value));
|
||||
@ -122,13 +126,19 @@ define(
|
||||
describe("perform", function () {
|
||||
|
||||
var mockQ,
|
||||
mockDeferred,
|
||||
creationService,
|
||||
createObjectPromise,
|
||||
copyService,
|
||||
mockPersistenceService,
|
||||
mockNow,
|
||||
object,
|
||||
newParent,
|
||||
copyResult,
|
||||
copyFinished;
|
||||
copyFinished,
|
||||
persistObjectPromise,
|
||||
parentPersistenceCapability,
|
||||
resolvedValue;
|
||||
|
||||
beforeEach(function () {
|
||||
creationService = jasmine.createSpyObj(
|
||||
@ -138,44 +148,93 @@ define(
|
||||
createObjectPromise = synchronousPromise(undefined);
|
||||
creationService.createObject.andReturn(createObjectPromise);
|
||||
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 () {
|
||||
beforeEach(function () {
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
id: 'abc',
|
||||
model: {
|
||||
name: 'some object'
|
||||
}
|
||||
});
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent',
|
||||
id: '456',
|
||||
model: {
|
||||
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);
|
||||
copyFinished = jasmine.createSpy('copyFinished');
|
||||
copyResult.then(copyFinished);
|
||||
});
|
||||
|
||||
it("uses creation service", function () {
|
||||
expect(creationService.createObject)
|
||||
.toHaveBeenCalledWith(jasmine.any(Object), newParent);
|
||||
|
||||
expect(createObjectPromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
it("uses persistence service", function () {
|
||||
expect(mockPersistenceService.createObject)
|
||||
.toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel());
|
||||
|
||||
expect(persistObjectPromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("deep clones object model", function () {
|
||||
var newModel = creationService
|
||||
//var newModel = creationService
|
||||
var newModel = mockPersistenceService
|
||||
.createObject
|
||||
.mostRecentCall
|
||||
.args[0];
|
||||
|
||||
.args[2];
|
||||
expect(newModel).toEqual(object.model);
|
||||
expect(newModel).not.toBe(object.model);
|
||||
});
|
||||
@ -191,11 +250,15 @@ define(
|
||||
var newObject,
|
||||
childObject,
|
||||
compositionCapability,
|
||||
locationCapability,
|
||||
compositionPromise;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('mockQ', ['when']);
|
||||
mockQ.when.andCallFake(synchronousPromise);
|
||||
|
||||
|
||||
locationCapability = jasmine.createSpyObj('locationCapability', ['isLink']);
|
||||
locationCapability.isLink.andReturn(true);
|
||||
|
||||
childObject = domainObjectFactory({
|
||||
name: 'childObject',
|
||||
id: 'def',
|
||||
@ -205,24 +268,28 @@ define(
|
||||
});
|
||||
compositionCapability = jasmine.createSpyObj(
|
||||
'compositionCapability',
|
||||
['invoke']
|
||||
['invoke', 'add']
|
||||
);
|
||||
compositionPromise = jasmine.createSpyObj(
|
||||
'compositionPromise',
|
||||
['then']
|
||||
);
|
||||
|
||||
compositionCapability
|
||||
.invoke
|
||||
.andReturn(compositionPromise);
|
||||
.andReturn(synchronousPromise([childObject]));
|
||||
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
id: 'abc',
|
||||
model: {
|
||||
name: 'some object',
|
||||
composition: ['def']
|
||||
composition: ['def'],
|
||||
location: 'testLocation'
|
||||
},
|
||||
capabilities: {
|
||||
composition: compositionCapability
|
||||
composition: compositionCapability,
|
||||
location: locationCapability
|
||||
}
|
||||
});
|
||||
newObject = domainObjectFactory({
|
||||
@ -241,45 +308,45 @@ define(
|
||||
id: '456',
|
||||
model: {
|
||||
composition: []
|
||||
},
|
||||
capabilities: {
|
||||
composition: compositionCapability,
|
||||
persistence: parentPersistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
createObjectPromise = synchronousPromise(newObject);
|
||||
creationService.createObject.andReturn(createObjectPromise);
|
||||
copyService = new CopyService(mockQ, creationService, policyService);
|
||||
copyResult = copyService.perform(object, newParent);
|
||||
copyFinished = jasmine.createSpy('copyFinished');
|
||||
copyResult.then(copyFinished);
|
||||
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||
});
|
||||
|
||||
it("uses creation service", function () {
|
||||
expect(creationService.createObject)
|
||||
.toHaveBeenCalledWith(jasmine.any(Object), newParent);
|
||||
describe("the cloning process", function(){
|
||||
beforeEach(function() {
|
||||
copyResult = copyService.perform(object, newParent);
|
||||
copyFinished = jasmine.createSpy('copyFinished');
|
||||
copyResult.then(copyFinished);
|
||||
});
|
||||
|
||||
expect(createObjectPromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
it("copies object and children in a bottom-up" +
|
||||
" 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 () {
|
||||
var newModel = creationService
|
||||
.createObject
|
||||
.mostRecentCall
|
||||
.args[0];
|
||||
it("returns a promise", function () {
|
||||
expect(copyResult.then).toBeDefined();
|
||||
expect(copyFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(newModel.composition.length).toBe(0);
|
||||
expect(newModel.name).toBe('some object');
|
||||
});
|
||||
it("clears modified and sets persisted", function () {
|
||||
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 () {
|
||||
expect(creationService.createObject.calls.length).toBe(1);
|
||||
expect(compositionCapability.invoke).toHaveBeenCalled();
|
||||
compositionPromise.then.mostRecentCall.args[0]([childObject]);
|
||||
expect(creationService.createObject.calls.length).toBe(2);
|
||||
});
|
||||
it ("correctly locates cloned objects", function() {
|
||||
expect(mockPersistenceService.createObject.calls[0].args[2].location).toEqual(mockPersistenceService.createObject.calls[1].args[1]);
|
||||
});
|
||||
|
||||
it("returns a promise", function () {
|
||||
expect(copyResult.then).toBeDefined();
|
||||
expect(copyFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -301,7 +368,7 @@ define(
|
||||
|
||||
it("throws an error", function () {
|
||||
var copyService =
|
||||
new CopyService(mockQ, creationService, policyService);
|
||||
new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||
|
||||
function perform() {
|
||||
copyService.perform(object, newParent);
|
||||
|
Loading…
x
Reference in New Issue
Block a user