From 19fa782fb424bb8ea0f7f9ff8b0d5cd507cb42ef Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 23 May 2017 16:38:58 -0400 Subject: [PATCH] Add form submission, validation, rejection messaging --- .../credentials/add-credentials.controller.js | 26 ++--- .../credentials/add-credentials.view.html | 10 +- awx/ui/client/features/credentials/index.js | 6 +- awx/ui/client/lib/components/_index.less | 1 - .../dynamic/input-group.directive.js | 3 +- awx/ui/client/lib/components/form/_index.less | 1 - .../lib/components/form/action.partial.html | 4 +- .../lib/components/form/form.directive.js | 80 +++++++++++--- awx/ui/client/lib/components/index.js | 1 - .../client/lib/components/input/_index.less | 12 ++- .../lib/components/input/base.controller.js | 28 +++-- .../lib/components/input/secret.partial.html | 10 +- .../lib/components/input/select.partial.html | 12 ++- .../lib/components/input/text.partial.html | 11 +- .../components/input/textarea.partial.html | 13 ++- .../lib/components/panel/panel.directive.js | 17 ++- awx/ui/client/lib/models/Base.js | 102 +++++++++++------- awx/ui/client/lib/models/Credential.js | 20 +++- awx/ui/client/lib/models/CredentialType.js | 8 +- awx/ui/client/lib/models/index.js | 7 -- awx/ui/client/src/app.js | 1 - 21 files changed, 252 insertions(+), 121 deletions(-) delete mode 100644 awx/ui/client/lib/components/form/_index.less diff --git a/awx/ui/client/features/credentials/add-credentials.controller.js b/awx/ui/client/features/credentials/add-credentials.controller.js index e980407e74..ad1b9ae9d4 100644 --- a/awx/ui/client/features/credentials/add-credentials.controller.js +++ b/awx/ui/client/features/credentials/add-credentials.controller.js @@ -3,24 +3,26 @@ function AddCredentialsController (models) { let credential = models.credential; let credentialType = models.credentialType; - - vm.name = credential.getPostOptions('name'); - vm.description = credential.getPostOptions('description'); - vm.kind = Object.assign({ - data: credentialType.categorizeByKind(), - placeholder: 'Select a Type' - }, credential.getPostOptions('credential_type')); + vm.form = credential.createFormSchema('post', { + omit: ['user', 'team', 'inputs'] + }); - vm.dynamic = { - getInputs: credentialType.getTypeFromName, - source: vm.kind, - reference: 'vm.dynamic' + vm.form.credential_type.data = credentialType.categorizeByKind(); + vm.form.credential_type.placeholder = 'Select A Type'; + + vm.form.inputs = { + get: credentialType.getTypeFromName, + source: vm.form.credential_type, + reference: 'vm.form.inputs', + key: 'inputs' }; + + vm.form.save = credential.post; } AddCredentialsController.$inject = [ - 'credentialType' + 'resolvedModels' ]; export default AddCredentialsController; diff --git a/awx/ui/client/features/credentials/add-credentials.view.html b/awx/ui/client/features/credentials/add-credentials.view.html index bee8de01bc..10019be66a 100644 --- a/awx/ui/client/features/credentials/add-credentials.view.html +++ b/awx/ui/client/features/credentials/add-credentials.view.html @@ -7,12 +7,12 @@ - - - - + + + + - + Type Details diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index 0ccee11daf..d053bb728b 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -40,7 +40,7 @@ function config ($stateExtenderProvider, pathServiceProvider) { } }); - function credentialTypeResolve ($q, credentialModel, credentialTypeModel) { + function CredentialsAddResolve ($q, credentialModel, credentialTypeModel) { let promises = [ credentialModel.options(), credentialTypeModel.get() @@ -53,7 +53,7 @@ function config ($stateExtenderProvider, pathServiceProvider) { })); } - credentialTypeResolve.$inject = ['$q', 'CredentialModel', 'CredentialTypeModel']; + CredentialsAddResolve.$inject = ['$q', 'CredentialModel', 'CredentialTypeModel']; stateExtender.addState({ name: 'credentials.add', @@ -69,7 +69,7 @@ function config ($stateExtenderProvider, pathServiceProvider) { } }, resolve: { - credentialType: credentialTypeResolve + resolvedModels: CredentialsAddResolve } }); diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index ca2b8ab132..51040fdce4 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -1,7 +1,6 @@ @import 'action/_index'; @import 'badge/_index'; @import 'dynamic/_index'; -@import 'form/_index'; @import 'input/_index'; @import 'panel/_index'; @import 'popover/_index'; diff --git a/awx/ui/client/lib/components/dynamic/input-group.directive.js b/awx/ui/client/lib/components/dynamic/input-group.directive.js index 68703ae9e2..b993aaa52a 100644 --- a/awx/ui/client/lib/components/dynamic/input-group.directive.js +++ b/awx/ui/client/lib/components/dynamic/input-group.directive.js @@ -44,7 +44,7 @@ function AtDynamicInputGroupController ($scope, $compile) { state.value = source.value; - let inputs = state.getInputs(source.value); + let inputs = state.get(source.value); let components = vm.createComponentConfigs(inputs); vm.insert(components); @@ -70,6 +70,7 @@ function AtDynamicInputGroupController ($scope, $compile) { components.push(Object.assign({ element: vm.createElement(input, i), + key: 'inputs', dynamic: true }, input)); }); diff --git a/awx/ui/client/lib/components/form/_index.less b/awx/ui/client/lib/components/form/_index.less deleted file mode 100644 index 8b13789179..0000000000 --- a/awx/ui/client/lib/components/form/_index.less +++ /dev/null @@ -1 +0,0 @@ - diff --git a/awx/ui/client/lib/components/form/action.partial.html b/awx/ui/client/lib/components/form/action.partial.html index fd4df0881b..8affd3a414 100644 --- a/awx/ui/client/lib/components/form/action.partial.html +++ b/awx/ui/client/lib/components/form/action.partial.html @@ -1,5 +1,5 @@ diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js index 8bacc78bad..5d71d3b817 100644 --- a/awx/ui/client/lib/components/form/form.directive.js +++ b/awx/ui/client/lib/components/form/form.directive.js @@ -13,7 +13,9 @@ function AtFormController (eventService) { vm.components = []; vm.state = { - isValid: false + isValid: false, + disabled: false, + value: {} }; vm.init = (_scope_, _form_) => { @@ -27,10 +29,6 @@ function AtFormController (eventService) { component.category = category; component.form = vm.state; - if (category === 'input') { - component.state.index = vm.components.length; - } - vm.components.push(component) }; @@ -43,13 +41,12 @@ function AtFormController (eventService) { }; vm.submitOnEnter = event => { - if (event.key !== 'Enter') { + if (event.key !== 'Enter' || event.srcElement.type === 'textarea') { return; } event.preventDefault(); - - vm.submit(); + scope.$apply(vm.submit); }; vm.submit = event => { @@ -57,7 +54,58 @@ function AtFormController (eventService) { return; } - console.log('submit', event, vm.components); + vm.state.disabled = true; + + let data = vm.components + .filter(component => component.category === 'input') + .reduce((values, component) => { + if (!component.state.value) { + return values; + } + + if (component.state.dynamic) { + values[component.state.key] = values[component.state.key] || []; + values[component.state.key].push({ + [component.state.id]: component.state.value + }); + } else { + values[component.state.id] = component.state.value; + } + + return values; + }, {}); + + + scope.state.save(data) + .then(res => vm.onSaveSuccess(res)) + .catch(err => vm.onSaveError(err)) + .finally(() => vm.state.disabled = false); + }; + + vm.onSaveSuccess = res => { + console.info(res); + }; + + vm.onSaveError = err => { + if (err.status === 400) { + vm.setValidationErrors(err.data); + } + }; + + vm.setValidationErrors = errors => { + for (let id in errors) { + vm.components + .filter(component => component.category === 'input') + .forEach(component => { + if (component.state.id === id) { + component.state.rejected = true; + component.state.isValid = false; + component.state.message = errors[id].join(' '); + } + }); + } + + vm.check(); }; vm.validate = () => { @@ -86,12 +134,14 @@ function AtFormController (eventService) { }; vm.deregisterDynamicComponents = components => { - let offset = 0; - - components.forEach(component => { - vm.components.splice(component.index - offset, 1); - offset++; - }); + for (let i = 0; i < components.length; i++) { + for (let j = 0; j < vm.components.length; j++) { + if (components[i] === vm.components[j].state) { + vm.components.splice(j, 1); + break; + } + } + } }; } diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 348722a82c..4838891576 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -18,7 +18,6 @@ import toggleContent from './toggle/content.directive'; import BaseInputController from './input/base.controller'; - angular .module('at.lib.components', []) .directive('atActionGroup', actionGroup) diff --git a/awx/ui/client/lib/components/input/_index.less b/awx/ui/client/lib/components/input/_index.less index 515629a41f..468b2e929b 100644 --- a/awx/ui/client/lib/components/input/_index.less +++ b/awx/ui/client/lib/components/input/_index.less @@ -19,7 +19,10 @@ border-color: @at-blue; } -.at-InputLabel { +.at-Input--rejected { + &, &:focus { + border-color: @at-red; + } } .at-InputLabel-name { @@ -77,3 +80,10 @@ background-color: @at-white; } } + +.at-InputMessage--rejected { + font-size: @at-font-size; + color: @at-red; + margin: @at-space-3x 0 0 0; + padding: 0; +} diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 49b771b04a..814dfc54bf 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -1,3 +1,6 @@ +const REQUIRED_INPUT_MISSING_MESSAGE = 'Please enter a value.'; +const DEFAULT_INVALID_INPUT_MESSAGE = 'Invalid input for this type.'; + function BaseInputController () { return function extend (type, scope, element, form) { let vm = this; @@ -12,23 +15,36 @@ function BaseInputController () { vm.validate = () => { let isValid = true; + let message = ''; if (scope.state.required && !scope.state.value) { isValid = false; + message = REQUIRED_INPUT_MISSING_MESSAGE; } - if (scope.state.validate && !scope.state.validate(scope.state.value)) { - isValid = false; + if (scope.state.validate) { + let result = scope.state.validate(scope.state.value); + + if (!result.isValid) { + isValid = false; + message = result.message || DEFAULT_INVALID_INPUT_MESSAGE; + } } - return isValid; + return { + isValid, + message + }; }; vm.check = () => { - let isValid = vm.validate(); + let result = vm.validate(); + + if (result.isValid !== scope.state.isValid) { + scope.state.rejected = !result.isValid; + scope.state.isValid = result.isValid; + scope.state.message = result.message; - if (isValid !== scope.state.isValid) { - scope.state.isValid = isValid; form.check(); } }; diff --git a/awx/ui/client/lib/components/input/secret.partial.html b/awx/ui/client/lib/components/input/secret.partial.html index 1901f88e10..aef719c60f 100644 --- a/awx/ui/client/lib/components/input/secret.partial.html +++ b/awx/ui/client/lib/components/input/secret.partial.html @@ -3,15 +3,21 @@
- + ng-change="vm.check()" + ng-disabled="state.disabled || form.disabled" />
+

+ {{ state.message }} +

diff --git a/awx/ui/client/lib/components/input/select.partial.html b/awx/ui/client/lib/components/input/select.partial.html index 43853eeb31..dd7b5e2c13 100644 --- a/awx/ui/client/lib/components/input/select.partial.html +++ b/awx/ui/client/lib/components/input/select.partial.html @@ -4,13 +4,16 @@
+

+ {{ state.message }} +

diff --git a/awx/ui/client/lib/components/input/text.partial.html b/awx/ui/client/lib/components/input/text.partial.html index f9441e6ecc..7e1354d3a4 100644 --- a/awx/ui/client/lib/components/input/text.partial.html +++ b/awx/ui/client/lib/components/input/text.partial.html @@ -1,10 +1,17 @@
- + ng-change="vm.check()" + ng-disabled="state.disabled || form.disabled" /> + +

+ {{ state.message }} +

diff --git a/awx/ui/client/lib/components/input/textarea.partial.html b/awx/ui/client/lib/components/input/textarea.partial.html index d535dc876d..ff58753341 100644 --- a/awx/ui/client/lib/components/input/textarea.partial.html +++ b/awx/ui/client/lib/components/input/textarea.partial.html @@ -2,10 +2,15 @@
+

+ {{ state.message }} +

diff --git a/awx/ui/client/lib/components/panel/panel.directive.js b/awx/ui/client/lib/components/panel/panel.directive.js index 2fe356ff55..db5212ce3c 100644 --- a/awx/ui/client/lib/components/panel/panel.directive.js +++ b/awx/ui/client/lib/components/panel/panel.directive.js @@ -1,16 +1,13 @@ -function use (scope) { - scope.dismiss = this.dismiss; -} - -function dismiss ($state) { - $state.go('^'); -} - function AtPanelController ($state) { let vm = this; - vm.dismiss = dismiss.bind(vm, $state); - vm.use = use; + vm.dismiss = () => { + $state.go('^'); + }; + + vm.use = scope => { + scope.dismiss = this.dismiss; + }; } AtPanelController.$inject = ['$state']; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 923b11c59b..7448f46dda 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -1,49 +1,71 @@ -let $resource; - -function get() { - return $resource(this.path).get().$promise - .then(response => { - this.model.data = response; - }); -} - -function options () { - let actions = { - options: { - method: 'OPTIONS' - } - }; - - return $resource(this.path, null, actions).options().$promise - .then(response => { - this.model.options = response; - }); -} - -function getPostOptions (name) { - return this.model.options.actions.POST[name]; -} - -function normalizePath (resource) { - let version = '/api/v2/'; - - return `${version}${resource}/`; -} - -function BaseModel (_$resource_) { - $resource = _$resource_; - +function BaseModel ($http) { return function extend (path) { - this.get = get; - this.options = options; - this.getPostOptions = getPostOptions; - this.normalizePath = normalizePath; + this.get = () => { + let request = { + method: 'GET', + url: this.path + }; + + return $http(request) + .then(response => { + this.model.get = response; + }); + }; + + this.post = data => { + let request = { + method: 'POST', + url: this.path, + data, + }; + + return $http(request) + .then(response => { + this.model.post = response; + }); + }; + + this.options = () => { + let request = { + method: 'OPTIONS', + url: this.path + }; + + return $http(request) + .then(response => { + this.model.options = response; + }); + }; + + this.getOptions = (method, key) => { + if (!method) { + return this.model.options.data; + } + + method = method.toUpperCase(); + + if (method && !key) { + return this.model.options.data.actions[method]; + } + + if (method && key) { + return this.model.options.data.actions[method][key]; + } + + return null; + }; + + this.normalizePath = resource => { + let version = '/api/v2/'; + + return `${version}${resource}/`; + }; this.model = {}; this.path = this.normalizePath(path); }; } -BaseModel.$inject = ['$resource']; +BaseModel.$inject = ['$http']; export default BaseModel; diff --git a/awx/ui/client/lib/models/Credential.js b/awx/ui/client/lib/models/Credential.js index ae8c42d9eb..14d33b42ed 100644 --- a/awx/ui/client/lib/models/Credential.js +++ b/awx/ui/client/lib/models/Credential.js @@ -1,7 +1,23 @@ -function CredentialModel (BaseModel) { +function CredentialModel (BaseModel, CredentialTypeModel) { BaseModel.call(this, 'credentials'); + + this.createFormSchema = (type, config) => { + let schema = Object.assign({}, this.getOptions(type)); + + if (config && config.omit) { + config.omit.forEach(key => { + delete schema[key]; + }); + } + + for (let key in schema) { + schema[key].id = key; + } + + return schema; + }; } -CredentialModel.$inject = ['BaseModel']; +CredentialModel.$inject = ['BaseModel', 'CredentialTypeModel']; export default CredentialModel; diff --git a/awx/ui/client/lib/models/CredentialType.js b/awx/ui/client/lib/models/CredentialType.js index 1fa4960135..eee40a5514 100644 --- a/awx/ui/client/lib/models/CredentialType.js +++ b/awx/ui/client/lib/models/CredentialType.js @@ -4,7 +4,7 @@ function CredentialTypeModel (BaseModel) { this.categorizeByKind = () => { let group = {}; - this.model.data.results.forEach(result => { + this.model.get.data.results.forEach(result => { group[result.kind] = group[result.kind] || []; group[result.kind].push(result); }); @@ -16,7 +16,7 @@ function CredentialTypeModel (BaseModel) { }; this.getTypeFromName = name => { - let type = this.model.data.results.filter(result => result.name === name); + let type = this.model.get.data.results.filter(result => result.name === name); if (!type.length) { return null; @@ -36,6 +36,10 @@ function CredentialTypeModel (BaseModel) { return field; }); }; + + this.getResults = () => { + return this.model.get.data.results; + }; } CredentialTypeModel.$inject = ['BaseModel']; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 287aadb836..0c92326abb 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -2,15 +2,8 @@ import Base from './Base'; import Credential from './Credential'; import CredentialType from './CredentialType'; -function config ($resourceProvider) { - $resourceProvider.defaults.stripTrailingSlashes = false; -} - -config.$inject = ['$resourceProvider']; - angular .module('at.lib.models', []) - .config(config) .service('BaseModel', Base) .service('CredentialModel', Credential) .service('CredentialTypeModel', CredentialType); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index c608606503..06530ba096 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -136,7 +136,6 @@ var tower = angular.module('Tower', [ 'AWDirectives', 'features', - 'ngResource', 'at.lib.components', 'at.lib.models', 'at.lib.services',