Merge branch 'open1033' into open-master
This commit is contained in:
27
platform/commonUI/dialog/README.md
Normal file
27
platform/commonUI/dialog/README.md
Normal 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.
|
||||
@@ -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"
|
||||
|
||||
24
platform/commonUI/dialog/res/templates/overlay-options.html
Normal file
24
platform/commonUI/dialog/res/templates/overlay-options.html
Normal 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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"key": "EditController",
|
||||
"implementation": "controllers/EditController.js",
|
||||
"depends": [ "$scope", "navigationService" ]
|
||||
"depends": [ "$scope", "$q", "navigationService" ]
|
||||
},
|
||||
{
|
||||
"key": "EditActionController",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user