Upgrade @extollo/lib and finish dashboard interfaces
This commit is contained in:
@@ -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}[]
|
||||
|
||||
@@ -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[],
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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')}"
|
||||
|
||||
Reference in New Issue
Block a user