diff --git a/app/assets/app/cobalt/Form.component.js b/app/assets/app/cobalt/Form.component.js new file mode 100644 index 0000000..1cee255 --- /dev/null +++ b/app/assets/app/cobalt/Form.component.js @@ -0,0 +1,198 @@ +import { Component } from '../../lib/vues6/vues6.js' +import { utility } from '../service/Utility.service.js' +import { location_service } from '../service/Location.service.js' +import { resource_service } from '../service/Resource.service.js' +import { action_service } from '../service/Action.service.js' + +const template = ` +
+

+ {{ title }} +

+
+
+ + + + {{ field.error }} + + + + + {{ field.error }} + +
+
+
+ {{ error_message }} + {{ other_message }} + +
+
+` + +const title_map = { + insert: 'Create', + update: 'Edit', + view: 'View', +} + +export default class FormComponent extends Component { + static get selector() { + return 'cobalt-form' + } + + static get template() { + return template + } + + static get props() { + return ['resource', 'form_id', 'initial_mode'] + } + + definition = {} + data = {} + uuid = '' + title = '' + error_message = '' + other_message = '' + + is_ready = false + mode = '' + id = '' + + reset() { + this.definition = {} + this.data = {} + this.uuid = '' + this.title = '' + this.error_message = '' + this.other_message = '' + this.is_ready = false + } + + async vue_on_create(internal = false) { + if ( !internal ) { + this.mode = this.initial_mode + this.id = this.form_id + this.resource_class = await resource_service.get(this.resource) + } else { + this.reset() + } + + this.uuid = utility.uuid() + await this.load() + await this.init() + } + + async init() { + for ( const field of this.definition.fields ) { + if ( field.type.startsWith('select.dynamic') ) { + field._options = field._options || field.options + const rsc = await resource_service.get(field._options.resource) + + field.options = (await rsc.list()).map(item => { + return { + display: typeof field._options.display === 'function' ? field._options.display(item) : item[field._options.display || 'display'], + value: typeof field._options.value === 'function' ? field._options.value(item) : item[field._options.value || 'display'], + } + }) + } + + if ( field.type.endsWith('.multiple') ) { + this.data[field.field] = [] + } + } + + this.is_ready = true + this.$nextTick(() => { + if ( this.mode !== 'view' ) this.$refs.input[0].focus() + }) + } + + async load() { + this.definition = this.resource_class.form_definition + if (this.mode !== 'insert') { + this.data = await this.resource_class.get(this.id) + } + + this.title = title_map[this.mode] + ' ' + this.resource_class.item + } + + async on_create() { + this.id = this.data.id + this.mode = 'update' + await this.vue_on_create(true) + location_service.set_query(`mode=update&id=${this.id}`) + } + + async save_click() { + if ( !this.validate() ) return + try { + if (this.mode === 'insert') { + this.data = await this.resource_class.create(this.data) + await this.on_create() + } else if (this.mode === 'update') { + await this.resource_class.update(this.id, this.data) + } + + this.error_message = '' + this.other_message = `The ${this.resource_class.item} was saved.` + } catch (e) { + if ( e.response && e.response.data && e.response.data.message ) + this.error_message = e.response.data.message + else this.error_message = 'An unknown error occurred while saving the form.' + } + } + + validate() { + let valid = true + for ( const field of this.definition.fields ) { + if ( field.required && (!(field.field in this.data) || !this.data[field.field]) ) { + field.error = 'This field is required.' + valid = false + } else { + field.error = '' + } + } + + this.$forceUpdate() + + return valid + } + + back() { + return action_service.perform({ + text: '', + type: 'resource', + resource: this.resource, + action: 'list', + }) + } + + do_nothing() {} +} + diff --git a/app/assets/app/cobalt/Listing.component.js b/app/assets/app/cobalt/Listing.component.js index b897b50..460ebc9 100644 --- a/app/assets/app/cobalt/Listing.component.js +++ b/app/assets/app/cobalt/Listing.component.js @@ -1,21 +1,45 @@ import { Component } from '../../lib/vues6/vues6.js' +import { action_service } from '../service/Action.service.js' +import { message_service } from '../service/Message.service.js' const template = `
-

{{ definition.title }}

+
+

{{ resource_class.plural }}

+
+ +
+
+ - + + @@ -26,5 +50,55 @@ const template = ` export default class ListingComponent extends Component { static get selector() { return 'cobalt-listing' } static get template() { return template } - static get props() { return ['definition'] } + static get props() { return ['resource'] } + + definition = {} + data = [] + resource_class = {} + + async vue_on_create() { + // Load the resource + const resource_mod = await import(`../resource/${this.resource}.resource.js`) + if ( !resource_mod ) + throw new Error('Unable to load Cobalt listing resource.') + + const rsc_name = this.resource.toLowerCase().replace(/\//g, '_') + if ( !resource_mod[rsc_name] ) + throw new Error('Unable to extract resource object from module.') + + this.resource_class = resource_mod[rsc_name] + await this.load() + } + + async load() { + this.definition = this.resource_class.listing_definition + this.data = await this.resource_class.list() + } + + async perform($event, action, row = undefined) { + if ( action.confirm ) { + message_service.modal({ + title: 'Are you sure?', + message: `You are about to ${action.action}${row ? ' this '+this.resource_class.item : ''}. Do you want to continue?`, + buttons: [ + { + text: 'Cancel', + type: 'close', + }, + { + text: 'Continue', + class: ['btn', 'btn-primary'], + type: 'close', + on_click: async () => { + await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})}) + await this.load() + }, + }, + ], + }) + } else { + await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})}) + await this.load() + } + } } diff --git a/app/assets/app/dash-components.js b/app/assets/app/dash-components.js index e4cfa64..b670a57 100644 --- a/app/assets/app/dash-components.js +++ b/app/assets/app/dash-components.js @@ -5,6 +5,7 @@ import EditProfileComponent from './dash/profile/EditProfile.component.js' import AppPasswordFormComponent from './dash/profile/form/AppPassword.component.js' import ListingComponent from './cobalt/Listing.component.js' +import FormComponent from './cobalt/Form.component.js' const dash_components = { SideBarComponent, @@ -14,6 +15,7 @@ const dash_components = { AppPasswordFormComponent, ListingComponent, + FormComponent, } export { dash_components } diff --git a/app/assets/app/dash/SideBar.component.js b/app/assets/app/dash/SideBar.component.js index a50f479..374e876 100644 --- a/app/assets/app/dash/SideBar.component.js +++ b/app/assets/app/dash/SideBar.component.js @@ -37,17 +37,17 @@ export default class SideBarComponent extends Component { { text: 'Groups', action: 'redirect', - next: '/dash/groups', + next: '/dash/c/listing/ldap/Group', }, { text: 'LDAP Clients', action: 'redirect', - next: '/dash/ldap/clients', + next: '/dash/c/listing/ldap/Client', }, { text: 'SAML Service Providers', action: 'redirect', - next: '/dash/saml/service-providers', + next: '/dash/c/listing/saml/Provider', }, { text: 'Settings', diff --git a/app/assets/app/dash/message/MessageContainer.component.js b/app/assets/app/dash/message/MessageContainer.component.js index 5ddf1e2..78c548c 100644 --- a/app/assets/app/dash/message/MessageContainer.component.js +++ b/app/assets/app/dash/message/MessageContainer.component.js @@ -26,7 +26,7 @@ const template = ` aria-hidden="true" ref="modal" > -
# {{ col.name }}
{{ index + 1 }} - {{ col.field ? 'Yes' : 'No' }} - {{ col.field in row ? row[col.field] : '-' }} + {{ col.renderer(row[col.field]) }} + {{ row[col.field] ? 'Yes' : 'No' }} + {{ col.field in row ? row[col.field] : '-' }} + +