mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Add form submission, validation, rejection messaging
This commit is contained in:
parent
c41dff7996
commit
19fa782fb4
@ -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;
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
</at-tab-navigation>
|
||||
|
||||
<at-panel-body>
|
||||
<at-form>
|
||||
<at-input-text col="4" tab="1" state="vm.name"></at-input-text>
|
||||
<at-input-text col="4" tab="2" state="vm.description"></at-input-text>
|
||||
<at-input-select col="4" tab="3" state="vm.kind"></at-input-select>
|
||||
<at-form state="vm.form">
|
||||
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
|
||||
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
|
||||
<at-input-select col="4" tab="3" state="vm.form.credential_type"></at-input-select>
|
||||
|
||||
<at-dynamic-input-group col="4" tab="4" state="vm.dynamic">
|
||||
<at-dynamic-input-group col="4" tab="4" state="vm.form.inputs">
|
||||
Type Details
|
||||
</at-dynamic-input-group>
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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));
|
||||
});
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<button class="btn at-Button{{ fill }}--{{ color }}"
|
||||
ng-disabled="type !== 'cancel' && !form.isValid"
|
||||
ng-class="{ 'at-Button--disabled': !form.isValid }" ng-click="action()">
|
||||
ng-disabled="form.disabled || (type === 'save' && !form.isValid)"
|
||||
ng-click="action()">
|
||||
{{::text}}
|
||||
</button>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ import toggleContent from './toggle/content.directive';
|
||||
|
||||
import BaseInputController from './input/base.controller';
|
||||
|
||||
|
||||
angular
|
||||
.module('at.lib.components', [])
|
||||
.directive('atActionGroup', actionGroup)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,15 +3,21 @@
|
||||
<at-input-label state="state"></at-input-label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn at-ButtonHollow--white at-Input-button" ng-click="vm.toggle()">
|
||||
<button class="btn at-ButtonHollow--white at-Input-button"
|
||||
ng-disabled="state.disabled || form.disabled" ng-click="vm.toggle()">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</span>
|
||||
<input type="{{ type }}" class="form-control at-Input" ng-model="state.value"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state.placeholder || undefined }}"
|
||||
ng-change="vm.check()" ng-disabled="state.disabled" />
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state.disabled || form.disabled" />
|
||||
</div>
|
||||
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
|
||||
{{ state.message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
<div class="at-InputGroup at-InputSelect">
|
||||
<input type="text" class="form-control at-Input at-InputSelect-input"
|
||||
placeholder="{{ state.placeholder | uppercase }}"
|
||||
ng-model="state.value" ng-disabled="state.disabled"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-model="state.value"
|
||||
ng-disabled="state.disabled || form.disabled"
|
||||
ng-change="vm.check()" />
|
||||
|
||||
<select class="form-control at-InputSelect-select" ng-model="state.value"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-disabled="state.disabled">
|
||||
<optgroup ng-repeat="group in state.data" label="{{::group.category | uppercase }}">
|
||||
ng-disabled="state.disabled || form.disabled">
|
||||
<optgroup ng-repeat="group in state.data"
|
||||
label="{{::group.category | uppercase }}">
|
||||
<option ng-repeat="item in group.data" value="{{ item.name }}">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
@ -18,5 +21,8 @@
|
||||
</select>
|
||||
<i class="fa" ng-class="{ 'fa-chevron-down': !open, 'fa-chevron-up': open }"></i>
|
||||
</div>
|
||||
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
|
||||
{{ state.message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
<div class="col-sm-{{::col}}">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label state="state"></at-input-label>
|
||||
<input type="text" class="form-control at-Input" ng-model="state.value"
|
||||
<input type="text" class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-model="state.value"
|
||||
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state.placeholder || undefined }}"
|
||||
ng-change="vm.check()" ng-disabled="state.disabled" />
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state.disabled || form.disabled" />
|
||||
|
||||
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
|
||||
{{ state.message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,10 +2,15 @@
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label state="state"></at-input-label>
|
||||
<textarea class="form-control at-Input" ng-model="state.value"
|
||||
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state.placeholder || undefined }}"
|
||||
ng-change="vm.check()" ng-disabled="state.disabled" />
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state.placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state.disabled || form.disabled" />
|
||||
</textarea>
|
||||
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
|
||||
{{ state.message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -136,7 +136,6 @@ var tower = angular.module('Tower', [
|
||||
'AWDirectives',
|
||||
'features',
|
||||
|
||||
'ngResource',
|
||||
'at.lib.components',
|
||||
'at.lib.models',
|
||||
'at.lib.services',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user