Upgrade @extollo/lib and finish dashboard interfaces

This commit is contained in:
2022-07-09 12:58:13 -05:00
parent 4715bf2758
commit 48edd7134f
13 changed files with 409 additions and 143 deletions

View File

@@ -2,11 +2,13 @@
export enum FieldType {
text = 'text',
textarea = 'textarea',
html = 'html',
email = 'email',
number = 'number',
integer = 'integer',
date = 'date',
select = 'select',
bool = 'bool',
}
enum ResourceAction {
@@ -41,6 +43,10 @@ export type FieldBase = {
readonly?: boolean,
helpText?: string,
placeholder?: string,
hideOn?: {
form?: boolean,
listing?: boolean,
},
}
export type SelectOptions = {display: string, value: any}[]

View File

@@ -1,4 +1,4 @@
import {allResourceActions, FieldType, Renderer, ResourceConfiguration} from '../cobalt'
import {allResourceActions, FieldType, Renderer, ResourceAction, ResourceConfiguration} from '../cobalt'
export default {
resources: [
@@ -22,7 +22,7 @@ export default {
{
key: 'description',
display: 'Description',
type: FieldType.textarea,
type: FieldType.html,
required: true,
renderer: Renderer.html,
},
@@ -43,5 +43,178 @@ export default {
}
],
},
{
key: 'snippet',
primaryKey: 'snippet_id',
collection: 'snippets',
display: {
field: 'slug',
singular: 'Snippet',
plural: 'Snippets',
},
supportedActions: allResourceActions,
fields: [
{
key: 'slug',
display: 'Slug',
type: FieldType.text,
required: true,
sort: 'asc',
},
{
key: 'syntax',
display: 'Syntax',
type: FieldType.text,
required: true,
},
{
key: 'users_only',
display: 'Users only?',
type: FieldType.bool,
required: true,
renderer: Renderer.bool,
},
{
key: 'single_access_only',
display: 'Single-access only?',
type: FieldType.bool,
required: true,
renderer: Renderer.bool,
},
{
key: 'access_key',
display: 'Access key',
type: FieldType.text,
required: false,
hideOn: {
listing: true,
},
},
{
key: 'body',
display: 'Content',
type: FieldType.textarea,
required: true,
hideOn: {
listing: true,
},
},
],
},
{
key: 'contactSubmission',
primaryKey: 'contact_submission_id',
collection: 'contact_submissions',
display: {
field: 'name',
singular: 'Contact Submission',
plural: 'Contact Submissions',
},
supportedActions: [
ResourceAction.read, ResourceAction.readOne, ResourceAction.delete,
],
fields: [
{
key: 'name',
display: 'Name',
type: FieldType.text,
required: true,
},
{
key: 'email',
display: 'E-Mail Address',
type: FieldType.text,
required: true,
},
{
key: 'message',
display: 'Message',
type: FieldType.textarea,
required: true,
},
{
key: 'sent_at',
display: 'Sent At',
type: FieldType.date,
renderer: FieldType.date,
required: false,
sort: 'desc',
},
],
},
{
key: 'goLink',
primaryKey: 'go_link_id',
collection: 'go_links',
display: {
field: 'short',
singular: 'Go Link',
plural: 'Go Links',
},
supportedActions: allResourceActions,
fields: [
{
key: 'short',
display: 'Slug',
type: FieldType.text,
required: true,
sort: 'asc',
},
{
key: 'url',
display: 'URL',
type: FieldType.text,
required: true,
},
{
key: 'active',
display: 'Active?',
type: FieldType.bool,
renderer: Renderer.bool,
required: true,
},
],
},
{
key: 'feedPost',
primaryKey: 'feed_post_id',
collection: 'feed_posts',
display: {
// field: '',
singular: 'Feed Post',
plural: 'Feed Posts',
},
supportedActions: allResourceActions,
fields: [
{
key: 'posted_at',
display: 'Posted At',
type: FieldType.date,
sort: 'desc',
required: false,
renderer: Renderer.date,
},
{
key: 'tag',
display: 'Tag',
type: FieldType.text,
required: true,
},
{
key: 'body',
display: 'Body',
type: FieldType.html,
renderer: Renderer.html,
required: true,
},
{
key: 'visible',
display: 'Visible?',
type: FieldType.bool,
renderer: Renderer.bool,
required: false,
},
],
}
] as ResourceConfiguration[],
}

View File

@@ -38,6 +38,15 @@ export class Interface extends Controller {
})
}
public viewForm(key: string, id: number|string) {
this.getResourceConfigOrFail(key)
return view('dash:form', {
resourcekey: key,
resourceid: id,
resourcemode: 'view',
})
}
public insertForm(key: string) {
this.getResourceConfigOrFail(key)
return view('dash:form', {

View File

@@ -151,7 +151,7 @@ export class ResourceAPI extends Controller {
private castValue(fieldDef: FieldDefinition, value: any): any {
const { type, key, required } = fieldDef
if ( type === FieldType.text || type === FieldType.textarea ) {
if ( type === FieldType.text || type === FieldType.textarea || type === FieldType.html ) {
const cast = String(value || '')
if ( required && !cast ) {
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
@@ -205,6 +205,8 @@ export class ResourceAPI extends Controller {
if ( required ) {
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Invalid or missing value for select: ${key}`)
}
} else if ( type === FieldType.bool ) {
return !!value
}
}

View File

@@ -16,26 +16,6 @@ Route
.alias('@dash')
.calls<Dash>(Dash, dash => dash.main)
Route.get('/work-items')
.alias('@dash:work-items')
.handledBy(() => 'nope')
Route.get('/feed')
.alias('@dash:feed')
.handledBy(() => 'nope')
Route.get('/links')
.alias('@dash:links')
.handledBy(() => 'nope')
Route.get('/snippets')
.alias('@dash:snippets')
.handledBy(() => 'nope')
Route.get('/contact')
.alias('@dash:contact')
.handledBy(() => 'nope')
Route.group('/cobalt/resource', () => {
Route.get('/:key/configure')
.parameterMiddleware(parseKey)
@@ -79,6 +59,11 @@ Route
.parameterMiddleware(parseKey)
.parameterMiddleware(parseId)
.calls(Interface, (i: Interface) => i.updateForm)
Route.get('/cobalt/form/:key/:id/view')
.parameterMiddleware(parseKey)
.parameterMiddleware(parseId)
.calls(Interface, (i: Interface) => i.viewForm)
})
.pre(SessionAuthMiddleware)
.pre(AuthRequiredMiddleware)

View File

@@ -12,6 +12,17 @@ const template = `
<div v-if="status === 'loading'">Loading...</div>
<form v-if="status === 'ready'">
<div class="form-group" v-for="field of fields">
<input
v-if="field.type === 'bool'"
class="form-check-inline"
type="checkbox"
:name="field.key"
v-model="data[field.key]"
:id="id + field.key"
:aria-describedby="id + field.key + '_help'"
:readonly="mode === 'view' || field.readonly"
:required="field.required"
>
<label for="id + field.key">{{ field.display }}</label>
<input
v-if="field.type === 'text' || field.type === 'email' || field.type === 'number' || field.type === 'integer' || !field.type"
@@ -49,6 +60,16 @@ const template = `
:placeholder="field.placeholder || ''"
:readonly="mode === 'view' || field.readonly"
>
<cobalt-monaco
v-if="field.type === 'html' && !(mode === 'view' || field.readonly)"
syntax="html"
:initialvalue="data[field.key]"
@changed="value => data[field.key] = value"
></cobalt-monaco>
<div
v-if="field.type === 'html' && (mode === 'view' || field.readonly)"
v-html="data[field.key]"
></div>
<DatePicker
v-model="data[field.key]"
v-if="field.type === 'date' && !(mode === 'view' || field.readonly)"
@@ -148,7 +169,9 @@ export class FormComponent extends Component {
async load() {
this.data = this.internalResourceId ? await this.resource.readOne(this.internalResourceId) : {}
this.fields = [...this.resource.configuration.fields]
this.fields = [...this.resource.configuration.fields].filter(f => {
return !(f.hideOn && f.hideOn.form)
})
this.fields.forEach(field => {
if ( field.type === 'date' && this.data[field.key] ) {
@@ -165,6 +188,12 @@ export class FormComponent extends Component {
this.statusMessage = ''
}
onMonacoChange(field, data, args) {
console.log('on monaco change', {field, data, args})
this.data[field.key] = args[0]
console.log('after save', this.data)
}
async save() {
if ( !this.validate() ) return;
@@ -173,29 +202,35 @@ export class FormComponent extends Component {
const values = {}
this.fields.forEach(field => {
let value = this.data[field.key]
const isUndef = !value && !(['integer', 'number'].includes(field.type) && value === 0)
const isUndef = !value && !(['integer', 'number'].includes(field.type) && value === 0) && (field.type !== 'bool')
if ( isUndef ) return;
if ( field.type === 'number' ) value = parseFloat(String(value))
if ( field.type === 'integer' ) value = parseInt(String(value), 10)
if ( field.type === 'date' ) value = value.toISOString()
if ( field.type === 'bool' ) value = !!value
values[field.key] = value
})
if ( this.internalResourceId ) {
await this.resource.update(this.internalResourceId, values)
} else {
const result = await this.resource.create(values)
this.internalResourceId = result[this.resource.configuration.primaryKey]
history.replaceState({}, document.getElementsByTagName('title')[0].innerText, `${location.protocol}//${location.host}/dash/cobalt/form/${this.resource.key}/${this.internalResourceId}`)
try {
if (this.internalResourceId) {
await this.resource.update(this.internalResourceId, values)
} else {
const result = await this.resource.create(values)
this.internalResourceId = result[this.resource.configuration.primaryKey]
history.replaceState({}, document.getElementsByTagName('title')[0].innerText, `${location.protocol}//${location.host}/dash/cobalt/form/${this.resource.key}/${this.internalResourceId}`)
}
await this.load()
this.statusMessage = `${this.resource.singular()} ${this.mode === 'insert' ? 'created' : 'saved'}`
this.mode = 'edit'
setTimeout(() => this.statusMessage = '', 7000)
} catch (e) {
console.error(e)
this.statusMessage = 'An unknown error occurred while saving'
}
await this.load()
this.statusMessage = `${this.resource.singular()} ${this.mode === 'insert' ? 'created' : 'saved'}`
this.mode = 'edit'
setTimeout(() => this.statusMessage = '', 7000)
}
validate() {
@@ -206,7 +241,7 @@ export class FormComponent extends Component {
// FIXME select
const value = this.data[field.key]
const isUndef = !value && !(['integer', 'number'].includes(field.type) && value === 0)
const isUndef = !value && !(['integer', 'number'].includes(field.type) && value === 0) && (field.type !== 'bool')
if ( field.required && isUndef ) {
this.errors[field.key] = 'This field is required'
pass = false

View File

@@ -92,7 +92,9 @@ export class ListingComponent extends Component {
}
async load(reload = false) {
this.columns = [...this.resource.configuration.fields]
this.columns = [...this.resource.configuration.fields].filter(col => {
return !(col.hideOn && col.hideOn.listing)
})
if ( !reload && this.resource.supports(ResourceActions.create) ) {
this.actions.push({
@@ -117,6 +119,16 @@ export class ListingComponent extends Component {
action: 'update',
defer: true,
})
} else if ( !reload && this.resource.supports(ResourceActions.read) ) {
this.actions.push({
title: 'View',
color: 'primary',
icon: 'fa-eye',
type: 'resource',
resource: this.resource.key,
action: 'view',
defer: true,
})
}
if ( !reload && this.resource.supports(ResourceActions.delete) ) {

View File

@@ -0,0 +1,35 @@
import {Component} from '../../vues6.js'
import {uuid} from '../util.js'
const template = `
<div class="monaco-container" :id="id" style="width: 100%; height: 100%; min-height: 400px">
</div>
`
export class MonacoComponent extends Component {
static get template() { return template }
static get selector() { return 'cobalt-monaco' }
static get props() { return ['initialvalue', 'syntax'] }
constructor() {
super()
this.id = 'monaco-' + uuid()
this.value = ''
}
vue_on_create() {
this.value = this.initialvalue
this.$nextTick(() => {
this.el = document.querySelector(`#${this.id}`)
this.editor = monaco.editor.create(this.el, {
theme: 'vs-dark',
value: this.value,
language: this.syntax,
})
this.editor.onDidChangeModelContent(e => {
this.value = this.editor.getValue()
this.$emit('changed', this.value)
})
})
}
}

View File

@@ -2,12 +2,14 @@ import {MessageContainerComponent} from './component/MessageContainer.component.
import {ListingComponent} from './component/Listing.component.js'
import {FormComponent} from './component/Form.component.js'
import {ActionButtonComponent} from './component/ActionButton.component.js'
import {MonacoComponent} from './component/Monaco.component.js'
const components = {
MessageContainerComponent,
ListingComponent,
ActionButtonComponent,
FormComponent,
MonacoComponent,
}
export { components }

View File

@@ -21,6 +21,7 @@ export class ActionService {
if ( action.action === 'insert' ) this.launchResourceForm(action.resource)
if ( action.action === 'update' ) this.launchResourceForm(action.resource, data[resource.configuration.primaryKey])
if ( action.action === 'list' ) this.launchResourceListing(action.resource)
if ( action.action === 'view' ) this.launchResourceForm(action.resource, data[resource.configuration.primaryKey], true)
if ( action.action === 'delete' ) {
await Message.get()
.modal({
@@ -55,8 +56,8 @@ export class ActionService {
location.assign(Session.get().url(`dash/cobalt/listing/${key}`))
}
launchResourceForm(key, id) {
location.assign(Session.get().url(`dash/cobalt/form/${key}${id ? '/' + id : ''}`))
launchResourceForm(key, id, view = false) {
location.assign(Session.get().url(`dash/cobalt/form/${key}${id ? '/' + id : ''}${view ? '/view' : ''}`))
}
goBack() {

View File

@@ -13,6 +13,7 @@ head
link(href=asset('dash/vendor/fontawesome-free/css/all.min.css') rel='stylesheet' type='text/css')
link(href=asset('dash/vendor/datatables2/dataTables.bootstrap4.min.css'))
link(rel='stylesheet' href='https://unpkg.com/vue2-datepicker@latest/index.css')
link(rel='stylesheet' data-name='vs/editor/editor.main' href=asset('monaco/package/min/vs/editor/editor.main.css'))
#wrapper
ul#accordionSidebar.navbar-nav.bg-gradient-primary.sidebar.sidebar-dark.accordion
a.sidebar-brand.d-flex.align-items-center.justify-content-center(href='index.html')
@@ -40,7 +41,7 @@ head
hr.sidebar-divider
.sidebar-heading Analytics
li.nav-item
a.nav-link(href=named('@dash:contact'))
a.nav-link(href=route('dash/cobalt/listing/contactSubmission'))
i.fas.fa-fw.fa-comment-alt
span Contact Form
#content-wrapper.d-flex.flex-column
@@ -135,6 +136,11 @@ block scripts
script(src=asset('dash/vendor/datatables2/jquery.dataTables.min.js'))
script(src=asset('dash/vendor/datatables2/dataTables.bootstrap4.min.js'))
script(src='https://unpkg.com/vue2-datepicker@latest')
script.
var require = { paths: { vs: "#{asset('monaco/package/min/vs')}" }}
script(src=asset('monaco/package/min/vs/loader.js'))
script(src=asset('monaco/package/min/vs/editor/editor.main.nls.js'))
script(src=asset('monaco/package/min/vs/editor/editor.main.js'))
script(type='module').
import { Session } from "#{asset('cobalt/service/Session.service.js')}"
import { EventBus } from "#{asset('cobalt/service/EventBus.service.js')}"