mirror of
https://github.com/nasa/openmct.git
synced 2025-04-21 09:31:24 +00:00
Merge conflicts with wtd593
This commit is contained in:
commit
a9d6009610
bundles.json
example/generator
platform
commonUI
core
features/layout
forms
representation/src
12
bundles.json
12
bundles.json
@ -6,5 +6,13 @@
|
||||
"platform/commonUI/edit",
|
||||
"platform/commonUI/dialog",
|
||||
"platform/commonUI/general",
|
||||
"platform/persistence"
|
||||
]
|
||||
"platform/telemetry",
|
||||
"platform/features/layout",
|
||||
"platform/features/plot",
|
||||
"platform/features/scrolling",
|
||||
"platform/forms",
|
||||
"platform/persistence",
|
||||
|
||||
"example/generator",
|
||||
"example/persistence"
|
||||
]
|
||||
|
@ -26,8 +26,8 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"label": "Period",
|
||||
"control": "_textfield",
|
||||
"name": "Period",
|
||||
"control": "textfield",
|
||||
"key": "period",
|
||||
"required": true,
|
||||
"property": [ "telemetry", "period" ],
|
||||
|
@ -21,15 +21,21 @@
|
||||
"implementation": "ViewSwitcherController.js",
|
||||
"depends": [ "$scope" ]
|
||||
},
|
||||
{
|
||||
"key": "CreateButtonController",
|
||||
"implementation": "creation/CreateButtonController",
|
||||
"depends": [ "$scope", "$document" ]
|
||||
},
|
||||
{
|
||||
"key": "CreateMenuController",
|
||||
"implementation": "creation/CreateMenuController",
|
||||
"depends": [ "$scope" ]
|
||||
},
|
||||
{
|
||||
"key": "LocatorController",
|
||||
"implementation": "creation/LocatorController",
|
||||
"depends": [ "$scope" ]
|
||||
}
|
||||
],
|
||||
"controls": [
|
||||
{
|
||||
"key": "locator",
|
||||
"templateUrl": "templates/create/locator.html"
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
@ -46,11 +52,11 @@
|
||||
},
|
||||
{
|
||||
"key": "create-button",
|
||||
"templateUrl": "templates/create-button.html"
|
||||
"templateUrl": "templates/create/create-button.html"
|
||||
},
|
||||
{
|
||||
"key": "create-menu",
|
||||
"templateUrl": "templates/create-menu.html",
|
||||
"templateUrl": "templates/create/create-menu.html",
|
||||
"uses": [ "action" ]
|
||||
},
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div content="jquery-wrapper" class="abs holder-all browse-mode">
|
||||
<mct-include key="'topbar-browse'"></mct-include>
|
||||
<div class="holder browse-area outline abs" ng-controller="BrowseController as browse">
|
||||
<div class="holder browse-area outline abs" ng-controller="BrowseController">
|
||||
<div class='split-layout vertical contents abs'>
|
||||
<div class='split-pane-component treeview pane' style="width: 200px;">
|
||||
<mct-representation key="'create-button'" mct-object="navigatedObject">
|
||||
@ -8,7 +8,7 @@
|
||||
<div class='holder tree-holder abs'>
|
||||
<mct-representation key="'tree'"
|
||||
mct-object="domainObject"
|
||||
parameters="{callback: browse.setNavigation}">
|
||||
ng-model="treeModel">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div ng-controller="LocatorController" class="selector-list">
|
||||
<div>
|
||||
<mct-representation key="'tree'"
|
||||
mct-object="rootObject"
|
||||
ng-model="treeModel">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
@ -24,6 +24,7 @@ define(
|
||||
// that is currently navigated-to.
|
||||
function setNavigation(domainObject) {
|
||||
$scope.navigatedObject = domainObject;
|
||||
$scope.treeModel.selectedObject = domainObject;
|
||||
}
|
||||
|
||||
// Load the root object, put it in the scope.
|
||||
@ -48,30 +49,22 @@ define(
|
||||
}
|
||||
});
|
||||
|
||||
// Provide a model for the tree to modify
|
||||
$scope.treeModel = {
|
||||
selectedObject: navigationService.getNavigation()
|
||||
};
|
||||
|
||||
// Listen for changes in navigation state.
|
||||
navigationService.addListener(setNavigation);
|
||||
|
||||
// Also listen for changes which come from the tree
|
||||
$scope.$watch("treeModel.selectedObject", setNavigation);
|
||||
|
||||
// Clean up when the scope is destroyed
|
||||
$scope.$on("$destroy", function () {
|
||||
navigationService.removeListener(setNavigation);
|
||||
});
|
||||
|
||||
return {
|
||||
/**
|
||||
* Navigate to a specific domain object.
|
||||
*
|
||||
* This is exposed so that the browse tree has a callback
|
||||
* to invoke when the user clicks on a new object to navigate
|
||||
* to it.
|
||||
*
|
||||
* @method
|
||||
* @memberof BrowseController
|
||||
* @param {DomainObject} domainObject the object to navigate to
|
||||
*/
|
||||
setNavigation: function (domainObject) {
|
||||
navigationService.setNavigation(domainObject);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return BrowseController;
|
||||
|
@ -63,7 +63,8 @@ define(
|
||||
}
|
||||
|
||||
return dialogService.getUserInput(
|
||||
wizard.getFormModel()
|
||||
wizard.getFormStructure(),
|
||||
wizard.getInitialFormValue()
|
||||
).then(persistResult, doNothing);
|
||||
}
|
||||
|
||||
|
@ -32,33 +32,54 @@ define(
|
||||
* @return {FormModel} formModel the form model to
|
||||
* show in the create dialog
|
||||
*/
|
||||
getFormModel: function () {
|
||||
var parentRow = Object.create(parent),
|
||||
sections = [];
|
||||
getFormStructure: function () {
|
||||
var sections = [];
|
||||
|
||||
sections.push({
|
||||
name: "Properties",
|
||||
rows: properties.map(function (property) {
|
||||
rows: properties.map(function (property, index) {
|
||||
// Property definition is same as form row definition
|
||||
var row = Object.create(property.getDefinition());
|
||||
// But pull an initial value from the model
|
||||
row.value = property.getValue(model);
|
||||
|
||||
// Use index as the key into the formValue;
|
||||
// this correlates to the indexing provided by
|
||||
// getInitialFormValue
|
||||
row.key = index;
|
||||
|
||||
return row;
|
||||
})
|
||||
});
|
||||
|
||||
// Ensure there is always a "save in" section
|
||||
parentRow.name = "Save In";
|
||||
parentRow.cssclass = "selector-list";
|
||||
parentRow.control = "_locator";
|
||||
parentRow.key = "createParent";
|
||||
sections.push({ label: 'Location', rows: [parentRow]});
|
||||
sections.push({ name: 'Location', rows: [{
|
||||
name: "Save In",
|
||||
control: "locator",
|
||||
key: "createParent"
|
||||
}]});
|
||||
|
||||
return {
|
||||
sections: sections,
|
||||
name: "Create a New " + type.getName()
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Get the initial value for the form being described.
|
||||
* This will include the values for all properties described
|
||||
* in the structure.
|
||||
*
|
||||
* @returns {object} the initial value of the form
|
||||
*/
|
||||
getInitialFormValue: function () {
|
||||
// Start with initial values for properties
|
||||
var formValue = properties.map(function (property) {
|
||||
return property.getValue(model);
|
||||
});
|
||||
|
||||
// Include the createParent
|
||||
formValue.createParent = parent;
|
||||
|
||||
return formValue;
|
||||
},
|
||||
/**
|
||||
* Based on a populated form, get the domain object which
|
||||
* should be used as a parent for the newly-created object.
|
||||
@ -80,9 +101,8 @@ define(
|
||||
newModel.type = type.getKey();
|
||||
|
||||
// Update all properties
|
||||
properties.forEach(function (property) {
|
||||
var value = formValue[property.getDefinition().key];
|
||||
property.setValue(newModel, value);
|
||||
properties.forEach(function (property, index) {
|
||||
property.setValue(newModel, formValue[index]);
|
||||
});
|
||||
|
||||
return newModel;
|
||||
|
39
platform/commonUI/browse/src/creation/LocatorController.js
Normal file
39
platform/commonUI/browse/src/creation/LocatorController.js
Normal file
@ -0,0 +1,39 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Controller for the "locator" control, which provides the
|
||||
* user with the ability to select a domain object as the
|
||||
* destination for a newly-created object in the Create menu.
|
||||
* @constructor
|
||||
*/
|
||||
function LocatorController($scope) {
|
||||
// Populate values needed by the locator control. These are:
|
||||
// * rootObject: The top-level object, since we want to show
|
||||
// the full tree
|
||||
// * treeModel: The model for the embedded tree representation,
|
||||
// used for bi-directional object selection.
|
||||
function setLocatingObject(domainObject) {
|
||||
var context = domainObject &&
|
||||
domainObject.getCapability("context");
|
||||
|
||||
$scope.rootObject = context && context.getRoot();
|
||||
$scope.treeModel.selectedObject = domainObject;
|
||||
$scope.ngModel[$scope.field] = domainObject;
|
||||
}
|
||||
|
||||
// Initial state for the tree's model
|
||||
$scope.treeModel =
|
||||
{ selectedObject: $scope.ngModel[$scope.field] };
|
||||
|
||||
// Watch for changes from the tree
|
||||
$scope.$watch("treeModel.selectedObject", setLocatingObject);
|
||||
}
|
||||
|
||||
return LocatorController;
|
||||
}
|
||||
);
|
@ -25,7 +25,10 @@ define(
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj("$scope", [ "$on" ]);
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$on", "$watch" ]
|
||||
);
|
||||
mockObjectService = jasmine.createSpyObj(
|
||||
"objectService",
|
||||
[ "getObjects" ]
|
||||
@ -60,20 +63,6 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a means of changing navigation", function () {
|
||||
// Browse template needs a setNavigation function
|
||||
|
||||
// Verify precondition
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
// Set navigation via controller
|
||||
controller.setNavigation(mockDomainObject);
|
||||
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.toHaveBeenCalledWith(mockDomainObject);
|
||||
});
|
||||
|
||||
it("uses composition to set the navigated object, if there is none", function () {
|
||||
mockRootObject.useCapability.andReturn(mockPromise([
|
||||
mockDomainObject
|
||||
|
@ -12,6 +12,7 @@ define(
|
||||
var mockType,
|
||||
mockParent,
|
||||
mockProperties,
|
||||
testModel,
|
||||
wizard;
|
||||
|
||||
function createMockProperty(name) {
|
||||
@ -19,10 +20,8 @@ define(
|
||||
"property" + name,
|
||||
[ "getDefinition", "getValue", "setValue" ]
|
||||
);
|
||||
mockProperty.getDefinition.andReturn({
|
||||
name: name,
|
||||
key: name.toLowerCase()
|
||||
});
|
||||
mockProperty.getDefinition.andReturn({});
|
||||
mockProperty.getValue.andReturn(name);
|
||||
return mockProperty;
|
||||
}
|
||||
|
||||
@ -48,11 +47,13 @@ define(
|
||||
);
|
||||
mockProperties = [ "A", "B", "C" ].map(createMockProperty);
|
||||
|
||||
testModel = { someKey: "some value" };
|
||||
|
||||
mockType.getKey.andReturn("test");
|
||||
mockType.getGlyph.andReturn("T");
|
||||
mockType.getDescription.andReturn("a test type");
|
||||
mockType.getName.andReturn("Test");
|
||||
mockType.getInitialModel.andReturn({});
|
||||
mockType.getInitialModel.andReturn(testModel);
|
||||
mockType.getProperties.andReturn(mockProperties);
|
||||
|
||||
wizard = new CreateWizard(
|
||||
@ -62,33 +63,45 @@ define(
|
||||
});
|
||||
|
||||
it("creates a form model with a Properties section", function () {
|
||||
expect(wizard.getFormModel().sections[0].name)
|
||||
expect(wizard.getFormStructure().sections[0].name)
|
||||
.toEqual("Properties");
|
||||
});
|
||||
|
||||
it("adds one row per defined type property", function () {
|
||||
// Three properties were defined in the mock type
|
||||
expect(wizard.getFormModel().sections[0].rows.length)
|
||||
expect(wizard.getFormStructure().sections[0].rows.length)
|
||||
.toEqual(3);
|
||||
});
|
||||
|
||||
it("interprets form data using type-defined properties", function () {
|
||||
// Use key names from mock properties
|
||||
wizard.createModel({
|
||||
a: "field 0",
|
||||
b: "field 1",
|
||||
c: "field 2"
|
||||
});
|
||||
wizard.createModel([
|
||||
"field 0",
|
||||
"field 1",
|
||||
"field 2"
|
||||
]);
|
||||
|
||||
// Should have gotten a setValue call
|
||||
mockProperties.forEach(function (mockProperty, i) {
|
||||
expect(mockProperty.setValue).toHaveBeenCalledWith(
|
||||
{ type: 'test' },
|
||||
{ someKey: "some value", type: 'test' },
|
||||
"field " + i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("looks up initial values from properties", function () {
|
||||
var initialValue = wizard.getInitialFormValue();
|
||||
|
||||
expect(initialValue[0]).toEqual("A");
|
||||
expect(initialValue[1]).toEqual("B");
|
||||
expect(initialValue[2]).toEqual("C");
|
||||
|
||||
// Verify that expected argument was passed
|
||||
mockProperties.forEach(function (mockProperty) {
|
||||
expect(mockProperty.getValue)
|
||||
.toHaveBeenCalledWith(testModel);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -0,0 +1,73 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/creation/LocatorController"],
|
||||
function (LocatorController) {
|
||||
"use strict";
|
||||
|
||||
describe("The locator controller", function () {
|
||||
var mockScope,
|
||||
mockDomainObject,
|
||||
mockRootObject,
|
||||
mockContext,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch" ]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[ "getCapability" ]
|
||||
);
|
||||
mockRootObject = jasmine.createSpyObj(
|
||||
"rootObject",
|
||||
[ "getCapability" ]
|
||||
);
|
||||
mockContext = jasmine.createSpyObj(
|
||||
"context",
|
||||
[ "getRoot" ]
|
||||
);
|
||||
|
||||
mockDomainObject.getCapability.andReturn(mockContext);
|
||||
mockContext.getRoot.andReturn(mockRootObject);
|
||||
|
||||
mockScope.ngModel = {};
|
||||
mockScope.field = "someField";
|
||||
|
||||
controller = new LocatorController(mockScope);
|
||||
});
|
||||
|
||||
it("adds a treeModel to scope", function () {
|
||||
expect(mockScope.treeModel).toBeDefined();
|
||||
});
|
||||
|
||||
it("watches for changes to treeModel", function () {
|
||||
// This is what the embedded tree representation
|
||||
// will be modifying.
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"treeModel.selectedObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("changes its own model on embedded model updates", function () {
|
||||
// Need to pass on selection changes as updates to
|
||||
// the control's value
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockScope.ngModel.someField).toEqual(mockDomainObject);
|
||||
expect(mockScope.rootObject).toEqual(mockRootObject);
|
||||
|
||||
// Verify that the capability we expect to have been used
|
||||
// was used.
|
||||
expect(mockDomainObject.getCapability)
|
||||
.toHaveBeenCalledWith("context");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -6,6 +6,7 @@
|
||||
"creation/CreateMenuController",
|
||||
"creation/CreateWizard",
|
||||
"creation/CreationService",
|
||||
"creation/LocatorController",
|
||||
"navigation/NavigateAction",
|
||||
"navigation/NavigationService",
|
||||
"windowing/FullscreenAction"
|
||||
|
@ -12,11 +12,17 @@
|
||||
<div class="abs form outline editor">
|
||||
<div class='abs contents'>
|
||||
<!-- mct-include key="'form'" ng-model="" -->
|
||||
<textarea ng-model="ngModel.value"></textarea>
|
||||
<mct-form ng-model="ngModel.value"
|
||||
structure="ngModel.structure"
|
||||
name="createForm">
|
||||
</mct-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="abs bottom-bar">
|
||||
<a class='btn lg major' href='' ng-click="ngModel.confirm()">
|
||||
<a class='btn lg major'
|
||||
href=''
|
||||
ng-class="{ disabled: !createForm.$valid }"
|
||||
ng-click="ngModel.confirm()">
|
||||
OK
|
||||
</a>
|
||||
<a class='btn lg subtle' href='' ng-click="ngModel.cancel()">
|
||||
|
@ -1,4 +1,4 @@
|
||||
/*global define,Promise*/
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining DialogService. Created by vwoeltje on 11/10/14.
|
||||
@ -36,14 +36,8 @@ define(
|
||||
// overlay-dialog template and associated with a
|
||||
// OK button click
|
||||
function confirm() {
|
||||
var resultingValue;
|
||||
|
||||
// Temporary workaround, in the absence of a
|
||||
// forms package.
|
||||
resultingValue = JSON.parse(overlayModel.value);
|
||||
|
||||
// Pass along the result
|
||||
deferred.resolve(resultingValue);
|
||||
deferred.resolve(overlayModel.value);
|
||||
|
||||
// Stop showing the dialog
|
||||
dismiss();
|
||||
@ -73,8 +67,8 @@ define(
|
||||
overlayModel = {
|
||||
title: formModel.name,
|
||||
message: formModel.message,
|
||||
formModel: formModel,
|
||||
value: JSON.stringify(value),
|
||||
structure: formModel,
|
||||
value: value,
|
||||
confirm: confirm,
|
||||
cancel: cancel
|
||||
};
|
||||
|
@ -27,6 +27,15 @@
|
||||
"category": "view-control",
|
||||
"glyph": "p"
|
||||
},
|
||||
{
|
||||
"key": "properties",
|
||||
"category": "contextual",
|
||||
"implementation": "actions/PropertiesAction.js",
|
||||
"glyph": "p",
|
||||
"name": "Edit Properties...",
|
||||
"description": "Edit properties of this object.",
|
||||
"depends": [ "dialogService" ]
|
||||
},
|
||||
{
|
||||
"key": "remove",
|
||||
"category": "contextual",
|
||||
|
82
platform/commonUI/edit/src/actions/PropertiesAction.js
Normal file
82
platform/commonUI/edit/src/actions/PropertiesAction.js
Normal file
@ -0,0 +1,82 @@
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Edit the properties of a domain object. Shows a dialog
|
||||
* which should display a set of properties similar to that
|
||||
* shown in the Create wizard.
|
||||
*/
|
||||
define(
|
||||
['./PropertiesDialog'],
|
||||
function (PropertiesDialog) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Construct an action which will allow an object's metadata to be
|
||||
* edited.
|
||||
*
|
||||
* @param {DialogService} dialogService a service which will show the dialog
|
||||
* @param {DomainObject} object the object to be edited
|
||||
* @param {ActionContext} context the context in which this action is performed
|
||||
* @constructor
|
||||
*/
|
||||
function PropertiesAction(dialogService, context) {
|
||||
var object = context.domainObject;
|
||||
|
||||
// Persist modifications to this domain object
|
||||
function doPersist() {
|
||||
var persistence = object.getCapability('persistence');
|
||||
return persistence && persistence.persist();
|
||||
}
|
||||
|
||||
// Update the domain object model based on user input
|
||||
function updateModel(userInput, dialog) {
|
||||
return object.useCapability('mutation', function (model) {
|
||||
dialog.updateModel(model, userInput);
|
||||
});
|
||||
}
|
||||
|
||||
function showDialog(type) {
|
||||
// Create a dialog object to generate the form structure, etc.
|
||||
var dialog = new PropertiesDialog(type, object.getModel());
|
||||
|
||||
// Show the dialog
|
||||
return dialogService.getUserInput(
|
||||
dialog.getFormStructure(),
|
||||
dialog.getInitialFormValue()
|
||||
).then(function (userInput) {
|
||||
// Update the model, if user input was provided
|
||||
return userInput && updateModel(userInput, dialog);
|
||||
}).then(function (result) {
|
||||
return result && doPersist();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Perform this action.
|
||||
* @return {Promise} a promise which will be
|
||||
* fulfilled when the action has completed.
|
||||
*/
|
||||
perform: function () {
|
||||
var type = object.getCapability('type');
|
||||
return type && showDialog(type);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter this action for applicability against a given context.
|
||||
* This will ensure that a domain object is present in the
|
||||
* context.
|
||||
*/
|
||||
PropertiesAction.appliesTo = function (context) {
|
||||
return (context || {}).domainObject &&
|
||||
(context || {}).domainObject.hasCapability("type") &&
|
||||
(context || {}).domainObject.hasCapability("persistence");
|
||||
};
|
||||
|
||||
return PropertiesAction;
|
||||
}
|
||||
|
||||
);
|
||||
|
73
platform/commonUI/edit/src/actions/PropertiesDialog.js
Normal file
73
platform/commonUI/edit/src/actions/PropertiesDialog.js
Normal file
@ -0,0 +1,73 @@
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Defines the PropertiesDialog, used by the PropertiesAction to
|
||||
* populate the form shown in dialog based on the created type.
|
||||
*
|
||||
* @module common/actions/properties-dialog
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Construct a new Properties dialog.
|
||||
*
|
||||
* @param {TypeImpl} type the type of domain object for which properties
|
||||
* will be specified
|
||||
* @param {DomainObject} the object for which properties will be set
|
||||
* @constructor
|
||||
* @memberof module:common/actions/properties-dialog
|
||||
*/
|
||||
function PropertiesDialog(type, model) {
|
||||
var properties = type.getProperties();
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get sections provided by this dialog.
|
||||
* @return {FormStructure} the structure of this form
|
||||
*/
|
||||
getFormStructure: function () {
|
||||
return {
|
||||
name: "Edit " + model.name,
|
||||
sections: [{
|
||||
name: "Properties",
|
||||
rows: properties.map(function (property, index) {
|
||||
// Property definition is same as form row definition
|
||||
var row = Object.create(property.getDefinition());
|
||||
row.key = index;
|
||||
return row;
|
||||
})
|
||||
}]
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Get the initial state of the form shown by this dialog
|
||||
* (based on the object model)
|
||||
* @returns {object} initial state of the form
|
||||
*/
|
||||
getInitialFormValue: function () {
|
||||
// Start with initial values for properties
|
||||
// Note that index needs to correlate to row.key
|
||||
// from getFormStructure
|
||||
return properties.map(function (property) {
|
||||
return property.getValue(model);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update a domain object model based on the value of a form.
|
||||
*/
|
||||
updateModel: function (model, formValue) {
|
||||
// Update all properties
|
||||
properties.forEach(function (property, index) {
|
||||
property.setValue(model, formValue[index]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
return PropertiesDialog;
|
||||
}
|
||||
);
|
71
platform/commonUI/edit/test/actions/PropertiesActionSpec.js
Normal file
71
platform/commonUI/edit/test/actions/PropertiesActionSpec.js
Normal file
@ -0,0 +1,71 @@
|
||||
/*global define,describe,it,xit,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
['../../src/actions/PropertiesAction'],
|
||||
function (PropertiesAction) {
|
||||
"use strict";
|
||||
|
||||
describe("Properties action", function () {
|
||||
var capabilities, model, object, context, input, dialogService, action;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
capabilities = {
|
||||
type: { getProperties: function () { return []; } },
|
||||
persistence: jasmine.createSpyObj("persistence", ["persist"]),
|
||||
mutation: jasmine.createSpy("mutation")
|
||||
};
|
||||
model = {};
|
||||
input = {};
|
||||
object = {
|
||||
getId: function () { return 'test-id'; },
|
||||
getCapability: function (k) { return capabilities[k]; },
|
||||
getModel: function () { return model; },
|
||||
useCapability: function (k, v) { return capabilities[k](v); },
|
||||
hasCapability: function () { return true; }
|
||||
};
|
||||
context = { someKey: "some value", domainObject: object };
|
||||
dialogService = {
|
||||
getUserInput: function () {
|
||||
return mockPromise(input);
|
||||
}
|
||||
};
|
||||
|
||||
capabilities.mutation.andReturn(true);
|
||||
|
||||
action = new PropertiesAction(dialogService, context);
|
||||
});
|
||||
|
||||
it("persists when an action is performed", function () {
|
||||
action.perform();
|
||||
expect(capabilities.persistence.persist)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not persist any changes upon cancel", function () {
|
||||
input = undefined;
|
||||
action.perform();
|
||||
expect(capabilities.persistence.persist)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mutates an object when performed", function () {
|
||||
action.perform();
|
||||
expect(capabilities.mutation).toHaveBeenCalled();
|
||||
capabilities.mutation.mostRecentCall.args[0]({});
|
||||
});
|
||||
|
||||
it("is only applicable when a domain object is in context", function () {
|
||||
expect(PropertiesAction.appliesTo(context)).toBeTruthy();
|
||||
expect(PropertiesAction.appliesTo({})).toBeFalsy();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
53
platform/commonUI/edit/test/actions/PropertiesDialogSpec.js
Normal file
53
platform/commonUI/edit/test/actions/PropertiesDialogSpec.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*global define,describe,it,xit,expect,beforeEach*/
|
||||
|
||||
define(
|
||||
["../../src/actions/PropertiesDialog"],
|
||||
function (PropertiesDialog) {
|
||||
"use strict";
|
||||
|
||||
describe("Properties dialog", function () {
|
||||
|
||||
var type, properties, domainObject, model, dialog;
|
||||
|
||||
beforeEach(function () {
|
||||
type = {
|
||||
getProperties: function () { return properties; }
|
||||
};
|
||||
model = { x: "initial value" };
|
||||
properties = ["x", "y", "z"].map(function (k) {
|
||||
return {
|
||||
getValue: function (model) { return model[k]; },
|
||||
setValue: function (model, v) { model[k] = v; },
|
||||
getDefinition: function () { return {}; }
|
||||
};
|
||||
});
|
||||
|
||||
dialog = new PropertiesDialog(type, model);
|
||||
});
|
||||
|
||||
it("provides sections based on type properties", function () {
|
||||
expect(dialog.getFormStructure().sections[0].rows.length)
|
||||
.toEqual(properties.length);
|
||||
});
|
||||
|
||||
it("pulls initial values from object model", function () {
|
||||
expect(dialog.getInitialFormValue()[0])
|
||||
.toEqual("initial value");
|
||||
});
|
||||
|
||||
it("populates models with form results", function () {
|
||||
dialog.updateModel(model, [
|
||||
"new value",
|
||||
"other new value",
|
||||
42
|
||||
]);
|
||||
expect(model).toEqual({
|
||||
x: "new value",
|
||||
y: "other new value",
|
||||
z: 42
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -3,6 +3,8 @@
|
||||
"EditController",
|
||||
"actions/CancelAction",
|
||||
"actions/EditAction",
|
||||
"actions/PropertiesAction",
|
||||
"actions/PropertiesDialog",
|
||||
"actions/RemoveAction",
|
||||
"actions/SaveAction",
|
||||
"capabilities/EditableContextCapability",
|
||||
|
@ -8,9 +8,9 @@
|
||||
</span>
|
||||
<mct-representation key="'label'"
|
||||
mct-object="domainObject"
|
||||
parameters="parameters"
|
||||
ng-click="parameters.callback(domainObject)"
|
||||
ng-class="{selected: treeNode.isNavigated()}">
|
||||
ng-model="ngModel"
|
||||
ng-click="ngModel.selectedObject = domainObject"
|
||||
ng-class="{selected: treeNode.isSelected()}">
|
||||
</mct-representation>
|
||||
</span>
|
||||
<span class="tree-item-subtree"
|
||||
@ -18,7 +18,7 @@
|
||||
ng-if="model.composition !== undefined">
|
||||
|
||||
<mct-representation key="'tree'"
|
||||
parameters="parameters"
|
||||
ng-model="ngModel"
|
||||
mct-object="treeNode.hasBeenExpanded() && domainObject">
|
||||
</mct-representation>
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
<ul class="tree">
|
||||
<li ng-repeat="child in composition">
|
||||
<mct-representation key="'tree-node'" mct-object="child" parameters="parameters">
|
||||
<mct-representation key="'tree-node'"
|
||||
mct-object="child"
|
||||
ng-model="ngModel">
|
||||
</mct-representation>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -29,9 +29,9 @@ define(
|
||||
* expand-to-show-navigated-object behavior.)
|
||||
* @constructor
|
||||
*/
|
||||
function TreeNodeController($scope, navigationService) {
|
||||
var navigatedObject = navigationService.getNavigation(),
|
||||
isNavigated = false,
|
||||
function TreeNodeController($scope) {
|
||||
var selectedObject = ($scope.ngModel || {}).selectedObject,
|
||||
isSelected = false,
|
||||
hasBeenExpanded = false;
|
||||
|
||||
// Look up the id for a domain object. A convenience
|
||||
@ -73,7 +73,7 @@ define(
|
||||
// Check if the navigated object is in the subtree of this
|
||||
// node's domain object, by comparing the paths reported
|
||||
// by their context capability.
|
||||
function isOnNavigationPath(nodeObject, navObject) {
|
||||
function isOnSelectionPath(nodeObject, navObject) {
|
||||
var nodeContext = nodeObject &&
|
||||
nodeObject.getCapability('context'),
|
||||
navContext = navObject &&
|
||||
@ -92,19 +92,19 @@ define(
|
||||
|
||||
// Consider the currently-navigated object and update
|
||||
// parameters which support display.
|
||||
function checkNavigation() {
|
||||
function checkSelection() {
|
||||
var nodeObject = $scope.domainObject;
|
||||
|
||||
// Check if we are the navigated object. Check the parent
|
||||
// as well to make sure we are the same instance of the
|
||||
// navigated object.
|
||||
isNavigated =
|
||||
idsEqual(nodeObject, navigatedObject) &&
|
||||
idsEqual(parentOf(nodeObject), parentOf(navigatedObject));
|
||||
isSelected =
|
||||
idsEqual(nodeObject, selectedObject) &&
|
||||
idsEqual(parentOf(nodeObject), parentOf(selectedObject));
|
||||
|
||||
// Expand if necessary (if the navigated object will
|
||||
// be in this node's subtree)
|
||||
if (isOnNavigationPath(nodeObject, navigatedObject) &&
|
||||
if (isOnSelectionPath(nodeObject, selectedObject) &&
|
||||
$scope.toggle !== undefined) {
|
||||
$scope.toggle.setState(true);
|
||||
hasBeenExpanded = true;
|
||||
@ -113,17 +113,14 @@ define(
|
||||
|
||||
// Callback for the navigation service; track the currently
|
||||
// navigated object and update display parameters as needed.
|
||||
function setNavigation(object) {
|
||||
navigatedObject = object;
|
||||
checkNavigation();
|
||||
function setSelection(object) {
|
||||
selectedObject = object;
|
||||
checkSelection();
|
||||
}
|
||||
|
||||
// Listen for changes which will effect display parameters
|
||||
navigationService.addListener(setNavigation);
|
||||
$scope.$on("$destroy", function () {
|
||||
navigationService.removeListener(setNavigation);
|
||||
});
|
||||
$scope.$watch("domainObject", checkNavigation);
|
||||
$scope.$watch("ngModel.selectedObject", setSelection);
|
||||
$scope.$watch("domainObject", checkSelection);
|
||||
|
||||
return {
|
||||
/**
|
||||
@ -143,11 +140,13 @@ define(
|
||||
},
|
||||
/**
|
||||
* Check whether or not the domain object represented by
|
||||
* this tree node is currently the navigated object.
|
||||
* @returns true if this is the navigated object
|
||||
* this tree node should be highlighted.
|
||||
* An object will be highlighted if it matches
|
||||
* ngModel.selectedObject
|
||||
* @returns true if this should be highlighted
|
||||
*/
|
||||
isNavigated: function () {
|
||||
return isNavigated;
|
||||
isSelected: function () {
|
||||
return isSelected;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ define(
|
||||
|
||||
describe("The tree node controller", function () {
|
||||
var mockScope,
|
||||
mockNavigationService,
|
||||
controller;
|
||||
|
||||
function TestObject(id, context) {
|
||||
@ -24,26 +23,11 @@ define(
|
||||
"$scope",
|
||||
[ "$watch", "$on" ]
|
||||
);
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
[
|
||||
"getNavigation",
|
||||
"setNavigation",
|
||||
"addListener",
|
||||
"removeListener"
|
||||
]
|
||||
);
|
||||
controller = new TreeNodeController(
|
||||
mockScope,
|
||||
mockNavigationService
|
||||
mockScope
|
||||
);
|
||||
});
|
||||
|
||||
it("listens for navigation changes", function () {
|
||||
expect(mockNavigationService.addListener)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("allows tracking of expansion state", function () {
|
||||
// The tree node tracks whether or not it has ever
|
||||
// been expanded in order to lazily load the expanded
|
||||
@ -66,13 +50,15 @@ define(
|
||||
mockContext.getPath.andReturn([obj]);
|
||||
|
||||
// Verify precondition
|
||||
expect(controller.isNavigated()).toBeFalsy();
|
||||
expect(controller.isSelected()).toBeFalsy();
|
||||
|
||||
mockNavigationService.getNavigation.andReturn(obj);
|
||||
// Change the represented domain object
|
||||
mockScope.domainObject = obj;
|
||||
mockNavigationService.addListener.mostRecentCall.args[0](obj);
|
||||
|
||||
expect(controller.isNavigated()).toBeTruthy();
|
||||
// Invoke the watch with the new selection
|
||||
mockScope.$watch.calls[0].args[1](obj);
|
||||
|
||||
expect(controller.isSelected()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expands a node if it is on the navigation path", function () {
|
||||
@ -92,16 +78,16 @@ define(
|
||||
mockParentContext.getPath.andReturn([parent]);
|
||||
|
||||
// Set up such that we are on, but not at the end of, a path
|
||||
mockNavigationService.getNavigation.andReturn(child);
|
||||
mockScope.ngModel = { selectedObject: child };
|
||||
mockScope.domainObject = parent;
|
||||
mockScope.toggle = jasmine.createSpyObj("toggle", ["setState"]);
|
||||
|
||||
// Trigger update
|
||||
mockNavigationService.addListener.mostRecentCall.args[0](child);
|
||||
// Invoke the watch with the new selection
|
||||
mockScope.$watch.calls[0].args[1](child);
|
||||
|
||||
expect(mockScope.toggle.setState).toHaveBeenCalledWith(true);
|
||||
expect(controller.hasBeenExpanded()).toBeTruthy();
|
||||
expect(controller.isNavigated()).toBeFalsy();
|
||||
expect(controller.isSelected()).toBeFalsy();
|
||||
|
||||
});
|
||||
|
||||
@ -122,42 +108,18 @@ define(
|
||||
mockParentContext.getPath.andReturn([parent]);
|
||||
|
||||
// Set up such that we are on, but not at the end of, a path
|
||||
mockNavigationService.getNavigation.andReturn(child);
|
||||
mockScope.ngModel = { selectedObject: child };
|
||||
mockScope.domainObject = parent;
|
||||
mockScope.toggle = jasmine.createSpyObj("toggle", ["setState"]);
|
||||
|
||||
// Trigger update
|
||||
mockNavigationService.addListener.mostRecentCall.args[0](child);
|
||||
// Invoke the watch with the new selection
|
||||
mockScope.$watch.calls[0].args[1](child);
|
||||
|
||||
expect(mockScope.toggle.setState).not.toHaveBeenCalled();
|
||||
expect(controller.hasBeenExpanded()).toBeFalsy();
|
||||
expect(controller.isNavigated()).toBeFalsy();
|
||||
expect(controller.isSelected()).toBeFalsy();
|
||||
|
||||
});
|
||||
|
||||
it("removes its navigation listener when the scope is destroyed", function () {
|
||||
var navCallback =
|
||||
mockNavigationService.addListener.mostRecentCall.args[0];
|
||||
|
||||
// Make sure the controller is listening in the first place
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Verify precondition - no removeListener called
|
||||
expect(mockNavigationService.removeListener)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
// Call that listener (act as if scope is being destroyed)
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Verify precondition - no removeListener called
|
||||
expect(mockNavigationService.removeListener)
|
||||
.toHaveBeenCalledWith(navCallback);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -74,16 +74,16 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"control": "_textfield",
|
||||
"label": "Title",
|
||||
"control": "textfield",
|
||||
"name": "Title",
|
||||
"key": "name",
|
||||
"property": "name",
|
||||
"pattern": "\\S+",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"control": "_checkbox",
|
||||
"label": "Display title by default",
|
||||
"control": "checkbox",
|
||||
"name": "Display title by default",
|
||||
"key": "displayTitle",
|
||||
"property": [ "display", "title" ]
|
||||
}
|
||||
|
@ -24,6 +24,24 @@ define(
|
||||
propertyDefinition.conversion || "identity"
|
||||
);
|
||||
|
||||
// Check if a value is defined; used to check if initial array
|
||||
// values have been populated.
|
||||
function isUnpopulatedArray(value) {
|
||||
var i;
|
||||
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = 0; i < value.length; i += 1) {
|
||||
if (value[i] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perform a lookup for a value from an object,
|
||||
// which may recursively look at contained objects
|
||||
// based on the path provided.
|
||||
@ -78,11 +96,18 @@ define(
|
||||
*/
|
||||
getValue: function (model) {
|
||||
var property = propertyDefinition.property ||
|
||||
propertyDefinition.key;
|
||||
propertyDefinition.key,
|
||||
initialValue =
|
||||
property && lookupValue(model, property);
|
||||
|
||||
return property ? conversion.toFormValue(
|
||||
lookupValue(model, property)
|
||||
) : undefined;
|
||||
// Provide an empty array if this is a multi-item
|
||||
// property.
|
||||
if (Array.isArray(propertyDefinition.items)) {
|
||||
initialValue = initialValue ||
|
||||
new Array(propertyDefinition.items.length);
|
||||
}
|
||||
|
||||
return conversion.toFormValue(initialValue);
|
||||
},
|
||||
/**
|
||||
* Set a value associated with this property in
|
||||
@ -92,6 +117,13 @@ define(
|
||||
var property = propertyDefinition.property ||
|
||||
propertyDefinition.key;
|
||||
|
||||
// If an array contains all undefined values, treat it
|
||||
// as undefined, to filter back out arrays for input
|
||||
// that never got entered.
|
||||
value = isUnpopulatedArray(value) ? undefined : value;
|
||||
|
||||
// Convert to a value suitable for storage in the
|
||||
// domain object's model
|
||||
value = conversion.toModelValue(value);
|
||||
|
||||
return property ?
|
||||
|
@ -75,6 +75,32 @@ define(
|
||||
expect(property.getValue(model)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("provides empty arrays for values that are array-like", function () {
|
||||
var definition = {
|
||||
property: "someProperty",
|
||||
items: [ {}, {}, {} ]
|
||||
},
|
||||
model = {},
|
||||
property = new TypeProperty(definition);
|
||||
expect(property.getValue(model))
|
||||
.toEqual([undefined, undefined, undefined]);
|
||||
});
|
||||
|
||||
it("detects and ignores empty arrays on setValue", function () {
|
||||
var definition = {
|
||||
property: "someProperty",
|
||||
items: [ {}, {}, {} ]
|
||||
},
|
||||
model = {},
|
||||
property = new TypeProperty(definition);
|
||||
|
||||
property.setValue(model, [undefined, undefined, undefined]);
|
||||
expect(model.someProperty).toBeUndefined();
|
||||
|
||||
// Verify that this only happens when all are undefined
|
||||
property.setValue(model, [undefined, "x", 42]);
|
||||
expect(model.someProperty).toEqual([undefined, "x", 42]);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -13,43 +13,45 @@
|
||||
"model": { "composition": [] },
|
||||
"properties": [
|
||||
{
|
||||
"label": "Preferred Size",
|
||||
"control": "_textfields",
|
||||
"values": [
|
||||
"name": "Preferred Size",
|
||||
"control": "composite",
|
||||
"items": [
|
||||
{
|
||||
"label": "Width (px)",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$"
|
||||
"name": "Width (px)",
|
||||
"control": "textfield"
|
||||
},
|
||||
{
|
||||
"label": "Height (px)",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$"
|
||||
"name": "Height (px)",
|
||||
"control": "textfield"
|
||||
}
|
||||
],
|
||||
"key": "preferredSize",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$",
|
||||
"property": "preferredSize",
|
||||
"conversion": "number[]"
|
||||
},
|
||||
{
|
||||
"label": "Layout Grid",
|
||||
"control": "_textfields",
|
||||
"values": [
|
||||
"name": "Layout Grid",
|
||||
"control": "composite",
|
||||
"items": [
|
||||
{
|
||||
"label": "Horizontal grid (px)",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$"
|
||||
"name": "Horizontal grid (px)",
|
||||
"control": "textfield"
|
||||
},
|
||||
{
|
||||
"label": "Vertical grid (px)",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$"
|
||||
"name": "Vertical grid (px)",
|
||||
"control": "textfield"
|
||||
}
|
||||
],
|
||||
"key": "layoutGrid",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$",
|
||||
"property": "layoutGrid",
|
||||
"conversion": "number[]"
|
||||
},
|
||||
{
|
||||
"label": "Default View",
|
||||
"control": "_select",
|
||||
"values": [
|
||||
"Plot",
|
||||
"Scrolling"
|
||||
"name": "Default View",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{ "name": "Plot", "value": "plot" },
|
||||
{ "name": "Scrolling", "value": "scrolling" }
|
||||
],
|
||||
"comment": "TODO: Infer values from type",
|
||||
"key": "defaultView"
|
||||
|
@ -29,6 +29,10 @@
|
||||
{
|
||||
"key": "textfield",
|
||||
"templateUrl": "templates/controls/textfield.html"
|
||||
},
|
||||
{
|
||||
"key": "composite",
|
||||
"templateUrl": "templates/controls/composite.html"
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
@ -36,6 +40,10 @@
|
||||
"key": "DateTimeController",
|
||||
"implementation": "controllers/DateTimeController.js",
|
||||
"depends": [ "$scope" ]
|
||||
},
|
||||
{
|
||||
"key": "CompositeController",
|
||||
"implementation": "controllers/CompositeController.js"
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
|
15
platform/forms/res/templates/controls/composite.html
Normal file
15
platform/forms/res/templates/controls/composite.html
Normal file
@ -0,0 +1,15 @@
|
||||
<span ng-controller="CompositeController as compositeCtrl">
|
||||
<ng-form name="mctFormItem" ng-repeat="item in structure.items">
|
||||
<mct-control key="item.control"
|
||||
ng-model="ngModel[field]"
|
||||
ng-required="ngRequired || compositeCtrl.isNonEmpty(ngModel[field])"
|
||||
ng-pattern="ngPattern"
|
||||
options="item.options"
|
||||
structure="row"
|
||||
field="$index">
|
||||
</mct-control>
|
||||
<span class="composite-control-label">
|
||||
{{item.name}}
|
||||
</span>
|
||||
</ng-form>
|
||||
</span>
|
@ -7,46 +7,33 @@
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<ng-form name="mctFormInner" ng-repeat="row in section.rows">
|
||||
<div class="form-row validates"
|
||||
ng-class="{
|
||||
req: row.required,
|
||||
valid: mctFormInner.$dirty && mctFormInner.$valid,
|
||||
invalid: mctFormInner.$dirty && !mctFormInner.$valid
|
||||
}">
|
||||
<div class="form-row validates"
|
||||
ng-class="{
|
||||
req: row.required,
|
||||
valid: mctFormInner.$dirty && mctFormInner.$valid,
|
||||
invalid: mctFormInner.$dirty && !mctFormInner.$valid
|
||||
}">
|
||||
|
||||
<div class='label' title="{{row.description}}">
|
||||
{{row.name}}
|
||||
<span ng-if="row.description"
|
||||
class="ui-symbol">
|
||||
i
|
||||
</span>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<div class="wrapper" ng-if="row.control">
|
||||
<mct-control key="row.control"
|
||||
ng-model="ngModel"
|
||||
ng-required="row.required"
|
||||
ng-pattern="getRegExp(row.pattern)"
|
||||
options="row.options"
|
||||
structure="row"
|
||||
field="row.key">
|
||||
</mct-control>
|
||||
<div class='label' title="{{row.description}}">
|
||||
{{row.name}}
|
||||
<span ng-if="row.description"
|
||||
class="ui-symbol">
|
||||
i
|
||||
</span>
|
||||
</div>
|
||||
<div ng-repeat="item in row.items" class="validates">
|
||||
<ng-form name="mctFormItem">
|
||||
<mct-control key="item.control"
|
||||
<div class='controls'>
|
||||
<div class="wrapper" ng-if="row.control">
|
||||
<mct-control key="row.control"
|
||||
ng-model="ngModel"
|
||||
ng-required="item.required"
|
||||
ng-pattern="getRegExp(item.pattern)"
|
||||
options="item.options"
|
||||
ng-required="row.required"
|
||||
ng-pattern="getRegExp(row.pattern)"
|
||||
options="row.options"
|
||||
structure="row"
|
||||
field="item.key">
|
||||
field="row.key">
|
||||
</mct-control>
|
||||
{{item.name}}
|
||||
</ng-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
</span>
|
||||
|
48
platform/forms/src/controllers/CompositeController.js
Normal file
48
platform/forms/src/controllers/CompositeController.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The CompositeController supports the "composite" control type,
|
||||
* which provides an array of other controls. It is used specifically
|
||||
* to support validation when a particular row is not marked as
|
||||
* required; in this case, empty input should be allowed, but partial
|
||||
* input (where some but not all of the composite controls have been
|
||||
* filled in) should be disallowed. This is enforced in the template
|
||||
* by an ng-required directive, but that is supported by the
|
||||
* isNonEmpty check that this controller provides.
|
||||
* @constructor
|
||||
*/
|
||||
function CompositeController() {
|
||||
// Check if an element is defined; the map step of isNonEmpty
|
||||
function isDefined(element) {
|
||||
return typeof element !== 'undefined';
|
||||
}
|
||||
|
||||
// Boolean or; the reduce step of isNonEmpty
|
||||
function or(a, b) {
|
||||
return a || b;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Check if an array contains anything other than
|
||||
* undefined elements.
|
||||
* @param {Array} value the array to check
|
||||
* @returns {boolean} true if any non-undefined
|
||||
* element is in the array
|
||||
*/
|
||||
isNonEmpty: function (value) {
|
||||
return Array.isArray(value) &&
|
||||
value.map(isDefined).reduce(or, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return CompositeController;
|
||||
|
||||
}
|
||||
);
|
36
platform/forms/test/controllers/CompositeControllerSpec.js
Normal file
36
platform/forms/test/controllers/CompositeControllerSpec.js
Normal file
@ -0,0 +1,36 @@
|
||||
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/controllers/CompositeController"],
|
||||
function (CompositeController) {
|
||||
"use strict";
|
||||
|
||||
describe("The composite controller", function () {
|
||||
var controller;
|
||||
|
||||
beforeEach(function () {
|
||||
controller = new CompositeController();
|
||||
});
|
||||
|
||||
it("detects non-empty arrays", function () {
|
||||
expect(controller.isNonEmpty(["a", "b", undefined]))
|
||||
.toBeTruthy();
|
||||
expect(controller.isNonEmpty([3]))
|
||||
.toBeTruthy();
|
||||
});
|
||||
|
||||
it("detects empty arrays", function () {
|
||||
expect(controller.isNonEmpty([undefined, undefined, undefined]))
|
||||
.toBeFalsy();
|
||||
expect(controller.isNonEmpty([]))
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
it("ignores non-arrays", function () {
|
||||
expect(controller.isNonEmpty("this is not an array"))
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -1,5 +1,6 @@
|
||||
[
|
||||
"MCTControl",
|
||||
"MCTForm",
|
||||
"controllers/CompositeController",
|
||||
"controllers/DateTimeController"
|
||||
]
|
@ -141,7 +141,12 @@ define(
|
||||
|
||||
// Two-way bind key and parameters, get the represented domain
|
||||
// object as "mct-object"
|
||||
scope: { key: "=", domainObject: "=mctObject", parameters: "=" }
|
||||
scope: {
|
||||
key: "=",
|
||||
domainObject: "=mctObject",
|
||||
ngModel: "=",
|
||||
parameters: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user