Merge branch 'open1033' into open-master

This commit is contained in:
Victor Woeltjen
2015-04-06 08:20:02 -07:00
57 changed files with 2411 additions and 103 deletions

View File

@@ -0,0 +1,27 @@
This bundle provides `dialogService`, which can be used to prompt
for user input.
## `getUserChoice`
The `getUserChoice` method is useful for displaying a message and a set of
buttons. This method returns a promise which will resolve to the user's
chosen option (or, more specifically, its `key`), and will be rejected if
the user closes the dialog with the X in the top-right;
The `dialogModel` given as an argument to this method should have the
following properties.
* `title`: The title to display at the top of the dialog.
* `hint`: Short message to display below the title.
* `template`: Identifying key (as will be passed to `mct-include`) for
the template which will be used to populate the inner area of the dialog.
* `model`: Model to pass in the `ng-model` attribute of
`mct-include`.
* `parameters`: Parameters to pass in the `parameters` attribute of
`mct-include`.
* `options`: An array of options describing each button at the bottom.
Each option may have the following properties:
* `name`: Human-readable name to display in the button.
* `key`: Machine-readable key, to pass as the result of the resolved
promise when clicked.
* `description`: Description to show in tool tip on hover.

View File

@@ -17,6 +17,10 @@
"key": "overlay-dialog",
"templateUrl": "templates/overlay-dialog.html"
},
{
"key": "overlay-options",
"templateUrl": "templates/overlay-options.html"
},
{
"key": "form-dialog",
"templateUrl": "templates/dialog.html"

View File

@@ -0,0 +1,24 @@
<mct-container key="overlay">
<div class="abs top-bar">
<div class="title">{{ngModel.dialog.title}}</div>
<div class="hint">{{ngModel.dialog.hint}}</div>
</div>
<div class="abs form outline editor">
<div class='abs contents l-dialog'>
<mct-include key="ngModel.dialog.template"
parameters="ngModel.dialog.parameters"
ng-model="ngModel.dialog.model">
</mct-include>
</div>
</div>
<div class="abs bottom-bar">
<a ng-repeat="option in ngModel.dialog.options"
href=''
class="btn lg"
title="{{option.description}}"
ng-click="ngModel.confirm(option.key)"
ng-class="{ major: $first, subtle: !$first }">
{{option.name}}
</a>
</div>
</mct-container>

View File

@@ -26,7 +26,7 @@ define(
dialogVisible = false;
}
function getUserInput(formModel, value) {
function getDialogResponse(key, model, resultGetter) {
// We will return this result as a promise, because user
// input is asynchronous.
var deferred = $q.defer(),
@@ -35,9 +35,9 @@ define(
// Confirm function; this will be passed in to the
// overlay-dialog template and associated with a
// OK button click
function confirm() {
function confirm(value) {
// Pass along the result
deferred.resolve(overlayModel.value);
deferred.resolve(resultGetter ? resultGetter() : value);
// Stop showing the dialog
dismiss();
@@ -51,6 +51,10 @@ define(
dismiss();
}
// Add confirm/cancel callbacks
model.confirm = confirm;
model.cancel = cancel;
if (dialogVisible) {
// Only one dialog should be shown at a time.
// The application design should be such that
@@ -58,26 +62,15 @@ define(
$log.warn([
"Dialog already showing; ",
"unable to show ",
formModel.name
model.name
].join(""));
deferred.reject();
} else {
// To be passed to the overlay-dialog template,
// via ng-model
overlayModel = {
title: formModel.name,
message: formModel.message,
structure: formModel,
value: value,
confirm: confirm,
cancel: cancel
};
// Add the overlay using the OverlayService, which
// will handle actual insertion into the DOM
overlay = overlayService.createOverlay(
"overlay-dialog",
overlayModel
key,
model
);
// Track that a dialog is already visible, to
@@ -88,6 +81,35 @@ define(
return deferred.promise;
}
function getUserInput(formModel, value) {
var overlayModel = {
title: formModel.name,
message: formModel.message,
structure: formModel,
value: value
};
// Provide result from the model
function resultGetter() {
return overlayModel.value;
}
// Show the overlay-dialog
return getDialogResponse(
"overlay-dialog",
overlayModel,
resultGetter
);
}
function getUserChoice(dialogModel) {
// Show the overlay-options dialog
return getDialogResponse(
"overlay-options",
{ dialog: dialogModel }
);
}
return {
/**
* Request user input via a window-modal dialog.
@@ -100,7 +122,14 @@ define(
* user input cannot be obtained (for instance,
* because the user cancelled the dialog)
*/
getUserInput: getUserInput
getUserInput: getUserInput,
/**
* Request that the user chooses from a set of options,
* which will be shown as buttons.
*
* @param dialogModel a description of the dialog to show
*/
getUserChoice: getUserChoice
};
}

View File

@@ -86,6 +86,19 @@ define(
expect(mockDeferred.reject).not.toHaveBeenCalled();
});
it("provides an options dialogs", function () {
var dialogModel = {};
dialogService.getUserChoice(dialogModel);
expect(mockOverlayService.createOverlay).toHaveBeenCalledWith(
'overlay-options',
{
dialog: dialogModel,
confirm: jasmine.any(Function),
cancel: jasmine.any(Function)
}
);
});
});
}
);

View File

@@ -10,7 +10,7 @@
{
"key": "EditController",
"implementation": "controllers/EditController.js",
"depends": [ "$scope", "navigationService" ]
"depends": [ "$scope", "$q", "navigationService" ]
},
{
"key": "EditActionController",

View File

@@ -13,24 +13,18 @@ define(
function SaveAction($location, context) {
var domainObject = context.domainObject;
// Look up the object's "editor.completion" capability;
// Invoke any save behavior introduced by the editor capability;
// this is introduced by EditableDomainObject which is
// used to insulate underlying objects from changes made
// during editing.
function getEditorCapability() {
return domainObject.getCapability("editor");
}
// Invoke any save behavior introduced by the editor.completion
// capability.
function doSave(editor) {
return editor.save();
function doSave() {
return domainObject.getCapability("editor").save();
}
// Discard the current root view (which will be the editing
// UI, which will have been pushed atop the Browise UI.)
function returnToBrowse() {
$location.path("/browse");
return $location.path("/browse");
}
return {
@@ -41,7 +35,7 @@ define(
* cancellation has completed
*/
perform: function () {
return doSave(getEditorCapability()).then(returnToBrowse);
return doSave().then(returnToBrowse);
}
};
}

View File

@@ -29,6 +29,13 @@ define(
cache.markDirty(editableObject);
};
// Delegate refresh to the original object; this avoids refreshing
// the editable instance of the object, and ensures that refresh
// correctly targets the "real" version of the object.
persistence.refresh = function () {
return domainObject.getCapability('persistence').refresh();
};
return persistence;
}

View File

@@ -33,7 +33,7 @@ define(
// removed from the layer which gets dependency
// injection.
function resolvePromise(value) {
return value && value.then ? value : {
return (value && value.then) ? value : {
then: function (callback) {
return resolvePromise(callback(value));
}
@@ -50,19 +50,7 @@ define(
// Persist the underlying domain object
function doPersist() {
return persistenceCapability.persist();
}
// Save any other objects that have been modified in the cache.
// IMPORTANT: This must not be called until after this object
// has been marked as clean.
function saveOthers() {
return cache.saveAll();
}
// Indicate that this object has been saved.
function markClean() {
return cache.markClean(editableObject);
return domainObject.getCapability('persistence').persist();
}
return {
@@ -70,14 +58,15 @@ define(
* Save any changes that have been made to this domain object
* (as well as to others that might have been retrieved and
* modified during the editing session)
* @param {boolean} nonrecursive if true, save only this
* object (and not other objects with associated changes)
* @returns {Promise} a promise that will be fulfilled after
* persistence has completed.
*/
save: function () {
return resolvePromise(doMutate())
.then(doPersist)
.then(markClean)
.then(saveOthers);
save: function (nonrecursive) {
return nonrecursive ?
resolvePromise(doMutate()).then(doPersist) :
resolvePromise(cache.saveAll());
},
/**
* Cancel editing; Discard any changes that have been made to

View File

@@ -14,12 +14,12 @@ define(
* navigated domain object into the scope.
* @constructor
*/
function EditController($scope, navigationService) {
function EditController($scope, $q, navigationService) {
function setNavigation(domainObject) {
// Wrap the domain object such that all mutation is
// confined to edit mode (until Save)
$scope.navigatedObject =
domainObject && new EditableDomainObject(domainObject);
domainObject && new EditableDomainObject(domainObject, $q);
}
setNavigation(navigationService.getNavigation());

View File

@@ -48,7 +48,7 @@ define(
* and provides a "working copy" of the object's
* model to allow changes to be easily cancelled.
*/
function EditableDomainObject(domainObject) {
function EditableDomainObject(domainObject, $q) {
// The cache will hold all domain objects reached from
// the initial EditableDomainObject; this ensures that
// different versions of the same editable domain object
@@ -81,7 +81,7 @@ define(
return editableObject;
}
cache = new EditableDomainObjectCache(EditableDomainObjectImpl);
cache = new EditableDomainObjectCache(EditableDomainObjectImpl, $q);
return cache.getEditableObject(domainObject);
}

View File

@@ -29,10 +29,11 @@ define(
* constructor function which takes a regular domain object as
* an argument, and returns an editable domain object as its
* result.
* @param $q Angular's $q, for promise handling
* @constructor
* @memberof module:editor/object/editable-domain-object-cache
*/
function EditableDomainObjectCache(EditableDomainObject) {
function EditableDomainObjectCache(EditableDomainObject, $q) {
var cache = new EditableModelCache(),
dirty = {},
root;
@@ -88,23 +89,20 @@ define(
* Initiate a save on all objects that have been cached.
*/
saveAll: function () {
var object;
// Get a list of all dirty objects
var objects = Object.keys(dirty).map(function (k) {
return dirty[k];
});
// Clear dirty set, since we're about to save.
dirty = {};
// Most save logic is handled by the "editor.completion"
// capability, but this in turn will typically invoke
// Save All. An infinite loop is avoided by marking
// objects as clean as we go.
while (Object.keys(dirty).length > 0) {
// Pick the first dirty object
object = dirty[Object.keys(dirty)[0]];
// Mark non-dirty to avoid successive invocations
this.markClean(object);
// Invoke its save behavior
object.getCapability('editor').save();
}
// capability, so that is delegated here.
return $q.all(objects.map(function (object) {
// Save; pass a nonrecursive flag to avoid looping
return object.getCapability('editor').save(true);
}));
}
};
}

View File

@@ -15,7 +15,7 @@ define(
beforeEach(function () {
mockPersistence = jasmine.createSpyObj(
"persistence",
[ "persist" ]
[ "persist", "refresh" ]
);
mockEditableObject = jasmine.createSpyObj(
"editableObject",
@@ -30,6 +30,8 @@ define(
[ "markDirty" ]
);
mockDomainObject.getCapability.andReturn(mockPersistence);
capability = new EditablePersistenceCapability(
mockPersistence,
mockEditableObject,
@@ -49,6 +51,18 @@ define(
expect(mockPersistence.persist).not.toHaveBeenCalled();
});
it("refreshes using the original domain object's persistence", function () {
// Refreshing needs to delegate via the unwrapped domain object.
// Otherwise, only the editable version of the object will be updated;
// we instead want the real version of the object to receive these
// changes.
expect(mockDomainObject.getCapability).not.toHaveBeenCalled();
expect(mockPersistence.refresh).not.toHaveBeenCalled();
capability.refresh();
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('persistence');
expect(mockPersistence.refresh).toHaveBeenCalled();
});
});
}
);

View File

@@ -5,7 +5,7 @@ define(
function (EditorCapability) {
"use strict";
describe("An editable context capability", function () {
describe("The editor capability", function () {
var mockPersistence,
mockEditableObject,
mockDomainObject,
@@ -32,6 +32,8 @@ define(
);
mockCallback = jasmine.createSpy("callback");
mockDomainObject.getCapability.andReturn(mockPersistence);
model = { someKey: "some value", x: 42 };
capability = new EditorCapability(
@@ -42,8 +44,8 @@ define(
);
});
it("mutates the real domain object on save", function () {
capability.save().then(mockCallback);
it("mutates the real domain object on nonrecursive save", function () {
capability.save(true).then(mockCallback);
// Wait for promise to resolve
waitsFor(function () {
@@ -60,19 +62,6 @@ define(
});
});
it("marks the saved object as clean in the editing cache", function () {
capability.save().then(mockCallback);
// Wait for promise to resolve
waitsFor(function () {
return mockCallback.calls.length > 0;
}, 250);
runs(function () {
expect(mockCache.markClean).toHaveBeenCalledWith(mockEditableObject);
});
});
it("tells the cache to save others", function () {
capability.save().then(mockCallback);

View File

@@ -7,6 +7,7 @@ define(
describe("The Edit mode controller", function () {
var mockScope,
mockQ,
mockNavigationService,
mockObject,
mockCapability,
@@ -17,6 +18,7 @@ define(
"$scope",
[ "$on" ]
);
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[ "getNavigation", "addListener", "removeListener" ]
@@ -37,6 +39,7 @@ define(
controller = new EditController(
mockScope,
mockQ,
mockNavigationService
);
});

View File

@@ -1,4 +1,4 @@
/*global define,describe,it,expect,beforeEach*/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/objects/EditableDomainObjectCache"],
@@ -10,6 +10,7 @@ define(
var captured,
completionCapability,
object,
mockQ,
cache;
@@ -33,6 +34,7 @@ define(
}
beforeEach(function () {
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
captured = {};
completionCapability = {
save: function () {
@@ -40,7 +42,7 @@ define(
}
};
cache = new EditableDomainObjectCache(WrapObject);
cache = new EditableDomainObjectCache(WrapObject, mockQ);
});
it("wraps objects using provided constructor", function () {