Merge pull request #94 from gconsidine/ui/fix/credentials-lookup

Ui/fix/credentials lookup
This commit is contained in:
Greg Considine
2017-07-28 15:36:37 -04:00
committed by GitHub
15 changed files with 319 additions and 107 deletions

View File

@@ -1,3 +1,3 @@
.at-CredentialsPermissions { .at-CredentialsPermissions {
margin-top: 20px; margin-top: 50px;
} }

View File

@@ -19,6 +19,8 @@ function AddCredentialsController (models, $state, strings) {
omit: ['user', 'team', 'inputs'] omit: ['user', 'team', 'inputs']
}); });
vm.form.disabled = !credential.isCreatable();
vm.form.organization._resource = 'organization'; vm.form.organization._resource = 'organization';
vm.form.organization._route = 'credentials.add.organization'; vm.form.organization._route = 'credentials.add.organization';
vm.form.organization._model = organization; vm.form.organization._model = organization;
@@ -31,9 +33,9 @@ function AddCredentialsController (models, $state, strings) {
vm.form.inputs = { vm.form.inputs = {
_get: id => { _get: id => {
let type = credentialType.getById(id); credentialType.mergeInputProperties();
return credentialType.mergeInputProperties(type); return credentialType.get('inputs.fields');
}, },
_source: vm.form.credential_type, _source: vm.form.credential_type,
_reference: 'vm.form.inputs', _reference: 'vm.form.inputs',

View File

@@ -5,7 +5,6 @@ function EditCredentialsController (models, $state, $scope, strings) {
let credential = models.credential; let credential = models.credential;
let credentialType = models.credentialType; let credentialType = models.credentialType;
let organization = models.organization; let organization = models.organization;
let selectedCredentialType = models.selectedCredentialType;
vm.mode = 'edit'; vm.mode = 'edit';
vm.strings = strings; vm.strings = strings;
@@ -36,10 +35,12 @@ function EditCredentialsController (models, $state, $scope, strings) {
// Only exists for permissions compatibility // Only exists for permissions compatibility
$scope.credential_obj = credential.get(); $scope.credential_obj = credential.get();
vm.form = credential.createFormSchema('put', { vm.form = credential.createFormSchema({
omit: ['user', 'team', 'inputs'] omit: ['user', 'team', 'inputs']
}); });
vm.form.disabled = !credential.isEditable();
vm.form.organization._resource = 'organization'; vm.form.organization._resource = 'organization';
vm.form.organization._model = organization; vm.form.organization._model = organization;
vm.form.organization._route = 'credentials.edit.organization'; vm.form.organization._route = 'credentials.edit.organization';
@@ -50,30 +51,34 @@ function EditCredentialsController (models, $state, $scope, strings) {
vm.form.credential_type._resource = 'credential_type'; vm.form.credential_type._resource = 'credential_type';
vm.form.credential_type._model = credentialType; vm.form.credential_type._model = credentialType;
vm.form.credential_type._route = 'credentials.edit.credentialType'; vm.form.credential_type._route = 'credentials.edit.credentialType';
vm.form.credential_type._value = selectedCredentialType.get('id'); vm.form.credential_type._value = credentialType.get('id');
vm.form.credential_type._displayValue = selectedCredentialType.get('name'); vm.form.credential_type._displayValue = credentialType.get('name');
vm.form.credential_type._placeholder = strings.get('inputs.CREDENTIAL_TYPE_PLACEHOLDER'); vm.form.credential_type._placeholder = strings.get('inputs.CREDENTIAL_TYPE_PLACEHOLDER');
vm.form.inputs = { vm.form.inputs = {
_get (id) { _get (id) {
let type = credentialType.getById(id); credentialType.mergeInputProperties();
let inputs = credentialType.mergeInputProperties(type);
if (type.id === credential.get('credential_type')) { if (credentialType.get('id') === credential.get('credential_type')) {
inputs = credential.assignInputGroupValues(inputs); return credential.assignInputGroupValues(credentialType.get('inputs.fields'));
} }
return inputs; return credentialType.get('inputs.fields');
}, },
_source: vm.form.credential_type, _source: vm.form.credential_type,
_reference: 'vm.form.inputs', _reference: 'vm.form.inputs',
_key: 'inputs' _key: 'inputs'
}; };
/**
* If a credential's `credential_type` is changed while editing, the inputs associated with
* the old type need to be cleared before saving the inputs associated with the new type.
* Otherwise inputs are merged together making the request invalid.
*/
vm.form.save = data => { vm.form.save = data => {
data.user = me.getSelf().id; data.user = me.getSelf().id;
credential.clearTypeInputs(); credential.unset('inputs');
return credential.request('put', data); return credential.request('put', data);
}; };

View File

@@ -5,16 +5,15 @@ import CredentialsStrings from './credentials.strings'
function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) { function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) {
let id = $stateParams.credential_id; let id = $stateParams.credential_id;
let models;
let promises = { let promises = {
me: new Me('get'), me: new Me('get')
credentialType: new CredentialType('get'),
organization: new Organization('get')
}; };
if (!id) { if (!id) {
promises.credential = new Credential('options'); promises.credential = new Credential('options');
promises.credentialType = new CredentialType();
promises.organization = new Organization();
return $q.all(promises) return $q.all(promises)
} }
@@ -22,16 +21,22 @@ function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, O
promises.credential = new Credential(['get', 'options'], [id, id]); promises.credential = new Credential(['get', 'options'], [id, id]);
return $q.all(promises) return $q.all(promises)
.then(_models_ => { .then(models => {
models = _models_; let typeId = models.credential.get('credential_type');
let credentialTypeId = models.credential.get('credential_type'); let orgId = models.credential.get('organization');
return models.credentialType.graft(credentialTypeId); let dependents = {
}) credentialType: new CredentialType('get', typeId),
.then(selectedCredentialType => { organization: new Organization('get', orgId)
models.selectedCredentialType = selectedCredentialType; };
return models; return $q.all(dependents)
.then(related => {
models.credentialType = related.credentialType;
models.organization = related.organization;
return models;
});
}); });
} }

View File

@@ -1,5 +1,5 @@
<button class="btn at-Button{{ fill }}--{{ color }}" <button class="btn at-Button{{ fill }}--{{ color }}"
ng-disabled="form.disabled || (type === 'save' && !form.isValid)" ng-disabled="type !== 'cancel' && (form.disabled || (type === 'save' && !form.isValid))"
ng-click="action()"> ng-click="action()">
{{::text}} {{::text}}
</button> </button>

View File

@@ -27,6 +27,8 @@ function AtFormController (eventService, strings) {
form = _form_; form = _form_;
modal = scope[scope.ns].modal; modal = scope[scope.ns].modal;
vm.state.disabled = scope.state.disabled;
vm.setListeners(); vm.setListeners();
}; };

View File

@@ -67,16 +67,22 @@ function BaseInputController (strings) {
}; };
}; };
vm.check = () => { vm.updateValidationState = result => {
let result = vm.validate(); if (!scope.state._touched && scope.state._required) {
return;
if (scope.state._touched || !scope.state._required) {
scope.state._rejected = !result.isValid;
scope.state._isValid = result.isValid;
scope.state._message = result.message;
form.check();
} }
scope.state._rejected = !result.isValid;
scope.state._isValid = result.isValid;
scope.state._message = result.message;
form.check();
};
vm.check = result => {
result = result || vm.validate();
vm.updateValidationState(result);
}; };
vm.toggleRevertReplace = () => { vm.toggleRevertReplace = () => {

View File

@@ -1,3 +1,6 @@
const DEFAULT_DEBOUNCE = 250;
const DEFAULT_KEY = 'name';
function atInputLookupLink (scope, element, attrs, controllers) { function atInputLookupLink (scope, element, attrs, controllers) {
let formController = controllers[0]; let formController = controllers[0];
let inputController = controllers[1]; let inputController = controllers[1];
@@ -9,28 +12,40 @@ function atInputLookupLink (scope, element, attrs, controllers) {
inputController.init(scope, element, formController); inputController.init(scope, element, formController);
} }
function AtInputLookupController (baseInputController, $state, $stateParams) { function AtInputLookupController (baseInputController, $q, $state, $stateParams) {
let vm = this || {}; let vm = this || {};
let scope; let scope;
let model;
let search;
vm.init = (_scope_, element, form) => { vm.init = (_scope_, element, form) => {
baseInputController.call(vm, 'input', _scope_, element, form); baseInputController.call(vm, 'input', _scope_, element, form);
scope = _scope_; scope = _scope_;
model = scope.state._model;
scope.state._debounce = scope.state._debounce || DEFAULT_DEBOUNCE;
search = scope.state._search || {
key: DEFAULT_KEY,
config: {
unique: true
}
};
scope.$watch(scope.state._resource, vm.watchResource); scope.$watch(scope.state._resource, vm.watchResource);
scope.state._validate = vm.checkOnInput;
vm.check(); vm.check();
}; };
vm.watchResource = () => { vm.watchResource = () => {
if (!scope[scope.state._resource]) {
return;
}
if (scope[scope.state._resource] !== scope.state._value) { if (scope[scope.state._resource] !== scope.state._value) {
scope.state._value = scope[scope.state._resource];
scope.state._displayValue = scope[`${scope.state._resource}_name`]; scope.state._displayValue = scope[`${scope.state._resource}_name`];
vm.check(); vm.search();
} }
}; };
@@ -49,32 +64,58 @@ function AtInputLookupController (baseInputController, $state, $stateParams) {
scope[scope.state._resource] = undefined; scope[scope.state._resource] = undefined;
}; };
vm.checkOnInput = () => { vm.searchAfterDebounce = () => {
if (!scope.state._touched) { vm.isDebouncing = true;
return { isValid: true };
vm.debounce = window.setTimeout(() => {
vm.isDebouncing = false;
vm.search();
}, scope.state._debounce);
};
vm.resetDebounce = () => {
clearTimeout(vm.debounce);
vm.searchAfterDebounce();
};
vm.search = () => {
scope.state._touched = true;
if (scope.state._displayValue === '' && !scope.state._required) {
return vm.check({ isValid: true });
} }
let result = scope.state._model.match('get', 'name', scope.state._displayValue); return model.search({ [search.key]: scope.state._displayValue }, search.config)
.then(found => {
if (!found) {
return vm.reset();
}
if (result) { scope[scope.state._resource] = model.get('id');
scope[scope.state._resource] = result.id; scope.state._value = model.get('id');
scope.state._value = result.id; scope.state._displayValue = model.get('name');
scope.state._displayValue = result.name; })
.catch(() => vm.reset())
.finally(() => {
let isValid = scope.state._value !== undefined;
let message = isValid ? '' : vm.strings.get('lookup.NOT_FOUND');
return { isValid: true }; vm.check({ isValid, message });
});
};
vm.searchOnInput = () => {
if (vm.isDebouncing) {
return vm.resetDebounce();
} }
vm.reset(); vm.searchAfterDebounce();
return {
isValid: false,
message: vm.strings.get('lookup.NOT_FOUND')
};
}; };
} }
AtInputLookupController.$inject = [ AtInputLookupController.$inject = [
'BaseInputController', 'BaseInputController',
'$q',
'$state', '$state',
'$stateParams' '$stateParams'
]; ];

View File

@@ -16,7 +16,7 @@
ng-model="state._displayValue" ng-model="state._displayValue"
ng-attr-tabindex="{{ tab || undefined }}" ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}" ng-attr-placeholder="{{::state._placeholder || undefined }}"
ng-change="vm.check()" ng-change="vm.searchOnInput()"
ng-disabled="state._disabled || form.disabled" /> ng-disabled="state._disabled || form.disabled" />
</div> </div>

View File

@@ -11,16 +11,54 @@ function request (method, resource) {
return this.http[method](resource); return this.http[method](resource);
} }
function httpGet (resource) { /**
this.method = this.method || 'GET'; * Intended to be useful in searching and filtering results using params
* supported by the API.
*
* @param {object} params - An object of keys and values to to format and
* to the URL as a query string. Refer to the API documentation for the
* resource in use for specifics.
* @param {object} config - Configuration specific to the UI to accommodate
* common use cases.
*
* @returns {Promise} - $http
* @yields {(Boolean|object)}
*/
function search (params, config) {
let req = { let req = {
method: this.method, method: 'GET',
url: this.path,
params
};
return $http(req)
.then(({ data }) => {
if (!data.count) {
return false;
}
if (config.unique) {
if (data.count !== 1) {
return false;
}
this.model.GET = data.results[0];
} else {
this.model.GET = data;
}
return true;
});
}
function httpGet (resource) {
let req = {
method: 'GET',
url: this.path url: this.path
}; };
if (typeof resource === 'object') { if (typeof resource === 'object') {
this.model[this.method] = resource; this.model.GET = resource;
return $q.resolve(); return $q.resolve();
} else if (resource) { } else if (resource) {
@@ -43,9 +81,9 @@ function httpPost (data) {
}; };
return $http(req).then(res => { return $http(req).then(res => {
this.model.GET = res.data; this.model.GET = res.data;
return res; return res;
}); });
} }
@@ -87,7 +125,56 @@ function get (keys) {
return this.find('get', keys); return this.find('get', keys);
} }
function unset (method, keys) {
if (!keys) {
keys = method;
method = 'GET';
}
method = method.toUpperCase();
keys = keys.split('.');
if (!keys.length) {
delete this.model[method];
} else if (keys.length === 1) {
delete this.model[method][keys[0]];
} else {
let property = keys.splice(-1);
keys = keys.join('.');
let model = this.find(method, keys)
delete model[property];
}
}
function set (method, keys, value) {
if (!value) {
value = keys;
keys = method;
method = 'GET';
}
keys = keys.split('.');
if (keys.length === 1) {
model[keys[0]] = value;
} else {
let property = keys.splice(-1);
keys = keys.join('.');
let model = this.find(method, keys)
model[property] = value;
}
}
function match (method, key, value) { function match (method, key, value) {
if(!value) {
value = key;
key = method;
method = 'GET';
}
let model = this.model[method.toUpperCase()]; let model = this.model[method.toUpperCase()];
if (!model) { if (!model) {
@@ -143,32 +230,104 @@ function find (method, keys) {
return value; return value;
} }
function has (method, keys) {
if (!keys) {
keys = method;
method = 'GET';
}
method = method.toUpperCase();
let value;
switch (method) {
case 'OPTIONS':
value = this.options(keys);
break;
default:
value = this.get(keys);
}
return value !== undefined && value !== null;
}
function normalizePath (resource) { function normalizePath (resource) {
let version = '/api/v2/'; let version = '/api/v2/';
return `${version}${resource}/`; return `${version}${resource}/`;
} }
function getById (id) { function isEditable () {
let canEdit = this.get('summary_fields.user_capabilities.edit');
if (canEdit) {
return true;
}
if (this.has('options', 'actions.PUT')) {
return true;
}
return false;
}
function isCreatable () {
if (this.has('options', 'actions.POST')) {
return true;
}
return false;
}
function graft (id) {
let item = this.get('results').filter(result => result.id === id); let item = this.get('results').filter(result => result.id === id);
return item ? item[0] : undefined; item = item ? item[0] : undefined;
if (!item) {
return undefined;
}
return new this.Constructor('get', item, true);
}
function create (method, resource, graft) {
if (!method) {
return this;
}
this.promise = this.request(method, resource);
if (graft) {
return this;
}
return this.promise
.then(() => this);
} }
function BaseModel (path) { function BaseModel (path) {
this.model = {}; this.create = create;
this.get = get;
this.options = options;
this.find = find; this.find = find;
this.get = get;
this.graft = graft;
this.has = has;
this.isEditable = isEditable;
this.isCreatable = isCreatable;
this.match = match; this.match = match;
this.model = {};
this.normalizePath = normalizePath; this.normalizePath = normalizePath;
this.getById = getById; this.options = options;
this.request = request; this.request = request;
this.search = search;
this.set = set;
this.unset = unset;
this.http = { this.http = {
get: httpGet.bind(this), get: httpGet.bind(this),
options: httpOptions.bind(this), options: httpOptions.bind(this),
post: httpPost.bind(this), post: httpPost.bind(this),
put: httpPut.bind(this) put: httpPut.bind(this),
}; };
this.path = this.normalizePath(path); this.path = this.normalizePath(path);

View File

@@ -3,18 +3,21 @@ const ENCRYPTED_VALUE = '$encrypted$';
let BaseModel; let BaseModel;
function createFormSchema (method, config) { function createFormSchema (method, config) {
if (!config) {
config = method;
method = 'GET';
}
let schema = Object.assign({}, this.options(`actions.${method.toUpperCase()}`)); let schema = Object.assign({}, this.options(`actions.${method.toUpperCase()}`));
if (config && config.omit) { if (config && config.omit) {
config.omit.forEach(key => { config.omit.forEach(key => delete schema[key]);
delete schema[key];
});
} }
for (let key in schema) { for (let key in schema) {
schema[key].id = key; schema[key].id = key;
if (method === 'put') { if (this.has(key)) {
schema[key]._value = this.get(key); schema[key]._value = this.get(key);
} }
} }
@@ -33,19 +36,14 @@ function assignInputGroupValues (inputs) {
}); });
} }
function clearTypeInputs () { function CredentialModel (method, resource, graft) {
delete this.model.GET.inputs;
}
function CredentialModel (method, resource) {
BaseModel.call(this, 'credentials'); BaseModel.call(this, 'credentials');
this.Constructor = CredentialModel;
this.createFormSchema = createFormSchema.bind(this); this.createFormSchema = createFormSchema.bind(this);
this.assignInputGroupValues = assignInputGroupValues.bind(this); this.assignInputGroupValues = assignInputGroupValues.bind(this);
this.clearTypeInputs = clearTypeInputs.bind(this);
return this.request(method, resource) return this.create(method, resource, graft);
.then(() => this);
} }
function CredentialModelLoader (_BaseModel_ ) { function CredentialModelLoader (_BaseModel_ ) {

View File

@@ -14,33 +14,26 @@ function categorizeByKind () {
})); }));
} }
function mergeInputProperties (type) { function mergeInputProperties () {
return type.inputs.fields.map(field => { let required = this.get('inputs.required');
if (!type.inputs.required || type.inputs.required.indexOf(field.id) === -1) {
field.required = false;
} else {
field.required = true;
}
return field; return this.get('inputs.fields').map((field, i) => {
if (!required || required.indexOf(field.id) === -1) {
this.set(`inputs.fields[${i}].required`, false);
} else {
this.set(`inputs.fields[${i}].required`, true);
}
}); });
} }
function graft (id) { function CredentialTypeModel (method, resource, graft) {
let data = this.getById(id);
return new CredentialTypeModel('get', data);
}
function CredentialTypeModel (method, id) {
BaseModel.call(this, 'credential_types'); BaseModel.call(this, 'credential_types');
this.Constructor = CredentialTypeModel;
this.categorizeByKind = categorizeByKind.bind(this); this.categorizeByKind = categorizeByKind.bind(this);
this.mergeInputProperties = mergeInputProperties.bind(this); this.mergeInputProperties = mergeInputProperties.bind(this);
this.graft = graft.bind(this);
return this.request(method, id) return this.create(method, resource, graft);
.then(() => this);
} }
function CredentialTypeModelLoader (_BaseModel_) { function CredentialTypeModelLoader (_BaseModel_) {

View File

@@ -4,13 +4,13 @@ function getSelf () {
return this.get('results[0]'); return this.get('results[0]');
} }
function MeModel (method) { function MeModel (method, resource, graft) {
BaseModel.call(this, 'me'); BaseModel.call(this, 'me');
this.Constructor = MeModel;
this.getSelf = getSelf.bind(this); this.getSelf = getSelf.bind(this);
return this.request(method) return this.create(method, resource, graft);
.then(() => this);
} }
function MeModelLoader (_BaseModel_) { function MeModelLoader (_BaseModel_) {

View File

@@ -1,10 +1,11 @@
let BaseModel; let BaseModel;
function OrganizationModel (method) { function OrganizationModel (method, resource, graft) {
BaseModel.call(this, 'organizations'); BaseModel.call(this, 'organizations');
return this.request(method) this.Constructor = OrganizationModel;
.then(() => this);
return this.create(method, resource, graft);
} }
function OrganizationModelLoader (_BaseModel_) { function OrganizationModelLoader (_BaseModel_) {

View File

@@ -46,7 +46,7 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
} }
$scope[list.name].forEach(credential => { $scope[list.name].forEach(credential => {
credential.kind = credentialType.getById(credential.credential_type).name; credential.kind = credentialType.match('id', credential.credential_type).name;
}); });
} }