From d1ea5726e281e55ae8e2f438a7c34cc08cbca6d3 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 15:07:33 -0700 Subject: [PATCH 01/12] [Core] Add 'relationship' capability Add a general-purpose capability for typed relationships. Unlike composition, these do not appear in the tree, but instead appear only in user interfaces which specifically look for these typed relationships. WTD-1007. --- .../capabilities/RelationshipCapability.js | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 platform/core/src/capabilities/RelationshipCapability.js diff --git a/platform/core/src/capabilities/RelationshipCapability.js b/platform/core/src/capabilities/RelationshipCapability.js new file mode 100644 index 0000000000..fda5ddf149 --- /dev/null +++ b/platform/core/src/capabilities/RelationshipCapability.js @@ -0,0 +1,118 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Relationship capability. Describes a domain objects relationship + * to other domain objects within the system, and provides a way to + * access related objects. + * + * For most cases, this is not the capability to use; the + * `composition` capability describes the more general relationship + * between objects typically seen (e.g. in the tree.) This capability + * is instead intended for the more unusual case of relationships + * which are not intended to appear in the tree, but are instead + * intended only for special, limited usage. + * + * @constructor + */ + function RelationshipCapability($injector, domainObject) { + var objectService, + lastPromise = {}, + lastModified; + + // Get a reference to the object service from $injector + function injectObjectService() { + objectService = $injector.get("objectService"); + return objectService; + } + + // Get a reference to the object service (either cached or + // from the injector) + function getObjectService() { + return objectService || injectObjectService(); + } + + // Promise this domain object's composition (an array of domain + // object instances corresponding to ids in its model.) + function promiseRelationships(key) { + var model = domainObject.getModel(), + ids; + + // Package objects as an array + function packageObject(objects) { + return ids.map(function (id) { + return objects[id]; + }).filter(function (obj) { + return obj; + }); + } + + // Clear cached promises if modification has occurred + if (lastModified !== model.modified) { + lastPromise = {}; + lastModified = model.modified; + } + + // Make a new request if needed + if (!lastPromise[key]) { + ids = (model.relationships || {})[key] || []; + lastModified = model.modified; + // Load from the underlying object service + lastPromise[key] = getObjectService().getObjects(ids) + .then(packageObject); + } + + return lastPromise; + } + + // List types of relationships which this object has + function listRelationships() { + var relationships = + (domainObject.getModel() || {}).relationships || {}; + + // Check if this key really does expose an array of ids + // (to filter out malformed relationships) + function isArray(key) { + return Array.isArray(relationships[key]); + } + + return Object.keys(relationships).filter(isArray).sort(); + } + + return { + /** + * List all types of relationships exposed by this + * object. + * @returns {string[]} a list of all relationship types + */ + list: listRelationships, + /** + * Request related objects, with a given relationship type. + * This will typically require asynchronous lookup, so this + * returns a promise. + * @param {string} key the type of relationship + * @returns {Promise.} a promise for related + * domain objects + */ + getRelatedObjects: promiseRelationships + }; + } + + /** + * Test to determine whether or not this capability should be exposed + * by a domain object based on its model. Checks for the presence of + * a `relationships` field, that must be an object. + * @param model the domain object model + * @returns {boolean} true if this object has relationships + */ + RelationshipCapability.appliesTo = function (model) { + return Array.isArray((model || {}).relationships); + }; + + return RelationshipCapability; + } +); \ No newline at end of file From 97fe3787511a2cface04573e909c0b592c97448a Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 15:22:23 -0700 Subject: [PATCH 02/12] [Core] Test relationship capability Test relationship capability, WTD-1007. --- .../capabilities/RelationshipCapability.js | 4 +- .../RelationshipCapabilitySpec.js | 125 ++++++++++++++++++ platform/core/test/suite.json | 1 + 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 platform/core/test/capabilities/RelationshipCapabilitySpec.js diff --git a/platform/core/src/capabilities/RelationshipCapability.js b/platform/core/src/capabilities/RelationshipCapability.js index fda5ddf149..bebc90aba7 100644 --- a/platform/core/src/capabilities/RelationshipCapability.js +++ b/platform/core/src/capabilities/RelationshipCapability.js @@ -89,7 +89,7 @@ define( * object. * @returns {string[]} a list of all relationship types */ - list: listRelationships, + listRelationships: listRelationships, /** * Request related objects, with a given relationship type. * This will typically require asynchronous lookup, so this @@ -110,7 +110,7 @@ define( * @returns {boolean} true if this object has relationships */ RelationshipCapability.appliesTo = function (model) { - return Array.isArray((model || {}).relationships); + return !!(model || {}).relationships; }; return RelationshipCapability; diff --git a/platform/core/test/capabilities/RelationshipCapabilitySpec.js b/platform/core/test/capabilities/RelationshipCapabilitySpec.js new file mode 100644 index 0000000000..8421531305 --- /dev/null +++ b/platform/core/test/capabilities/RelationshipCapabilitySpec.js @@ -0,0 +1,125 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * CompositionCapabilitySpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../../src/capabilities/RelationshipCapability"], + function (RelationshipCapability) { + "use strict"; + + var DOMAIN_OBJECT_METHODS = [ + "getId", + "getModel", + "getCapability", + "hasCapability", + "useCapability" + ]; + + describe("The relationship capability", function () { + var mockDomainObject, + mockInjector, + mockObjectService, + relationship; + + // Composition Capability makes use of promise chaining, + // so support that, but don't introduce complication of + // native promises. + function mockPromise(value) { + return { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + beforeEach(function () { + mockDomainObject = jasmine.createSpyObj( + "domainObject", + DOMAIN_OBJECT_METHODS + ); + + mockObjectService = jasmine.createSpyObj( + "objectService", + [ "getObjects" ] + ); + + mockInjector = { + get: function (name) { + return (name === "objectService") && mockObjectService; + } + }; + + mockObjectService.getObjects.andReturn(mockPromise([])); + + relationship = new RelationshipCapability( + mockInjector, + mockDomainObject + ); + }); + + it("applies only to models with a 'relationships' field", function () { + expect(RelationshipCapability.appliesTo({ relationships: {} })) + .toBeTruthy(); + expect(RelationshipCapability.appliesTo({})) + .toBeFalsy(); + }); + + it("requests ids found in model's composition from the object service", function () { + var ids = [ "a", "b", "c", "xyz" ]; + + mockDomainObject.getModel.andReturn({ relationships: { xyz: ids } }); + + relationship.getRelatedObjects('xyz'); + + expect(mockObjectService.getObjects).toHaveBeenCalledWith(ids); + }); + + it("provides a list of relationship types", function () { + mockDomainObject.getModel.andReturn({ relationships: { + abc: [ 'a', 'b' ], + def: "not an array, should be ignored", + xyz: [] + } }); + expect(relationship.listRelationships()).toEqual(['abc', 'xyz']); + }); + + it("avoids redundant requests", function () { + // Lookups can be expensive, so this capability + // should have some self-caching + var response; + + mockDomainObject.getModel + .andReturn({ relationships: { xyz: ['a'] } }); + + // Call twice; response should be the same object instance + expect(relationship.getRelatedObjects('xyz')) + .toBe(relationship.getRelatedObjects('xyz')); + + // Should have only made one call + expect(mockObjectService.getObjects.calls.length) + .toEqual(1); + }); + + it("makes new requests on modification", function () { + // Lookups can be expensive, so this capability + // should have some self-caching + var response, testModel; + + testModel = { relationships: { xyz: ['a'] } }; + + mockDomainObject.getModel.andReturn(testModel); + + // Call twice, but as if modification had occurred in between + relationship.getRelatedObjects('xyz'); + testModel.modified = 123; + relationship.getRelatedObjects('xyz'); + + // Should have only made one call + expect(mockObjectService.getObjects.calls.length) + .toEqual(2); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json index 335b86d190..68990a191e 100644 --- a/platform/core/test/suite.json +++ b/platform/core/test/suite.json @@ -11,6 +11,7 @@ "capabilities/DelegationCapability", "capabilities/MutationCapability", "capabilities/PersistenceCapability", + "capabilities/RelationshipCapability", "models/ModelAggregator", "models/PersistedModelProvider", From bbe26cd06c5b6d18ba96f87ceeaf0c1c30294e96 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 15:29:25 -0700 Subject: [PATCH 03/12] [Core] Edit-wrap relationship capability Wrap objects retrieved via the relationship capability with Edit mode caching etc, for WTD-1007. --- .../capabilities/EditableContextCapability.js | 2 +- .../EditableRelationshipCapability.js | 36 +++++++++++++ .../edit/src/objects/EditableDomainObject.js | 8 ++- .../EditableRelationshipCapabilitySpec.js | 54 +++++++++++++++++++ platform/commonUI/edit/test/suite.json | 1 + 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js create mode 100644 platform/commonUI/edit/test/capabilities/EditableRelationshipCapabilitySpec.js diff --git a/platform/commonUI/edit/src/capabilities/EditableContextCapability.js b/platform/commonUI/edit/src/capabilities/EditableContextCapability.js index b8658aa19a..a21dc9ba31 100644 --- a/platform/commonUI/edit/src/capabilities/EditableContextCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditableContextCapability.js @@ -28,7 +28,7 @@ define( editableObject, domainObject, cache, - true // Not idempotent + true // Idempotent ); }; } diff --git a/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js b/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js new file mode 100644 index 0000000000..cc0082757a --- /dev/null +++ b/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js @@ -0,0 +1,36 @@ +/*global define*/ + + +define( + ['./EditableLookupCapability'], + function (EditableLookupCapability) { + 'use strict'; + + /** + * Wrapper for the "relationship" capability; + * ensures that any domain objects reachable in Edit mode + * are also wrapped as EditableDomainObjects. + * + * Meant specifically for use by EditableDomainObject and the + * associated cache; the constructor signature is particular + * to a pattern used there and may contain unused arguments. + */ + return function EditableRelationshipCapability( + relationshipCapability, + editableObject, + domainObject, + cache + ) { + // This is a "lookup" style capability (it looks up other + // domain objects), but we do not want to return the same + // specific value every time (composition may change) + return new EditableLookupCapability( + relationshipCapability, + editableObject, + domainObject, + cache, + false // Not idempotent + ); + }; + } +); \ No newline at end of file diff --git a/platform/commonUI/edit/src/objects/EditableDomainObject.js b/platform/commonUI/edit/src/objects/EditableDomainObject.js index a6b3d503d7..4e3363c8e9 100644 --- a/platform/commonUI/edit/src/objects/EditableDomainObject.js +++ b/platform/commonUI/edit/src/objects/EditableDomainObject.js @@ -14,6 +14,7 @@ define( '../capabilities/EditablePersistenceCapability', '../capabilities/EditableContextCapability', '../capabilities/EditableCompositionCapability', + '../capabilities/EditableRelationshipCapability', '../capabilities/EditorCapability', './EditableDomainObjectCache' ], @@ -21,6 +22,7 @@ define( EditablePersistenceCapability, EditableContextCapability, EditableCompositionCapability, + EditableRelationshipCapability, EditorCapability, EditableDomainObjectCache ) { @@ -30,6 +32,7 @@ define( persistence: EditablePersistenceCapability, context: EditableContextCapability, composition: EditableCompositionCapability, + relationship: EditableRelationshipCapability, editor: EditorCapability }; @@ -64,7 +67,10 @@ define( // Override certain capabilities editableObject.getCapability = function (name) { var delegateArguments = getDelegateArguments(name, arguments), - capability = domainObject.getCapability.apply(this, delegateArguments), + capability = domainObject.getCapability.apply( + this, + delegateArguments + ), factory = capabilityFactories[name]; return (factory && capability) ? diff --git a/platform/commonUI/edit/test/capabilities/EditableRelationshipCapabilitySpec.js b/platform/commonUI/edit/test/capabilities/EditableRelationshipCapabilitySpec.js new file mode 100644 index 0000000000..65d044d7c8 --- /dev/null +++ b/platform/commonUI/edit/test/capabilities/EditableRelationshipCapabilitySpec.js @@ -0,0 +1,54 @@ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +define( + ["../../src/capabilities/EditableRelationshipCapability"], + function (EditableRelationshipCapability) { + "use strict"; + + describe("An editable relationship capability", function () { + var mockContext, + mockEditableObject, + mockDomainObject, + mockTestObject, + someValue, + mockFactory, + capability; + + beforeEach(function () { + // EditableContextCapability should watch ALL + // methods for domain objects, so give it an + // arbitrary interface to wrap. + mockContext = + jasmine.createSpyObj("context", [ "getDomainObject" ]); + mockTestObject = jasmine.createSpyObj( + "domainObject", + [ "getId", "getModel", "getCapability" ] + ); + mockFactory = + jasmine.createSpyObj("factory", ["getEditableObject"]); + + someValue = { x: 42 }; + + mockContext.getDomainObject.andReturn(mockTestObject); + mockFactory.getEditableObject.andReturn(someValue); + + capability = new EditableRelationshipCapability( + mockContext, + mockEditableObject, + mockDomainObject, + mockFactory + ); + + }); + + // Most behavior is tested for EditableLookupCapability, + // so just verify that this isse + it("presumes non-idempotence of its wrapped capability", function () { + expect(capability.getDomainObject()) + .toEqual(capability.getDomainObject()); + expect(mockContext.getDomainObject.calls.length).toEqual(2); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/edit/test/suite.json b/platform/commonUI/edit/test/suite.json index 5744ff8ea8..da4fbb899e 100644 --- a/platform/commonUI/edit/test/suite.json +++ b/platform/commonUI/edit/test/suite.json @@ -12,6 +12,7 @@ "capabilities/EditableContextCapability", "capabilities/EditableLookupCapability", "capabilities/EditablePersistenceCapability", + "capabilities/EditableRelationshipCapability", "capabilities/EditorCapability", "objects/EditableDomainObject", "objects/EditableDomainObjectCache", From 66c8f5fe762917bccc81f5870b8d6ef09b72ec21 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 16:42:46 -0700 Subject: [PATCH 04/12] [Forms] Add selector template Add template for domain object selector, needed for dialogs related to WTD-987. --- .../res/templates/controls/selector.html | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 platform/commonUI/general/res/templates/controls/selector.html diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html new file mode 100644 index 0000000000..a867ad0516 --- /dev/null +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -0,0 +1,39 @@ +
+
+
Available
+
+ + M +
+ +
+ Showing {{shown}} of {{count}} available options. +
+ +
+ +
+
+
+
+ > + < +
+
+
+
Selected
+
+ + M +
+
+ Showing {{shown}} of {{count}} available options. +
+ +
+
+
+
\ No newline at end of file From 67bb110dc8d11cca390b9231ef79dfb005767794 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 16:54:22 -0700 Subject: [PATCH 05/12] [Forms] Update selector template Update selector template to request necessary information to populate domain object selector, as shown in UI diagrams for WTD-987. --- .../res/templates/controls/selector.html | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html index a867ad0516..b8ec1a2849 100644 --- a/platform/commonUI/general/res/templates/controls/selector.html +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -1,5 +1,5 @@
+ ng-controller="SelectorController as selector">
Available
@@ -14,26 +14,45 @@
- + +
Selected
Showing {{shown}} of {{count}} available options.
+ +
\ No newline at end of file From 4146a2ad016ceb1f097823b491588be012c0cb48 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 17:03:28 -0700 Subject: [PATCH 06/12] [Forms] Begin adding controller for selector Begin implementing controller to populate the domain object selector, added for WTD-987. --- .../src/controllers/SelectorController.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 platform/commonUI/general/src/controllers/SelectorController.js diff --git a/platform/commonUI/general/src/controllers/SelectorController.js b/platform/commonUI/general/src/controllers/SelectorController.js new file mode 100644 index 0000000000..cec420f682 --- /dev/null +++ b/platform/commonUI/general/src/controllers/SelectorController.js @@ -0,0 +1,56 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Controller for the domain object selector control. + * @constructor + * @param {ObjectService} objectService service from which to + * read domain objects + * @param $scope Angular scope for this controller + */ + function SelectorController(objectService, $scope) { + var treeModel = {}, + previousSelected; + + // For watch; look at the user's selection in the tree + function getTreeSelection() { + return treeModel.selectedObject; + } + + // Get the value of the field being edited + function getField() { + return $scope.ngModel[$scope.field]; + } + + // Check that a selection is of the valid type + function validateTreeSelection(selectedObject) { + var type = selectedObject && + selectedObject.getCapability('type'); + + // Delegate type-checking to the capability... + if (!type || !type.instanceOf($scope.structure.type)) { + treeModel.selectedObject = previousSelected; + } + + // Track current selection to restore it if an invalid + // selection is made later. + previousSelected = treeModel.selectedObject; + } + + // Reject attempts to select objects of the wrong type + $scope.$watch(getTreeSelection, validateTreeSelection); + + return { + // Expose tree model for use in template directly + treeModel: treeModel + }; + } + + + return SelectorController; + } +); \ No newline at end of file From 66e0d2fcfac948ec95d49ffcfd3165b71d6a0da8 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 17:16:31 -0700 Subject: [PATCH 07/12] [Forms] Complete initial selector Complete initial implementation of the domain object selector for WTD-987. --- .../res/templates/controls/selector.html | 4 +- .../src/controllers/SelectorController.js | 74 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html index b8ec1a2849..7411f2ac10 100644 --- a/platform/commonUI/general/res/templates/controls/selector.html +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -27,7 +27,7 @@ > + ng-click="selector.deselect(selector.listModel.selectedObject)"> < @@ -48,7 +48,7 @@
diff --git a/platform/commonUI/general/src/controllers/SelectorController.js b/platform/commonUI/general/src/controllers/SelectorController.js index cec420f682..1ec4fcac46 100644 --- a/platform/commonUI/general/src/controllers/SelectorController.js +++ b/platform/commonUI/general/src/controllers/SelectorController.js @@ -5,6 +5,8 @@ define( function () { "use strict"; + var ROOT_ID = "ROOT"; + /** * Controller for the domain object selector control. * @constructor @@ -14,6 +16,9 @@ define( */ function SelectorController(objectService, $scope) { var treeModel = {}, + listModel = {}, + selectedObjects = [], + rootObject, previousSelected; // For watch; look at the user's selection in the tree @@ -26,6 +31,16 @@ define( return $scope.ngModel[$scope.field]; } + // Get the value of the field being edited + function setField(value) { + $scope.ngModel[$scope.field] = value; + } + + // Store root object for subsequent exposure to template + function storeRoot(objects) { + rootObject = objects[ROOT_ID]; + } + // Check that a selection is of the valid type function validateTreeSelection(selectedObject) { var type = selectedObject && @@ -41,12 +56,67 @@ define( previousSelected = treeModel.selectedObject; } + // Update the right-hand list of currently-selected objects + function updateList(ids) { + function updateSelectedObjects(objects) { + // Look up from the + function getObject(id) { return objects[id]; } + selectedObjects = ids.filter(getObject).map(getObject); + } + + // Look up objects by id, then populate right-hand list + objectService.get(ids).then(updateSelectedObjects); + } + // Reject attempts to select objects of the wrong type $scope.$watch(getTreeSelection, validateTreeSelection); + // Make sure right-hand list matches underlying model + $scope.$watchCollection(getField, updateList); + + // Look up root object, then store it + objectService.get(ROOT_ID).then(storeRoot); + return { - // Expose tree model for use in template directly - treeModel: treeModel + /** + * Get the root object to show in the left-hand tree. + * @returns {DomainObject} the root object + */ + root: function () { + return rootObject; + }, + /** + * Add a domain object to the list of selected objects. + * @param {DomainObject} the domain object to select + */ + select: function (domainObject) { + var id = domainObject && domainObject.getId(), + list = getField() || []; + // Only select if we have a valid id, + // and it isn't already selected + if (id && list.indexOf(id) === -1) { + setField(list.concat([id])); + } + }, + /** + * Remove a domain object from the list of selected objects. + * @param {DomainObject} the domain object to select + */ + deselect: function (domainObject) { + var id = domainObject && domainObject.getId(), + list = getField() || []; + // Only change if this was a valid id, + // for an object which was already selected + if (id && list.indexOf(id) !== -1) { + // Filter it out of the current field + setField(list.filter(function (otherId) { + return otherId !== id; + })); + } + }, + // Expose tree/list model for use in template directly + treeModel: treeModel, + listModel: listModel }; } From 26ba75f6363f9a73854bdbab0d5d51a52f6215ad Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 9 Mar 2015 17:26:37 -0700 Subject: [PATCH 08/12] [Forms] Update selector controller Update controller for domain object selector, for WTD-987. --- platform/commonUI/general/bundle.json | 11 +++++++++++ .../general/res/templates/controls/selector.html | 4 ++-- .../general/src/controllers/SelectorController.js | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json index bd94eaabdf..3dfc0c0447 100644 --- a/platform/commonUI/general/bundle.json +++ b/platform/commonUI/general/bundle.json @@ -88,6 +88,11 @@ { "key": "SplitPaneController", "implementation": "controllers/SplitPaneController.js" + }, + { + "key": "SelectorController", + "implementation": "controllers/SelectorController.js", + "depends": [ "objectService", "$scope" ] } ], "directives": [ @@ -185,6 +190,12 @@ "uses": [ "view" ] } ], + "controls": [ + { + "key": "selector", + "templateUrl": "templates/controls/selector.html" + } + ], "licenses": [ { "name": "Modernizr", diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html index 7411f2ac10..070126e6c1 100644 --- a/platform/commonUI/general/res/templates/controls/selector.html +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -22,11 +22,11 @@
- > - < diff --git a/platform/commonUI/general/src/controllers/SelectorController.js b/platform/commonUI/general/src/controllers/SelectorController.js index 1ec4fcac46..7ef26cd2e6 100644 --- a/platform/commonUI/general/src/controllers/SelectorController.js +++ b/platform/commonUI/general/src/controllers/SelectorController.js @@ -28,7 +28,7 @@ define( // Get the value of the field being edited function getField() { - return $scope.ngModel[$scope.field]; + return $scope.ngModel[$scope.field] || []; } // Get the value of the field being edited @@ -65,7 +65,7 @@ define( } // Look up objects by id, then populate right-hand list - objectService.get(ids).then(updateSelectedObjects); + objectService.getObjects(ids).then(updateSelectedObjects); } // Reject attempts to select objects of the wrong type @@ -75,7 +75,7 @@ define( $scope.$watchCollection(getField, updateList); // Look up root object, then store it - objectService.get(ROOT_ID).then(storeRoot); + objectService.getObjects([ROOT_ID]).then(storeRoot); return { /** @@ -112,8 +112,17 @@ define( setField(list.filter(function (otherId) { return otherId !== id; })); + // Clear the current list selection + delete listModel.selectedObject; } }, + /** + * Get the currently-selected domain objects. + * @returns {DomainObject[]} the current selection + */ + selected: function () { + return selectedObjects; + }, // Expose tree/list model for use in template directly treeModel: treeModel, listModel: listModel From 1efe80f12b92b0947b68b850ba8348997eb3c845 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 10 Mar 2015 07:34:43 -0700 Subject: [PATCH 09/12] [Forms] Hide filter in selector Hide filter shown in selector control, added for WTD-987. --- .../commonUI/general/res/templates/controls/selector.html | 8 ++++---- platform/features/layout/bundle.json | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html index 070126e6c1..4b54a61624 100644 --- a/platform/commonUI/general/res/templates/controls/selector.html +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -2,7 +2,7 @@ ng-controller="SelectorController as selector">
Available
-
+
Selected
-
+
Date: Tue, 10 Mar 2015 07:40:01 -0700 Subject: [PATCH 10/12] [Forms] Add placeholder spec Add placeholder spec for SelectorController, WTD-987. --- .../controllers/SelectorControllerSpec.js | 45 +++++++++++++++++++ platform/commonUI/general/test/suite.json | 1 + 2 files changed, 46 insertions(+) create mode 100644 platform/commonUI/general/test/controllers/SelectorControllerSpec.js diff --git a/platform/commonUI/general/test/controllers/SelectorControllerSpec.js b/platform/commonUI/general/test/controllers/SelectorControllerSpec.js new file mode 100644 index 0000000000..30648a1d8e --- /dev/null +++ b/platform/commonUI/general/test/controllers/SelectorControllerSpec.js @@ -0,0 +1,45 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/controllers/SelectorController"], + function (SelectorController) { + "use strict"; + + describe("The controller for the 'selector' control", function () { + var mockObjectService, + mockScope, + controller; + + function promiseOf(v) { + return (v || {}).then ? v : { + then: function (callback) { + return promiseOf(callback(v)); + } + }; + } + + beforeEach(function () { + mockObjectService = jasmine.createSpyObj( + 'objectService', + ['getObjects'] + ); + mockScope = jasmine.createSpyObj( + '$scope', + ['$watch', '$watchCollection'] + ); + + mockObjectService.getObjects.andReturn(promiseOf({})); + + controller = new SelectorController( + mockObjectService, + mockScope + ); + }); + + it("loads the root object", function () { + expect(mockObjectService.getObjects) + .toHaveBeenCalledWith(["ROOT"]); + }); + }); + } +); diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json index 58d94a4d95..e8c9674b44 100644 --- a/platform/commonUI/general/test/suite.json +++ b/platform/commonUI/general/test/suite.json @@ -4,6 +4,7 @@ "controllers/ClickAwayController", "controllers/ContextMenuController", "controllers/GetterSetterController", + "controllers/SelectorController", "controllers/SplitPaneController", "controllers/ToggleController", "controllers/TreeNodeController", From 411deb9f4fcbc96e87fe1afc54511ebdc57f6be7 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 10 Mar 2015 08:59:56 -0700 Subject: [PATCH 11/12] [Forms] Wrap selector in ul Present selected elements in an object selector as an unordered list, for WTD-987. --- .../general/res/templates/controls/selector.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/platform/commonUI/general/res/templates/controls/selector.html b/platform/commonUI/general/res/templates/controls/selector.html index 4b54a61624..538b6b2a83 100644 --- a/platform/commonUI/general/res/templates/controls/selector.html +++ b/platform/commonUI/general/res/templates/controls/selector.html @@ -47,12 +47,16 @@
- - +
    +
  • + + + +
  • +
\ No newline at end of file From d8d28d892d64eb0a9bd7b9494dc4c71fab215341 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 10 Mar 2015 09:32:40 -0700 Subject: [PATCH 12/12] [Forms] Add test cases Add test cases for the SelectorController (used to support dialog in WTD-987) to satisfy coverage standards. --- .../controllers/SelectorControllerSpec.js | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/platform/commonUI/general/test/controllers/SelectorControllerSpec.js b/platform/commonUI/general/test/controllers/SelectorControllerSpec.js index 30648a1d8e..0f03e72c45 100644 --- a/platform/commonUI/general/test/controllers/SelectorControllerSpec.js +++ b/platform/commonUI/general/test/controllers/SelectorControllerSpec.js @@ -8,6 +8,9 @@ define( describe("The controller for the 'selector' control", function () { var mockObjectService, mockScope, + mockDomainObject, + mockType, + mockDomainObjects, controller; function promiseOf(v) { @@ -18,6 +21,15 @@ define( }; } + function makeMockObject(id) { + var mockObject = jasmine.createSpyObj( + 'object-' + id, + [ 'getId' ] + ); + mockObject.getId.andReturn(id); + return mockObject; + } + beforeEach(function () { mockObjectService = jasmine.createSpyObj( 'objectService', @@ -27,8 +39,24 @@ define( '$scope', ['$watch', '$watchCollection'] ); + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + [ 'getCapability', 'hasCapability' ] + ); + mockType = jasmine.createSpyObj( + 'type', + [ 'instanceOf' ] + ); + mockDomainObjects = {}; - mockObjectService.getObjects.andReturn(promiseOf({})); + [ "ROOT", "abc", "def", "xyz" ].forEach(function (id) { + mockDomainObjects[id] = makeMockObject(id); + }); + + mockDomainObject.getCapability.andReturn(mockType); + mockObjectService.getObjects.andReturn(promiseOf(mockDomainObjects)); + mockScope.field = "testField"; + mockScope.ngModel = {}; controller = new SelectorController( mockObjectService, @@ -40,6 +68,96 @@ define( expect(mockObjectService.getObjects) .toHaveBeenCalledWith(["ROOT"]); }); + + it("watches for changes in selection in left-hand tree", function () { + var testObject = { a: 123, b: 456 }; + // This test is sensitive to ordering of watch calls + expect(mockScope.$watch.calls.length).toEqual(1); + // Make sure we're watching the correct object + controller.treeModel.selectedObject = testObject; + expect(mockScope.$watch.calls[0].args[0]()).toBe(testObject); + }); + + it("watches for changes in controlled property", function () { + var testValue = [ "a", "b", 1, 2 ]; + // This test is sensitive to ordering of watch calls + expect(mockScope.$watchCollection.calls.length).toEqual(1); + // Make sure we're watching the correct object + mockScope.ngModel = { testField: testValue }; + expect(mockScope.$watchCollection.calls[0].args[0]()).toBe(testValue); + }); + + it("rejects selection of incorrect types", function () { + mockScope.structure = { type: "someType" }; + mockType.instanceOf.andReturn(false); + controller.treeModel.selectedObject = mockDomainObject; + // Fire the watch + mockScope.$watch.calls[0].args[1](mockDomainObject); + // Should have cleared the selection + expect(controller.treeModel.selectedObject).toBeUndefined(); + // Verify interaction (that instanceOf got a useful argument) + expect(mockType.instanceOf).toHaveBeenCalledWith("someType"); + }); + + it("permits selection of matching types", function () { + mockScope.structure = { type: "someType" }; + mockType.instanceOf.andReturn(true); + controller.treeModel.selectedObject = mockDomainObject; + // Fire the watch + mockScope.$watch.calls[0].args[1](mockDomainObject); + // Should have preserved the selection + expect(controller.treeModel.selectedObject).toEqual(mockDomainObject); + // Verify interaction (that instanceOf got a useful argument) + expect(mockType.instanceOf).toHaveBeenCalledWith("someType"); + }); + + it("loads objects when the underlying list changes", function () { + var testIds = [ "abc", "def", "xyz" ]; + // This test is sensitive to ordering of watch calls + expect(mockScope.$watchCollection.calls.length).toEqual(1); + // Make sure we're watching the correct object + mockScope.ngModel = { testField: testIds }; + // Fire the watch + mockScope.$watchCollection.calls[0].args[1](testIds); + // Should have loaded the corresponding objects + expect(mockObjectService.getObjects).toHaveBeenCalledWith(testIds); + }); + + it("exposes the root object to populate the left-hand tree", function () { + expect(controller.root()).toEqual(mockDomainObjects.ROOT); + }); + + it("adds objects to the underlying model", function () { + expect(mockScope.ngModel.testField).toBeUndefined(); + controller.select(mockDomainObjects.def); + expect(mockScope.ngModel.testField).toEqual(["def"]); + controller.select(mockDomainObjects.abc); + expect(mockScope.ngModel.testField).toEqual(["def", "abc"]); + }); + + it("removes objects to the underlying model", function () { + controller.select(mockDomainObjects.def); + controller.select(mockDomainObjects.abc); + expect(mockScope.ngModel.testField).toEqual(["def", "abc"]); + controller.deselect(mockDomainObjects.def); + expect(mockScope.ngModel.testField).toEqual(["abc"]); + }); + + it("provides a list of currently-selected objects", function () { + // Verify precondition + expect(controller.selected()).toEqual([]); + // Select some objects + controller.select(mockDomainObjects.def); + controller.select(mockDomainObjects.abc); + // Fire the watch for the id changes... + mockScope.$watchCollection.calls[0].args[1]( + mockScope.$watchCollection.calls[0].args[0]() + ); + // Should have loaded and exposed those objects + expect(controller.selected()).toEqual( + [mockDomainObjects.def, mockDomainObjects.abc] + ); + }); }); } );