Merge "Add Angular Schema Form"
This commit is contained in:
commit
216e673717
@ -82,6 +82,9 @@ module.exports = function (config) {
|
||||
xstaticPath + 'angular_fileupload/data/ng-file-upload-all.js',
|
||||
xstaticPath + 'spin/data/spin.js',
|
||||
xstaticPath + 'spin/data/spin.jquery.js',
|
||||
xstaticPath + 'tv4/data/tv4.js',
|
||||
xstaticPath + 'objectpath/data/ObjectPath.js',
|
||||
xstaticPath + 'angular_schema_form/data/schema-form.js',
|
||||
|
||||
// from jasmine_tests.py; only those that are deps for others
|
||||
'horizon/js/horizon.js',
|
||||
|
@ -12,6 +12,7 @@
|
||||
'horizon.framework.util.promise-toggle',
|
||||
'horizon.framework.util.q',
|
||||
'horizon.framework.util.tech-debt',
|
||||
'horizon.framework.util.uuid',
|
||||
'horizon.framework.util.workflow',
|
||||
'horizon.framework.util.validators',
|
||||
'horizon.framework.util.extensible'
|
||||
|
45
horizon/static/framework/util/uuid/uuid.js
Normal file
45
horizon/static/framework/util/uuid/uuid.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Cisco Systems
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.framework.util.uuid', [])
|
||||
.factory('horizon.framework.util.uuid.service', uuidService);
|
||||
|
||||
/**
|
||||
* @name horizon.framework.util.uuid
|
||||
* @description
|
||||
* Generates a UUID. This is useful for ensuring HTML components
|
||||
* have unique IDs for interactions.
|
||||
*/
|
||||
function uuidService() {
|
||||
var service = {
|
||||
generate: generate
|
||||
};
|
||||
return service;
|
||||
|
||||
function generate() {
|
||||
var d = new Date().getTime();
|
||||
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
})();
|
70
horizon/static/framework/util/uuid/uuid.spec.js
Normal file
70
horizon/static/framework/util/uuid/uuid.spec.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Cisco Systems
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.framework.util.uuid module', function() {
|
||||
it('should have been defined', function () {
|
||||
expect(angular.module('horizon.framework.util.uuid')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uuid', function () {
|
||||
var uuid;
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
uuid = $injector.get('horizon.framework.util.uuid.service');
|
||||
}));
|
||||
|
||||
it('should be defined', function () {
|
||||
expect(uuid).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate multiple unique IDs', function() {
|
||||
var unique = [];
|
||||
var ids = [];
|
||||
var i, j, potentialUUID, uniqueLen, isUnique;
|
||||
|
||||
// Generate 10 IDs
|
||||
for (i = 0; i < 10; i += 1) {
|
||||
ids.push(uuid.generate());
|
||||
}
|
||||
|
||||
// Check that the IDs are unique
|
||||
// Iterate through the IDs, check that it isn't part of our unique list,
|
||||
// then append
|
||||
for (i -= 1; i >= 0; i -= 1) {
|
||||
potentialUUID = ids[i];
|
||||
isUnique = true;
|
||||
for (j = 0, uniqueLen = unique.length; j < uniqueLen; j += 1) {
|
||||
if (potentialUUID === unique[j]) {
|
||||
isUnique = false;
|
||||
}
|
||||
}
|
||||
if (isUnique) {
|
||||
unique.push(potentialUUID);
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the array, because Jasmine's "toEqual" won't work otherwise.
|
||||
unique.reverse();
|
||||
|
||||
expect(ids).toEqual(unique);
|
||||
});
|
||||
});
|
||||
}());
|
66
horizon/static/framework/widgets/form/builders.provider.js
Normal file
66
horizon/static/framework/widgets/form/builders.provider.js
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* (c) Copyright 2016 Cisco Systems
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use self file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('schemaForm')
|
||||
.provider('hzBuilder', provider);
|
||||
|
||||
/**
|
||||
* @ngDoc provider
|
||||
* @name horizon.framework.widgets.form.builders
|
||||
*/
|
||||
function provider() {
|
||||
var builders = {
|
||||
tabsBuilder: tabsBuilder
|
||||
};
|
||||
|
||||
this.$get = function() {
|
||||
return builders;
|
||||
};
|
||||
|
||||
function tabsBuilder(args) {
|
||||
if (args.form.tabs && args.form.tabs.length > 0) {
|
||||
var tabLi = args.fieldFrag.querySelector('li');
|
||||
/* eslint-disable max-len */
|
||||
tabLi.setAttribute('ng-if', '!tab.condition ? true : evalExpr(tab.condition, { model: model, "arrayIndex": $index })');
|
||||
/* eslint-enable max-len */
|
||||
var tabContent = args.fieldFrag.querySelector('.tab-content');
|
||||
|
||||
args.form.tabs.forEach(function(tab, index) {
|
||||
tab.items.forEach(function(item) {
|
||||
if (item.required) {
|
||||
tab.required = true;
|
||||
}
|
||||
});
|
||||
var div = document.createElement('div');
|
||||
div.setAttribute('ng-show', 'model.tabs.selected === ' + index);
|
||||
div.setAttribute('ng-if', tab.condition || true);
|
||||
|
||||
var childFrag = args.build(
|
||||
tab.items,
|
||||
args.path + '.tabs[' + index + '].items',
|
||||
args.state
|
||||
);
|
||||
div.appendChild(childFrag);
|
||||
tabContent.appendChild(div);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Cisco Systems
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('hzBuilderProvider', function() {
|
||||
var provider, $compile, $scope, args, element, elementTwo;
|
||||
|
||||
var html = "<div><ul><li></li></ul><div class='tab-content'></div></div>";
|
||||
var htmlTwo = "<div class='test'></div>";
|
||||
|
||||
beforeEach(module('schemaForm'));
|
||||
|
||||
beforeEach(inject(function($injector) {
|
||||
$scope = $injector.get('$rootScope').$new();
|
||||
$compile = $injector.get('$compile');
|
||||
provider = $injector.get('hzBuilder');
|
||||
element = $compile(html)($scope);
|
||||
elementTwo = $compile(htmlTwo)($scope);
|
||||
$scope.$apply();
|
||||
|
||||
args = { form:
|
||||
{ tabs: [
|
||||
{ "title": "tabZero", "items": [ { "title": "item", "required": true } ] },
|
||||
{ "title": "tabOne", "condition": true, "items": [] },
|
||||
{ "title": "tabTwo", "condition": false, "items": [] }
|
||||
]},
|
||||
fieldFrag: element[0],
|
||||
build: function() {
|
||||
return elementTwo[0];
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
it('should correctly build tabs', function() {
|
||||
provider.tabsBuilder(args);
|
||||
|
||||
expect(element[0].querySelector('li').getAttribute("ng-if")).toBeDefined();
|
||||
expect(
|
||||
element[0].querySelector('.tab-content').querySelector('div').getAttribute("ng-show")
|
||||
).toBe('model.tabs.selected === 0');
|
||||
expect(
|
||||
element[0].querySelector('.tab-content').querySelector('div').getAttribute("ng-if")
|
||||
).toBe('true');
|
||||
expect(args.form.tabs[0].required).toBe(true);
|
||||
expect(args.form.tabs[1].required).not.toBeDefined();
|
||||
expect(args.form.tabs[2].required).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
})();
|
162
horizon/static/framework/widgets/form/decorator.js
Normal file
162
horizon/static/framework/widgets/form/decorator.js
Normal file
@ -0,0 +1,162 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Horizon custom decorator for angular schema form
|
||||
angular
|
||||
.module('schemaForm')
|
||||
.config(config);
|
||||
|
||||
config.$inject = [
|
||||
'schemaFormDecoratorsProvider',
|
||||
'sfBuilderProvider',
|
||||
'sfPathProvider',
|
||||
'sfErrorMessageProvider',
|
||||
'$windowProvider',
|
||||
'hzBuilderProvider'
|
||||
];
|
||||
|
||||
function config(
|
||||
decoratorsProvider,
|
||||
sfBuilderProvider,
|
||||
sfPathProvider,
|
||||
sfErrorMessageProvider,
|
||||
$windowProvider,
|
||||
hzBuilderProvider
|
||||
) {
|
||||
var base = $windowProvider.$get().STATIC_URL + 'framework/widgets/form/fields/';
|
||||
var simpleTransclusion = sfBuilderProvider.builders.simpleTransclusion;
|
||||
var ngModelOptions = sfBuilderProvider.builders.ngModelOptions;
|
||||
var ngModel = sfBuilderProvider.builders.ngModel;
|
||||
var sfField = sfBuilderProvider.builders.sfField;
|
||||
var condition = sfBuilderProvider.builders.condition;
|
||||
var array = sfBuilderProvider.builders.array;
|
||||
var tabs = hzBuilderProvider.$get().tabsBuilder;
|
||||
var defaults = [sfField, ngModel, ngModelOptions, condition];
|
||||
|
||||
// Define all our templates
|
||||
decoratorsProvider.defineDecorator('bootstrapDecorator', {
|
||||
textarea: {
|
||||
template: base + 'textarea.html',
|
||||
builder: defaults
|
||||
},
|
||||
fieldset: {
|
||||
template: base + 'fieldset.html',
|
||||
builder: [sfField, simpleTransclusion, condition]
|
||||
},
|
||||
array: {
|
||||
template: base + 'array.html',
|
||||
builder: [sfField, ngModelOptions, ngModel, array, condition]
|
||||
},
|
||||
tabarray: {
|
||||
template: base + 'tabarray.html',
|
||||
builder: [sfField, ngModelOptions, ngModel, array, condition]
|
||||
},
|
||||
tabs: {
|
||||
template: base + 'tabs.html',
|
||||
builder: [sfField, ngModelOptions, tabs, condition]
|
||||
},
|
||||
section: {
|
||||
template: base + 'section.html',
|
||||
builder: [sfField, simpleTransclusion, condition]
|
||||
},
|
||||
conditional: {
|
||||
template: base + 'section.html',
|
||||
builder: [sfField, simpleTransclusion, condition]
|
||||
},
|
||||
select: {
|
||||
template: base + 'select.html',
|
||||
builder: defaults
|
||||
},
|
||||
checkbox: {
|
||||
template: base + 'checkbox.html',
|
||||
builder: defaults
|
||||
},
|
||||
checkboxes: {
|
||||
template: base + 'checkboxes.html',
|
||||
builder: [sfField, ngModelOptions, ngModel, array, condition]
|
||||
},
|
||||
number: {
|
||||
template: base + 'default.html',
|
||||
builder: defaults
|
||||
},
|
||||
password: {
|
||||
template: base + 'default.html',
|
||||
builder: defaults
|
||||
},
|
||||
submit: {
|
||||
template: base + 'submit.html',
|
||||
builder: defaults
|
||||
},
|
||||
button: {
|
||||
template: base + 'submit.html',
|
||||
builder: defaults
|
||||
},
|
||||
radios: {
|
||||
template: base + 'radios.html',
|
||||
builder: defaults
|
||||
},
|
||||
'radios-inline': {
|
||||
template: base + 'radios-inline.html',
|
||||
builder: defaults
|
||||
},
|
||||
radiobuttons: {
|
||||
template: base + 'radio-buttons.html',
|
||||
builder: defaults
|
||||
},
|
||||
help: {
|
||||
template: base + 'help.html',
|
||||
builder: defaults
|
||||
},
|
||||
'default': {
|
||||
template: base + 'default.html',
|
||||
builder: defaults
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Define and register our validation messages
|
||||
// These are the error codes provided by the tv4 validator:
|
||||
// https://github.com/geraintluff/tv4/blob/master/source/api.js
|
||||
var defaultMessages = {
|
||||
"default": gettext("The data in this field is invalid"),
|
||||
0: gettext("Invalid type, expected {$schema.type$}"),
|
||||
1: gettext("No enum match for: {$viewValue$}"),
|
||||
10: gettext("Data does not match any schemas from 'anyOf'"),
|
||||
11: gettext("Data does not match any schemas from 'oneOf'"),
|
||||
12: gettext("Data is valid against more than one schema from 'oneOf'"),
|
||||
13: gettext("Data matches schema from 'not'"),
|
||||
// Numeric errors
|
||||
100: gettext("{$viewValue$} is not a multiple of {$schema.multipleOf$}"),
|
||||
101: gettext("{$viewValue$} is less than the allowed minimum of {$schema.minimum$}"),
|
||||
102: gettext("{$viewValue$} is equal to the exclusive minimum {$schema.minimum$}"),
|
||||
103: gettext("{$viewValue$} is greater than the allowed maximum of {$schema.maximum$}"),
|
||||
104: gettext("{$viewValue$} is equal to the exclusive maximum {$schema.maximum$}"),
|
||||
105: gettext("{$viewValue$} is not a valid number"),
|
||||
// String errors
|
||||
/* eslint-disable max-len */
|
||||
200: gettext("{$schema.title$} is too short ({$viewValue.length$} characters), minimum {$schema.minLength$}"),
|
||||
201: gettext("{$schema.title$} is too long ({$viewValue.length$} characters), maximum {$schema.maxLength$}"),
|
||||
/* eslint-enable max-len */
|
||||
202: gettext("{$schema.title$} is formatted incorrectly"),
|
||||
// Object errors
|
||||
300: gettext("Too few properties defined, minimum {$schema.minProperties$}"),
|
||||
301: gettext("Too many properties defined, maximum {$schema.maxProperties$}"),
|
||||
302: gettext("{$schema.title$} is a required field"),
|
||||
303: gettext("Additional properties not allowed"),
|
||||
304: gettext("Dependency failed - key must exist"),
|
||||
// Array errors
|
||||
400: gettext("Array is too short ({$value.length$} items), minimum {$schema.minItems$}"),
|
||||
401: gettext("Array is too long ({$value.length$} items), maximum {$schema.maxItems$}"),
|
||||
402: gettext("Array items must be unique"),
|
||||
403: gettext("Additional items not allowed"),
|
||||
// Format errors
|
||||
500: gettext("Format validation failed"),
|
||||
501: gettext("Keyword failed: '{$title$}'"),
|
||||
// Schema structure
|
||||
600: gettext("Circular $refs"),
|
||||
// Non-standard validation options
|
||||
1000: gettext("Unknown property (not in schema)")
|
||||
};
|
||||
|
||||
sfErrorMessageProvider.setDefaultMessages(defaultMessages);
|
||||
}
|
||||
})();
|
60
horizon/static/framework/widgets/form/decorator.spec.js
Normal file
60
horizon/static/framework/widgets/form/decorator.spec.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Cisco Systems
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('schemaForm decorator', function() {
|
||||
var decoratorsProvider, sfErrorMessageProvider;
|
||||
|
||||
beforeEach(module('schemaForm'));
|
||||
beforeEach(module('templates'));
|
||||
beforeEach(inject(function($injector) {
|
||||
decoratorsProvider = $injector.get('schemaFormDecorators');
|
||||
sfErrorMessageProvider = $injector.get('sfErrorMessage');
|
||||
}));
|
||||
|
||||
it('should be defined', function() {
|
||||
expect(angular.module('schemaForm')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should build tabs correctly', function() {
|
||||
});
|
||||
|
||||
it('should define messages for all the error codes', function() {
|
||||
// We don't need to check the specifics of each message in a test,
|
||||
// but we should check they all exist
|
||||
var messageCodes = Object.keys(sfErrorMessageProvider.defaultMessages);
|
||||
var expectedMessageCodes = [
|
||||
'0', '1', '10', '11', '12', '13', '100', '101', '102', '103', '104',
|
||||
'105', '200', '201', '202', '300', '301', '302', '303', '304', '400',
|
||||
'401', '402', '403', '500', '501', '600', '1000', 'default'
|
||||
];
|
||||
expect(messageCodes).toEqual(expectedMessageCodes);
|
||||
});
|
||||
|
||||
it('should define all the fields', function() {
|
||||
var fields = Object.keys(decoratorsProvider.decorator('bootstrapDecorator'));
|
||||
var expectedFields = [
|
||||
'__name', 'textarea', 'fieldset', 'array', 'tabarray', 'tabs', 'section',
|
||||
'conditional', 'select', 'checkbox', 'checkboxes', 'number',
|
||||
'password', 'submit', 'button', 'radios', 'radios-inline', 'radiobuttons',
|
||||
'help', 'default'
|
||||
];
|
||||
expect(fields).toEqual(expectedFields);
|
||||
});
|
||||
});
|
||||
})();
|
39
horizon/static/framework/widgets/form/fields/array.html
Normal file
39
horizon/static/framework/widgets/form/fields/array.html
Normal file
@ -0,0 +1,39 @@
|
||||
<div class="hz-array {$::form.htmlClass$}"
|
||||
sf-field-model="sf-new-array"
|
||||
sf-new-array>
|
||||
<label class="control-label" ng-show="showTitle()">{$:: form.title $}</label>
|
||||
<ol class="list-group" sf-field-model ui-sortable="form.sortOptions">
|
||||
<li class="list-group-item {$::form.fieldHtmlClass$}"
|
||||
schema-form-array-items
|
||||
sf-field-model="ng-repeat"
|
||||
ng-repeat="item in $$value$$ track by $index">
|
||||
<button ng-hide="form.readonly || form.remove === null"
|
||||
ng-click="deleteFromArray($index)"
|
||||
ng-disabled="form.schema.minItems >= modelArray.length"
|
||||
type="button" class="close pull-right">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">
|
||||
<translate>Close</translate>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="clearfix" ng-model="modelArray" schema-validate="form">
|
||||
<div class="help-block"
|
||||
ng-show="(hasError() && errorMessage(schemaError())) || form.description"
|
||||
ng-bind-html="(hasError() && errorMessage(schemaError())) || form.description">
|
||||
</div>
|
||||
|
||||
<button ng-hide="form.readonly || form.add === null"
|
||||
ng-click="appendToArray()"
|
||||
ng-disabled="form.schema.maxItems <= modelArray.length"
|
||||
type="button"
|
||||
class="btn {$::form.style.add || 'btn-default' $} pull-right">
|
||||
<span class="fa fa-plus"></span>
|
||||
{$::form.add$}
|
||||
<span ng-if="!form.add">
|
||||
<translate>Add</translate>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
18
horizon/static/framework/widgets/form/fields/checkbox.html
Normal file
18
horizon/static/framework/widgets/form/fields/checkbox.html
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="form-group hz-checkbox">
|
||||
<div class="themable-checkbox {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<input type="checkbox"
|
||||
sf-changed="form"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
schema-validate="form"
|
||||
class="themable-checkbox {$::form.fieldHtmlClass$}"
|
||||
name="{$::form.key.slice(-1)[0]$}"
|
||||
id="{$::form.key.slice(-1)[0]$}">
|
||||
<label for="{$::form.key.slice(-1)[0]$}"
|
||||
class="{$::form.labelHtmlClass$}">
|
||||
<span>{$::form.title$}</span>
|
||||
</label>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
||||
</div>
|
22
horizon/static/framework/widgets/form/fields/checkboxes.html
Normal file
22
horizon/static/framework/widgets/form/fields/checkboxes.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div sf-field-model="sf-new-array"
|
||||
sf-new-array
|
||||
class="hz-checkboxes form-group {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<label class="control-label {$::form.labelHtmlClass$}"
|
||||
sf-field-model
|
||||
schema-validate="form"
|
||||
ng-show="showTitle()">{$::form.title$}</label>
|
||||
|
||||
<div class="checkbox" ng-repeat="val in titleMapValues track by $index" >
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-disabled="form.readonly"
|
||||
sf-changed="form"
|
||||
class="{$::form.fieldHtmlClass$}"
|
||||
ng-model="titleMapValues[$index]"
|
||||
name="{$::form.key.slice(-1)[0]$}">
|
||||
<span ng-bind-html="form.titleMap[$index].name"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
55
horizon/static/framework/widgets/form/fields/default.html
Normal file
55
horizon/static/framework/widgets/form/fields/default.html
Normal file
@ -0,0 +1,55 @@
|
||||
<div class="form-group hz-input {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess(), 'has-feedback': form.feedback !== false }">
|
||||
<label class="control-label {$::form.labelHtmlClass$}" ng-class="{'sr-only': !showTitle()}" for="{$::form.key.slice(-1)[0]$}">
|
||||
<span>{$::form.title$}</span>
|
||||
<span ng-if="form.required" class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
|
||||
<input ng-if="!form.fieldAddonLeft && !form.fieldAddonRight"
|
||||
ng-show="form.key"
|
||||
type="{$::form.type$}"
|
||||
step="any"
|
||||
sf-changed="form"
|
||||
placeholder="{$::form.placeholder$}"
|
||||
class="form-control {$::form.fieldHtmlClass$}"
|
||||
id="{$::form.key.slice(-1)[0]$}"
|
||||
sf-field-model
|
||||
ng-disabled="form.readonly"
|
||||
schema-validate="form"
|
||||
name="{$::form.key.slice(-1)[0]$}"
|
||||
aria-describedby="{$::form.key.slice(-1)[0] + 'Status'$}">
|
||||
|
||||
<div ng-if="form.fieldAddonLeft || form.fieldAddonRight"
|
||||
ng-class="{'input-group': (form.fieldAddonLeft || form.fieldAddonRight)}">
|
||||
<span ng-if="form.fieldAddonLeft"
|
||||
class="input-group-addon"
|
||||
ng-bind-html="form.fieldAddonLeft"></span>
|
||||
<input ng-show="form.key"
|
||||
type="{$::form.type$}"
|
||||
step="any"
|
||||
sf-changed="form"
|
||||
placeholder="{$::form.placeholder$}"
|
||||
class="form-control {$::form.fieldHtmlClass$}"
|
||||
id="{$::form.key.slice(-1)[0]$}"
|
||||
sf-field-model
|
||||
ng-disabled="form.readonly"
|
||||
schema-validate="form"
|
||||
name="{$::form.key.slice(-1)[0]$}"
|
||||
aria-describedby="{$::form.key.slice(-1)[0] + 'Status'$}">
|
||||
|
||||
<span ng-if="form.fieldAddonRight"
|
||||
class="input-group-addon"
|
||||
ng-bind-html="form.fieldAddonRight"></span>
|
||||
</div>
|
||||
|
||||
<span ng-if="form.feedback !== false"
|
||||
class="form-control-feedback"
|
||||
ng-class="evalInScope(form.feedback) || {'fa': true, 'fa-check': hasSuccess(), 'fa-times': hasError() }"
|
||||
aria-hidden="true"></span>
|
||||
|
||||
<span ng-if="hasError() || hasSuccess()"
|
||||
id="{$::form.key.slice(-1)[0] + 'Status'$}"
|
||||
class="sr-only">{$ hasSuccess() ? '(success)' : '(error)' $}</span>
|
||||
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
<fieldset ng-disabled="form.readonly" class="hz-fieldset {$::form.htmlClass$}">
|
||||
<legend ng-class="{'sr-only': !showTitle() }">{$:: form.title $}</legend>
|
||||
<div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div>
|
||||
</fieldset>
|
1
horizon/static/framework/widgets/form/fields/help.html
Normal file
1
horizon/static/framework/widgets/form/fields/help.html
Normal file
@ -0,0 +1 @@
|
||||
<p class="hz-help {$::form.htmlClass$}" ng-bind-html="form.helpvalue"></p>
|
@ -0,0 +1,23 @@
|
||||
<div class="form-group hz-radio-buttons {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<div>
|
||||
<label class="{$::form.labelHtmlClass$}" ng-show="showTitle()">{$::form.title$}</label>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<label sf-field-model="replaceAll" class="btn {$:: (item.value === $$value$$) ? form.style.selected || 'btn-default' : form.style.unselected || 'btn-default'; $}"
|
||||
ng-class="{ active: item.value === $$value$$ }"
|
||||
ng-repeat="item in form.titleMap">
|
||||
<input type="radio"
|
||||
class="{$::form.fieldHtmlClass$}"
|
||||
sf-changed="form"
|
||||
style="display: none;"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
schema-validate="form"
|
||||
ng-value="item.value"
|
||||
name="{$::form.key.join('.')$}">
|
||||
<span>{$::item.name$}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
@ -0,0 +1,19 @@
|
||||
<div class="hz-radios-inline form-group {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<label class="control-label {$::form.labelHtmlClass$}"
|
||||
ng-show="showTitle()" sf-field-model
|
||||
schema-validate="form" >{$::form.title$}</label>
|
||||
<div>
|
||||
<label class="radio-inline" ng-repeat="item in form.titleMap">
|
||||
<input type="radio"
|
||||
class="{$::form.fieldHtmlClass$}"
|
||||
sf-changed="form"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
ng-value="item.value"
|
||||
name="{$::form.key.join('.')$}">
|
||||
<span ng-bind-html="item.name"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
19
horizon/static/framework/widgets/form/fields/radios.html
Normal file
19
horizon/static/framework/widgets/form/fields/radios.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div class="hz-radios form-group {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<label class="control-label {$::form.labelHtmlClass$}"
|
||||
sf-field-model schema-validate="form"
|
||||
ng-show="showTitle()">{$::form.title$}</label>
|
||||
<div class="radio" ng-repeat="item in form.titleMap">
|
||||
<label>
|
||||
<input type="radio"
|
||||
class="{$::form.fieldHtmlClass$}"
|
||||
sf-changed="form"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
ng-value="item.value"
|
||||
name="{$::form.key.join('.')$}">
|
||||
<span ng-bind-html="item.name"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
<div class="hz-section {$::form.htmlClass$}"></div>
|
16
horizon/static/framework/widgets/form/fields/select.html
Normal file
16
horizon/static/framework/widgets/form/fields/select.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="hz-select form-group {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-feedback': form.feedback !== false}">
|
||||
<label class="control-label {$::form.labelHtmlClass$}" ng-show="showTitle()">
|
||||
<span>{$::form.title$}</span>
|
||||
<span ng-if="form.required" class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<select sf-field-model
|
||||
ng-disabled="form.readonly"
|
||||
sf-changed="form"
|
||||
class="form-control {$::form.fieldHtmlClass$}"
|
||||
schema-validate="form"
|
||||
ng-options="item.value as item.name group by item.group for item in form.titleMap"
|
||||
name="{$::form.key.slice(-1)[0]$}">
|
||||
</select>
|
||||
<div class="help-block" sf-message="form.description"></div>
|
||||
</div>
|
15
horizon/static/framework/widgets/form/fields/submit.html
Normal file
15
horizon/static/framework/widgets/form/fields/submit.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="hz-submit form-group {$::form.htmlClass$}">
|
||||
<input type="submit"
|
||||
class="btn {$:: form.style || 'btn-primary' $} {$::form.fieldHtmlClass$}"
|
||||
value="{$::form.title$}"
|
||||
ng-disabled="form.readonly"
|
||||
ng-if="form.type === 'submit'">
|
||||
<button class="btn {$:: form.style || 'btn-default' $}"
|
||||
type="button"
|
||||
ng-click="buttonClick($event,form)"
|
||||
ng-disabled="form.readonly"
|
||||
ng-if="form.type !== 'submit'">
|
||||
<span ng-if="form.icon" class="{$::form.icon$}"></span>
|
||||
<span class="hz-submit-title">{$::form.title$}</span>
|
||||
</button>
|
||||
</div>
|
70
horizon/static/framework/widgets/form/fields/tabarray.html
Normal file
70
horizon/static/framework/widgets/form/fields/tabarray.html
Normal file
@ -0,0 +1,70 @@
|
||||
<div ng-init="selected = { tab: 0 }"
|
||||
ng-model="modelArray" schema-validate="form"
|
||||
sf-field-model="sf-new-array"
|
||||
sf-new-array
|
||||
class="clearfix hz-tabarray schema-form-tabarray-{$form.tabType || 'left'$} {$form.htmlClass$}">
|
||||
<div ng-if="!form.tabType || form.tabType !== 'right'"
|
||||
ng-class="{'col-xs-3': !form.tabType || form.tabType === 'left'}">
|
||||
<ul class="nav nav-tabs"
|
||||
ng-class="{ 'tabs-left': !form.tabType || form.tabType === 'left'}">
|
||||
<li sf-field-model="ng-repeat"
|
||||
ng-repeat="item in $$value$$ track by $index"
|
||||
ng-click="$event.preventDefault() || (selected.tab = $index)"
|
||||
ng-class="{active: selected.tab === $index}">
|
||||
<a href="#">{$interp(form.title,{'$index':$index, value: item}) || $index$}</a>
|
||||
</li>
|
||||
<li ng-hide="form.readonly"
|
||||
ng-disabled="form.schema.maxItems <= modelArray.length"
|
||||
ng-click="$event.preventDefault() || (selected.tab = appendToArray().length - 1)">
|
||||
<a href="#">
|
||||
<span class="fa fa-plus"></span>
|
||||
<span class="hz-tabarray-add">{$ form.add || 'Add'$}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div ng-class="{'col-xs-9': !form.tabType || form.tabType === 'left' || form.tabType === 'right'}">
|
||||
<div class="tab-content {$form.fieldHtmlClass$}">
|
||||
<div class="tab-pane clearfix tab{$selected.tab$} index{$$index$}"
|
||||
sf-field-model="ng-repeat"
|
||||
ng-repeat="item in $$value$$ track by $index"
|
||||
ng-show="selected.tab === $index"
|
||||
ng-class="{active: selected.tab === $index}">
|
||||
|
||||
<div schema-form-array-items></div>
|
||||
<button ng-hide="form.readonly"
|
||||
ng-click="selected.tab = deleteFromArray($index).length - 1"
|
||||
ng-disabled="form.schema.minItems >= modelArray.length"
|
||||
type="button"
|
||||
class="btn {$ form.style.remove || 'btn-default' $} pull-right">
|
||||
<span class="fa fa-trash"></span>
|
||||
<span class="hz-tabarray-remove">{$::form.remove || 'Remove'$}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-block"
|
||||
ng-show="(hasError() && errorMessage(schemaError())) || form.description"
|
||||
ng-bind-html="(hasError() && errorMessage(schemaError())) || form.description">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="form.tabType === 'right'" class="col-xs-3">
|
||||
<ul class="nav nav-tabs tabs-right">
|
||||
<li sf-field-model="ng-repeat"
|
||||
ng-repeat="item in $$value$$ track by $index"
|
||||
ng-click="$event.preventDefault() || (selected.tab = $index)"
|
||||
ng-class="{active: selected.tab === $index}">
|
||||
<a href="#">{$interp(form.title,{'$index':$index, value: item}) || $index$}</a>
|
||||
</li>
|
||||
<li ng-hide="form.readonly"
|
||||
ng-disabled="form.schema.maxItems <= modelArray.length"
|
||||
ng-click="$event.preventDefault() || (selected.tab = appendToArray().length - 1)">
|
||||
<a href="#">
|
||||
<span class="fa fa-plus"></span>
|
||||
<span class="hz-tabarray-add">{$::form.add || 'Add'$}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
41
horizon/static/framework/widgets/form/fields/tabs.html
Normal file
41
horizon/static/framework/widgets/form/fields/tabs.html
Normal file
@ -0,0 +1,41 @@
|
||||
<div ng-init="model.tabs = { selected: 0 }" class="row hz-tabs {$::form.htmlClass$}">
|
||||
<div class="col-xs-12 col-sm-3">
|
||||
<button type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#wizard-side-nav"
|
||||
aria-expanded="false"
|
||||
class="navbar-toggle btn btn-default collapsed wizard-nav-toggle">
|
||||
<span translate class="sr-only">Toggle navigation</span>
|
||||
<span class="fa fa-bars"></span>
|
||||
<span translate>Toggle navigation</span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse wizard-nav" id="wizard-side-nav">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li role="presentation"
|
||||
class="nav-item"
|
||||
ng-repeat="tab in form.tabs"
|
||||
ng-init="model.tabs.length = form.tabs.length"
|
||||
ng-disabled="form.readonly"
|
||||
ng-click="$event.preventDefault() || (model.tabs.selected = $index)"
|
||||
ng-class="{active: model.tabs.selected === $index}">
|
||||
<a href="#">
|
||||
<span>{$::tab.title$}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"
|
||||
ng-show="tab.required"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content col-xs-12 col-sm-9 {$::form.fieldHtmlClass$}"></div>
|
||||
|
||||
<help-panel>
|
||||
<ng-include src="tab.help"
|
||||
ng-repeat="tab in form.tabs track by $index"
|
||||
ng-show="model.tabs.selected === $index">
|
||||
</ng-include>
|
||||
</help-panel>
|
||||
</div>
|
39
horizon/static/framework/widgets/form/fields/textarea.html
Normal file
39
horizon/static/framework/widgets/form/fields/textarea.html
Normal file
@ -0,0 +1,39 @@
|
||||
<div class="form-group hz-textarea has-feedback {$::form.htmlClass$}"
|
||||
ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
|
||||
<label class="control-label {$::form.labelHtmlClass$}"
|
||||
ng-class="{'sr-only': !showTitle()}"
|
||||
for="{$::form.key.slice(-1)[0]$}">
|
||||
<span>{$::form.title$}</span>
|
||||
<span ng-if="form.required" class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
|
||||
<textarea ng-if="!form.fieldAddonLeft && !form.fieldAddonRight"
|
||||
class="form-control {$::form.fieldHtmlClass$}"
|
||||
id="{$::form.key.slice(-1)[0]$}"
|
||||
sf-changed="form"
|
||||
placeholder="{$::form.placeholder$}"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
schema-validate="form"
|
||||
name="{$::form.key.slice(-1)[0]$}"></textarea>
|
||||
|
||||
<div ng-if="form.fieldAddonLeft || form.fieldAddonRight"
|
||||
ng-class="{'input-group': (form.fieldAddonLeft || form.fieldAddonRight)}">
|
||||
<span ng-if="form.fieldAddonLeft"
|
||||
class="input-group-addon"
|
||||
ng-bind-html="form.fieldAddonLeft"></span>
|
||||
<textarea class="form-control {$::form.fieldHtmlClass$}"
|
||||
id="{$::form.key.slice(-1)[0]$}"
|
||||
sf-changed="form"
|
||||
placeholder="{$::form.placeholder$}"
|
||||
ng-disabled="form.readonly"
|
||||
sf-field-model
|
||||
schema-validate="form"
|
||||
name="{$::form.key.slice(-1)[0]$}"></textarea>
|
||||
<span ng-if="form.fieldAddonRight"
|
||||
class="input-group-addon"
|
||||
ng-bind-html="form.fieldAddonRight"></span>
|
||||
</div>
|
||||
|
||||
<span class="help-block" sf-message="form.description"></span>
|
||||
</div>
|
25
horizon/static/framework/widgets/form/form.module.js
Normal file
25
horizon/static/framework/widgets/form/form.module.js
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2016 Cisco Systems, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name horizon.framework.widgets.form
|
||||
*/
|
||||
angular
|
||||
.module('horizon.framework.widgets.form', []);
|
||||
})();
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name horizon.framework.widgets.form.ModalFormController
|
||||
*
|
||||
* @param(object) modal instance from angular-bootstrap
|
||||
* @param(object) context object provided by the user
|
||||
*
|
||||
* @description
|
||||
* Controller for a schema-form based modal.
|
||||
* If user presses cancel button or closes dialog, modal gets dismissed.
|
||||
* If user presses submit button, form input is validated then the modal
|
||||
* is closed and the context object is passed back so that the caller can
|
||||
* use any of the inputs.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.framework.widgets.form')
|
||||
.controller('horizon.framework.widgets.form.ModalFormController', controller);
|
||||
|
||||
controller.$inject = [
|
||||
'$modalInstance',
|
||||
'context'
|
||||
];
|
||||
|
||||
function controller($modalInstance, context) {
|
||||
var ctrl = this;
|
||||
ctrl.formTitle = context.title;
|
||||
ctrl.form = context.form;
|
||||
ctrl.schema = context.schema;
|
||||
ctrl.model = context.model;
|
||||
ctrl.submit = submit;
|
||||
ctrl.submitText = context.submitText;
|
||||
ctrl.submitIcon = context.submitIcon;
|
||||
ctrl.cancel = cancel;
|
||||
|
||||
function submit() {
|
||||
return $modalInstance.close(context);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
return $modalInstance.dismiss(context);
|
||||
}
|
||||
|
||||
return ctrl;
|
||||
}
|
||||
})();
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('modal-form controller', function () {
|
||||
var ctrl, modalInstance, context;
|
||||
|
||||
beforeEach(module('horizon.framework.widgets.form'));
|
||||
|
||||
beforeEach(inject(function ($controller) {
|
||||
modalInstance = {
|
||||
close: angular.noop,
|
||||
dismiss: angular.noop
|
||||
};
|
||||
context = {
|
||||
title: "title",
|
||||
form: "form",
|
||||
schema: "schema",
|
||||
model: "model"
|
||||
};
|
||||
ctrl = $controller(
|
||||
'horizon.framework.widgets.form.ModalFormController',
|
||||
{
|
||||
$modalInstance: modalInstance,
|
||||
context: context
|
||||
});
|
||||
}));
|
||||
|
||||
it('sets formTitle on scope', function() {
|
||||
expect(ctrl.formTitle).toEqual('title');
|
||||
});
|
||||
|
||||
it('sets form on scope', function() {
|
||||
expect(ctrl.form).toEqual('form');
|
||||
});
|
||||
|
||||
it('sets schema on scope', function() {
|
||||
expect(ctrl.schema).toEqual('schema');
|
||||
});
|
||||
|
||||
it('sets model on scope', function() {
|
||||
expect(ctrl.model).toEqual('model');
|
||||
});
|
||||
|
||||
it('calls modalInstance close on submit', function() {
|
||||
spyOn(modalInstance, 'close');
|
||||
ctrl.submit();
|
||||
expect(modalInstance.close.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('calls modalInstance dismiss on cancel', function() {
|
||||
spyOn(modalInstance, 'dismiss');
|
||||
ctrl.cancel();
|
||||
expect(modalInstance.dismiss.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
}());
|
51
horizon/static/framework/widgets/form/modal-form.html
Normal file
51
horizon/static/framework/widgets/form/modal-form.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="modal-header">
|
||||
<a class="close" ng-click="$dismiss()">
|
||||
<span class="fa fa-times"></span>
|
||||
</a>
|
||||
<div class="h4 modal-title">
|
||||
{$::ctrl.formTitle$}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form name="schemaForm"
|
||||
sf-schema="ctrl.schema"
|
||||
sf-form="ctrl.form"
|
||||
sf-model="ctrl.model"
|
||||
sf-options="{ validateOnRender: true,
|
||||
pristine: { errors: false, success: false } }">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="$dismiss()">
|
||||
<span class="fa fa-close"></span>
|
||||
<translate>Cancel</translate>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-default"
|
||||
ng-click="ctrl.model.tabs.selected = ctrl.model.tabs.selected - 1"
|
||||
ng-disabled="ctrl.model.tabs.selected === 0"
|
||||
ng-if="ctrl.model.tabs.length > 1">
|
||||
<span class="fa fa-angle-left"></span>
|
||||
<translate>Back</translate>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-default"
|
||||
ng-click="ctrl.model.tabs.selected = ctrl.model.tabs.selected + 1"
|
||||
ng-if="ctrl.model.tabs.length > 1"
|
||||
ng-disabled="ctrl.model.tabs.selected === ctrl.model.tabs.length-1">
|
||||
<translate>Next</translate>
|
||||
<span class="fa fa-angle-right"></span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
ng-disabled="schemaForm.$invalid"
|
||||
ng-click="ctrl.submit()">
|
||||
<span class="fa fa-{$::ctrl.submitIcon$}"></span>
|
||||
{$::ctrl.submitText$}
|
||||
</button>
|
||||
</div>
|
72
horizon/static/framework/widgets/form/modal-form.service.js
Normal file
72
horizon/static/framework/widgets/form/modal-form.service.js
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
*
|
||||
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use self file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.framework.widgets.form')
|
||||
.factory('horizon.framework.widgets.form.ModalFormService', service);
|
||||
|
||||
service.$inject = [
|
||||
'$modal',
|
||||
'horizon.framework.widgets.basePath'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.framework.widgets.form.ModalFormService
|
||||
*
|
||||
* @Description
|
||||
* Loads a Schema-Form (see modal-form.html) in a modal and returns the modal result promise.
|
||||
*/
|
||||
function service(
|
||||
$modal,
|
||||
widgetsBasePath
|
||||
) {
|
||||
|
||||
var service = {
|
||||
open: open
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
/////////////////
|
||||
|
||||
function open(config) {
|
||||
var modalConfig = {
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
context: function() {
|
||||
return {
|
||||
title: config.title,
|
||||
submitText: config.submitText || gettext("Submit"),
|
||||
submitIcon: config.submitIcon || "check",
|
||||
schema: config.schema,
|
||||
form: config.form,
|
||||
model: config.model
|
||||
};
|
||||
}
|
||||
},
|
||||
controller: 'horizon.framework.widgets.form.ModalFormController as ctrl',
|
||||
templateUrl: widgetsBasePath + 'form/modal-form.html'
|
||||
};
|
||||
|
||||
return $modal.open(modalConfig).result;
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('modal-form service', function () {
|
||||
var service, $modal;
|
||||
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.framework.widgets.form'));
|
||||
|
||||
beforeEach(inject(function ($injector, _$modal_) {
|
||||
$modal = _$modal_;
|
||||
service = $injector.get(
|
||||
'horizon.framework.widgets.form.ModalFormService'
|
||||
);
|
||||
}));
|
||||
|
||||
it('sets open parameters to modal resolve.context', function() {
|
||||
spyOn($modal, 'open').and.callFake(function(config) {
|
||||
return {
|
||||
result: config
|
||||
};
|
||||
});
|
||||
var modalConfig = {
|
||||
"title": "title",
|
||||
"schema": "schema",
|
||||
"form": "form",
|
||||
"model": "model",
|
||||
"submitIcon": "icon",
|
||||
"submitText": "save"
|
||||
};
|
||||
var modalService = service.open(modalConfig);
|
||||
var context = modalService.resolve.context();
|
||||
expect(context.title).toEqual('title');
|
||||
expect(context.schema).toEqual('schema');
|
||||
expect(context.form).toEqual('form');
|
||||
expect(context.model).toEqual('model');
|
||||
expect(context.submitIcon).toEqual('icon');
|
||||
expect(context.submitText).toEqual('save');
|
||||
});
|
||||
|
||||
it('sets default values for optional parameters', function() {
|
||||
spyOn($modal, 'open').and.callFake(function(config) {
|
||||
return {
|
||||
result: config
|
||||
};
|
||||
});
|
||||
var modalConfig = {
|
||||
"title": "title",
|
||||
"schema": "schema",
|
||||
"form": "form",
|
||||
"model": "model"
|
||||
};
|
||||
var modalService = service.open(modalConfig);
|
||||
var context = modalService.resolve.context();
|
||||
expect(context.submitIcon).toEqual('check');
|
||||
expect(context.submitText).toEqual('Submit');
|
||||
});
|
||||
});
|
||||
}());
|
@ -19,7 +19,10 @@
|
||||
.module('horizon.framework.widgets.help-panel')
|
||||
.directive('helpPanel', helpPanel);
|
||||
|
||||
helpPanel.$inject = [ 'horizon.framework.widgets.basePath' ];
|
||||
helpPanel.$inject = [
|
||||
'horizon.framework.widgets.basePath',
|
||||
'horizon.framework.util.uuid.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@ -38,9 +41,14 @@
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
function helpPanel(path) {
|
||||
function helpPanel(path, uuid) {
|
||||
var link = function(scope) {
|
||||
scope.uuid = uuid.generate();
|
||||
};
|
||||
|
||||
var directive = {
|
||||
templateUrl: path + 'help-panel/help-panel.html',
|
||||
link: link,
|
||||
transclude: true
|
||||
};
|
||||
|
||||
|
@ -35,13 +35,13 @@
|
||||
});
|
||||
|
||||
it('should be closed by default', function () {
|
||||
expect(element[0].querySelector('#help-panel').className).toBe('collapse width');
|
||||
expect(element[0].querySelector('.help-panel').className).toContain('collapse width');
|
||||
});
|
||||
|
||||
it('should add "in" to class name if $scope.openHelp is true', function () {
|
||||
$scope.openHelp = true;
|
||||
$scope.$apply();
|
||||
expect(element[0].querySelector('#help-panel').className).toBe('collapse width in');
|
||||
expect(element[0].querySelector('.help-panel').className).toContain('collapse width in');
|
||||
});
|
||||
|
||||
it('should remove "in" from class name if $scope.openHelp is false', function () {
|
||||
@ -49,7 +49,7 @@
|
||||
$scope.$apply();
|
||||
$scope.openHelp = false;
|
||||
$scope.$apply();
|
||||
expect(element[0].querySelector('#help-panel').className).toBe('collapse width');
|
||||
expect(element[0].querySelector('.help-panel').className).toContain('collapse width');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
<button class="btn btn-default help-toggle collapsed" data-target="#help-panel"
|
||||
data-toggle="collapse" role="button" aria-expanded="false"
|
||||
aria-controls="help-panel" ng-hide="hideHelpBtn">
|
||||
<button class="btn btn-default help-toggle collapsed" data-target="#{$::uuid$}"
|
||||
data-toggle="collapse" role="button" aria-expanded="false"
|
||||
aria-controls="{$::uuid$}" ng-hide="hideHelpBtn" type="button">
|
||||
<span class="fa"></span>
|
||||
</button>
|
||||
|
||||
<div id="help-panel" class="collapse width" ng-class="{'in': openHelp}">
|
||||
<div class="well" ng-transclude></div>
|
||||
<div id="{$::uuid$}" class="help-panel collapse width" ng-class="{'in': openHelp}">
|
||||
<div class="well" ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@
|
||||
.module('horizon.framework.widgets', [
|
||||
'horizon.framework.widgets.headers',
|
||||
'horizon.framework.widgets.details',
|
||||
'horizon.framework.widgets.form',
|
||||
'horizon.framework.widgets.help-panel',
|
||||
'horizon.framework.widgets.wizard',
|
||||
'horizon.framework.widgets.property',
|
||||
|
@ -61,7 +61,7 @@
|
||||
$scope.workflow = {};
|
||||
$scope.workflow.title = "doesn't matter";
|
||||
$scope.$apply();
|
||||
expect(element[0].querySelectorAll('#help-panel').length).toBe(1);
|
||||
expect(element[0].querySelectorAll('.help-panel').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should toggle help icon button', function () {
|
||||
|
@ -100,6 +100,9 @@ module.exports = function (config) {
|
||||
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
|
||||
xstaticPath + 'spin/data/spin.js',
|
||||
xstaticPath + 'spin/data/spin.jquery.js',
|
||||
xstaticPath + 'tv4/data/tv4.js',
|
||||
xstaticPath + 'objectpath/data/ObjectPath.js',
|
||||
xstaticPath + 'angular_schema_form/data/schema-form.js',
|
||||
|
||||
// TODO: These should be mocked. However, that could be complex
|
||||
// and there's less harm in exposing these directly. These are
|
||||
|
@ -26,6 +26,7 @@
|
||||
'lrDragNDrop',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'schemaForm',
|
||||
'smart-table',
|
||||
'ngFileUpload',
|
||||
'ui.bootstrap'
|
||||
|
@ -8,7 +8,6 @@
|
||||
/* Fix for input icon alignment */
|
||||
input + .fa {
|
||||
line-height: $input-height-base;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.password-icon {
|
||||
|
@ -2,14 +2,14 @@ $help-panel-width: 400px;
|
||||
|
||||
.help-toggle,
|
||||
.wizard-help,
|
||||
#help-panel {
|
||||
.help-panel {
|
||||
position: absolute;
|
||||
top: $padding-xs-horizontal;
|
||||
right: 0;
|
||||
z-index: 2; // TODO(robcresswell) untangle the need for this sorcery
|
||||
}
|
||||
|
||||
#help-panel > div {
|
||||
.help-panel > div {
|
||||
width: $help-panel-width;
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import xstatic.pkg.angular_bootstrap
|
||||
import xstatic.pkg.angular_fileupload
|
||||
import xstatic.pkg.angular_gettext
|
||||
import xstatic.pkg.angular_lrdragndrop
|
||||
import xstatic.pkg.angular_schema_form
|
||||
import xstatic.pkg.angular_smart_table
|
||||
import xstatic.pkg.bootstrap_datepicker
|
||||
import xstatic.pkg.bootstrap_scss
|
||||
@ -40,10 +41,12 @@ import xstatic.pkg.jquery_tablesorter
|
||||
import xstatic.pkg.jquery_ui
|
||||
import xstatic.pkg.jsencrypt
|
||||
import xstatic.pkg.mdi
|
||||
import xstatic.pkg.objectpath
|
||||
import xstatic.pkg.rickshaw
|
||||
import xstatic.pkg.roboto_fontface
|
||||
import xstatic.pkg.spin
|
||||
import xstatic.pkg.termjs
|
||||
import xstatic.pkg.tv4
|
||||
|
||||
from horizon.utils import file_discovery
|
||||
|
||||
@ -67,6 +70,9 @@ def get_staticfiles_dirs(webroot='/'):
|
||||
('horizon/lib/angular',
|
||||
xstatic.main.XStatic(xstatic.pkg.angular_lrdragndrop,
|
||||
root_url=webroot).base_dir),
|
||||
('horizon/lib/angular',
|
||||
xstatic.main.XStatic(xstatic.pkg.angular_schema_form,
|
||||
root_url=webroot).base_dir),
|
||||
('horizon/lib/angular',
|
||||
xstatic.main.XStatic(xstatic.pkg.angular_smart_table,
|
||||
root_url=webroot).base_dir),
|
||||
@ -109,6 +115,9 @@ def get_staticfiles_dirs(webroot='/'):
|
||||
('horizon/lib/mdi',
|
||||
xstatic.main.XStatic(xstatic.pkg.mdi,
|
||||
root_url=webroot).base_dir),
|
||||
('horizon/lib/objectpath',
|
||||
xstatic.main.XStatic(xstatic.pkg.objectpath,
|
||||
root_url=webroot).base_dir),
|
||||
('horizon/lib',
|
||||
xstatic.main.XStatic(xstatic.pkg.rickshaw,
|
||||
root_url=webroot).base_dir),
|
||||
@ -121,6 +130,9 @@ def get_staticfiles_dirs(webroot='/'):
|
||||
('horizon/lib',
|
||||
xstatic.main.XStatic(xstatic.pkg.termjs,
|
||||
root_url=webroot).base_dir),
|
||||
('horizon/lib/tv4',
|
||||
xstatic.main.XStatic(xstatic.pkg.tv4,
|
||||
root_url=webroot).base_dir),
|
||||
]
|
||||
|
||||
if xstatic.main.XStatic(xstatic.pkg.jquery_ui,
|
||||
|
@ -66,6 +66,9 @@
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.firewalls.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.volumes.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/lib/jsencrypt/jsencrypt.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/lib/objectpath/ObjectPath.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/lib/tv4/tv4.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/lib/angular/schema-form.js'></script>
|
||||
|
||||
{% for file in HORIZON_CONFIG.js_files %}
|
||||
<script src='{{ STATIC_URL }}{{ file }}'></script>
|
||||
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
#help-panel {
|
||||
.help-panel {
|
||||
// Material is all about depth, lets add some
|
||||
& > .well {
|
||||
@extend .panel;
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- >
|
||||
[`blueprint angular-schema-form <https://blueprints.launchpad.net/horizon/+spec/angular-schema-form>`_]
|
||||
Added the Angular Schema Form library to Horizon. This
|
||||
allows developers to build angular forms and workflows
|
||||
from JSON. Read more at `<http://schemaform.io/>`_
|
@ -42,6 +42,7 @@ XStatic-Angular-Bootstrap>=0.11.0.2 # MIT License
|
||||
XStatic-Angular-FileUpload>=12.0.4.0 # MIT License
|
||||
XStatic-Angular-Gettext>=2.1.0.2 # MIT License
|
||||
XStatic-Angular-lrdragndrop>=1.0.2.2 # MIT License
|
||||
XStatic-Angular-Schema-Form>=0.8.13.0 # MIT
|
||||
XStatic-Bootstrap-Datepicker>=1.3.1.0 # Apache 2.0 License
|
||||
XStatic-Bootstrap-SCSS>=3 # Apache 2.0 License
|
||||
XStatic-bootswatch>=3.3.5.3 # MIT License
|
||||
@ -56,8 +57,10 @@ XStatic-JQuery.TableSorter>=2.14.5.1 # MIT License
|
||||
XStatic-jquery-ui>=1.10.1 # MIT License
|
||||
XStatic-JSEncrypt>=2.0.0.2 # MIT License
|
||||
XStatic-mdi>=1.4.57.0 # SIL OPEN FONT LICENSE Version 1.1
|
||||
XStatic-objectpath>=1.2.1.0 # MIT
|
||||
XStatic-Rickshaw>=1.5.0 # BSD License (prior)
|
||||
XStatic-roboto-fontface>=0.4.3.2 # Apache 2.0 License
|
||||
XStatic-smart-table>=1.4.5.3 # MIT License
|
||||
XStatic-Spin>=1.2.5.2 # MIT License
|
||||
XStatic-term.js>=0.0.4 # MIT License
|
||||
XStatic-tv4>=1.2.7.0 # MIT
|
||||
|
Loading…
Reference in New Issue
Block a user