events hooked up and working

This commit is contained in:
Michael Shanks 2020-09-10 21:11:05 +01:00
parent d63e03b852
commit d3225cb51f
17 changed files with 133 additions and 115 deletions

View File

@ -9,7 +9,6 @@
CircleIndicator, CircleIndicator,
EventsIcon, EventsIcon,
} from "components/common/Icons/" } from "components/common/Icons/"
import EventsEditor from "./EventsEditor"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
@ -21,7 +20,6 @@
let categories = [ let categories = [
{ value: "settings", name: "Settings" }, { value: "settings", name: "Settings" },
{ value: "design", name: "Design" }, { value: "design", name: "Design" },
{ value: "events", name: "Events" },
] ]
let selectedCategory = categories[0] let selectedCategory = categories[0]
@ -109,8 +107,6 @@
displayNameField={displayName} displayNameField={displayName}
onChange={onPropChanged} onChange={onPropChanged}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if} {/if}
</div> </div>

View File

@ -3,6 +3,10 @@
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
export let parameters export let parameters
@ -14,25 +18,36 @@
}) })
let idFields let idFields
let recordId
$: { $: {
idFields = bindableProperties.filter( idFields = bindableProperties.filter(
bindable => bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id") bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
) )
// ensure recordId is always defaulted - there is usually only one option // ensure recordId is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters.recordId) { if (idFields.length > 0 && !parameters._id) {
parameters.recordId = idFields[0].runtimeBinding recordId = idFields[0].runtimeBinding
parameters = parameters parameters = parameters
} else if (!recordId && parameters._id) {
recordId = parameters._id
.replace("{{", "")
.replace("}}", "")
.trim()
} }
} }
$: parameters._id = `{{ ${recordId} }}`
// just wraps binding in {{ ... }} // just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}` const toBindingExpression = bindingPath => {
console.log("yeo")
return `{{ ${bindingPath} }}`
}
// finds the selected idBinding, then reads the table/view // finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to. // from the component instance that it belongs to.
// then returns the field names for that schema // then returns the field names for that schema
const fieldNamesFromIdBinding = recordId => { const modelInfoFromIdBinding = recordId => {
if (!recordId) return [] if (!recordId) return []
const idBinding = bindableProperties.find( const idBinding = bindableProperties.find(
@ -52,14 +67,18 @@
const model = $backendUiStore.models.find( const model = $backendUiStore.models.find(
m => m._id === instance[component.context] m => m._id === instance[component.context]
) )
parameters.modelId = modelId
return Object.keys(model.schema) return Object.keys(model.schema)
} }
$: fieldNames = let fieldNames
parameters && parameters.recordId $: {
? fieldNamesFromIdBinding(parameters.recordId) if (parameters && recordId) {
: [] fieldNames = modelInfoFromIdBinding(recordId)
} else {
fieldNames = []
}
}
const onFieldsChanged = e => { const onFieldsChanged = e => {
parameters.fields = e.detail parameters.fields = e.detail
@ -74,16 +93,17 @@
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Record Id</Label> <Label size="m" color="dark">Record Id</Label>
<Select secondary bind:value={parameters.recordId}> <Select secondary bind:value={recordId}>
<option value="" />
{#each idFields as idField} {#each idFields as idField}
<option value={toBindingExpression(idField.runtimeBinding)}> <option value={idField.runtimeBinding}>
{idField.readableBinding} {idField.readableBinding}
</option> </option>
{/each} {/each}
</Select> </Select>
{/if} {/if}
{#if parameters.recordId} {#if recordId}
<SaveFields <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
schemaFields={fieldNames} schemaFields={fieldNames}

View File

@ -1 +0,0 @@
export { default } from "./EventsEditor.svelte"

View File

@ -53,7 +53,21 @@ const apiOpts = {
delete: del, delete: del,
} }
const createRecord = async params =>
await post({ url: `/api/${params.modelId}/records`, body: params.fields })
const updateRecord = async params => {
const record = params.fields
record._id = params._id
await patch({
url: `/api/${params.modelId}/records/${params._id}`,
body: record,
})
}
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
triggerWorkflow: triggerWorkflow(apiOpts), triggerWorkflow: triggerWorkflow(apiOpts),
createRecord,
updateRecord,
} }

View File

@ -50,7 +50,6 @@ export const createApp = ({
treeNode, treeNode,
onScreenSlotRendered, onScreenSlotRendered,
setupState: stateManager.setup, setupState: stateManager.setup,
getCurrentState: stateManager.getCurrentState,
}) })
return getInitialiseParams return getInitialiseParams

View File

@ -1,12 +1,13 @@
import setBindableComponentProp from "./setBindableComponentProp" import setBindableComponentProp from "./setBindableComponentProp"
import { attachChildren } from "../render/attachChildren" import { attachChildren } from "../render/attachChildren"
import store from "../state/store"
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "") export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
export const bbFactory = ({ export const bbFactory = ({
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
getCurrentState, runEventActions,
}) => { }) => {
const apiCall = method => (url, body) => { const apiCall = method => (url, body) => {
return fetch(url, { return fetch(url, {
@ -26,13 +27,6 @@ export const bbFactory = ({
delete: apiCall("DELETE"), delete: apiCall("DELETE"),
} }
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
return (treeNode, setupState) => { return (treeNode, setupState) => {
const attachParams = { const attachParams = {
componentLibraries, componentLibraries,
@ -44,12 +38,17 @@ export const bbFactory = ({
return { return {
attachChildren: attachChildren(attachParams), attachChildren: attachChildren(attachParams),
props: treeNode.props, props: treeNode.props,
call: safeCallEvent, call: async eventName =>
eventName &&
(await runEventActions(
treeNode.props[eventName],
store.getState(treeNode.contextStoreKey)
)),
setBinding: setBindableComponentProp(treeNode), setBinding: setBindableComponentProp(treeNode),
api, api,
parent, parent,
// these parameters are populated by screenRouter // these parameters are populated by screenRouter
routeParams: () => getCurrentState()["##routeParams"], routeParams: () => store.getState()["##routeParams"],
} }
} }
} }

View File

@ -1,22 +1,39 @@
import api from "../api" import api from "../api"
import renderTemplateString from "./renderTemplateString"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => { export const eventHandlers = routeTo => {
const handler = (parameters, execute) => ({ const handlers = {
execute, "Navigate To": param => routeTo(param && param.url),
parameters, "Create Record": api.createRecord,
}) "Update Record": api.updateRecord,
"Trigger Workflow": api.triggerWorkflow,
return {
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
"Create Record": handler(["url"], param => param),
"Update Record": handler(["url"], param => param),
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
} }
// when an event is called, this is what gets run
const runEventActions = async (actions, state) => {
if (!actions) return
for (let action of actions) {
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
const parameters = createParameters(action.parameters, state)
if (handler) {
await handler(parameters)
}
}
}
return runEventActions
} }
export const isEventType = prop => const createParameters = (parameterTemplateObj, state) => {
Array.isArray(prop) && const parameters = {}
prop.length > 0 && for (let key in parameterTemplateObj) {
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined if (typeof parameterTemplateObj[key] === "string") {
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
} else if (typeof parameterTemplateObj[key] === "object") {
parameters[key] = createParameters(parameterTemplateObj[key], state)
}
}
return parameters
}

View File

@ -1,8 +1,4 @@
import { import { eventHandlers } from "./eventHandlers"
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi" import { bbFactory } from "./bbComponentApi"
import renderTemplateString from "./renderTemplateString" import renderTemplateString from "./renderTemplateString"
import appStore from "./store" import appStore from "./store"
@ -25,33 +21,23 @@ export const createStateManager = ({
onScreenSlotRendered, onScreenSlotRendered,
routeTo, routeTo,
}) => { }) => {
let handlerTypes = eventHandlers(routeTo) let runEventActions = eventHandlers(routeTo)
// creating a reference to the current state
// this avoids doing store.get() ... which is expensive on
// hot paths, according to the svelte docs.
// the state object reference never changes (although it's internals do)
// so this should work fine for us
let currentState
appStore.subscribe(s => (currentState = s))
const getCurrentState = () => currentState
const bb = bbFactory({ const bb = bbFactory({
getCurrentState,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
runEventActions,
}) })
const setup = _setup({ handlerTypes, getCurrentState, bb }) const setup = _setup(bb)
return { return {
setup, setup,
destroy: () => {}, destroy: () => {},
getCurrentState,
} }
} }
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => { const _setup = bb => node => {
const props = node.props const props = node.props
const initialProps = { ...props } const initialProps = { ...props }
@ -70,53 +56,10 @@ const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
node.stateBound = true node.stateBound = true
} }
} }
if (isEventType(propValue)) {
const state = appStore.getState(node.contextStoreKey)
const handlersInfos = []
for (let event of propValue) {
const handlerInfo = {
handlerType: event[EVENT_TYPE_MEMBER_NAME],
parameters: event.parameters,
}
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
resolvedParams[paramName] = () =>
renderTemplateString(paramValue, state)
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
if (handlersInfos.length === 0) {
initialProps[propName] = doNothing
} else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
await handler(context)
}
}
}
}
} }
const setup = _setup({ handlerTypes, getCurrentState, bb }) const setup = _setup(bb)
initialProps._bb = bb(node, setup) initialProps._bb = bb(node, setup)
return initialProps return initialProps
} }
const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType]
return async context => {
const parameters = {}
for (let paramName in handlerInfo.parameters) {
parameters[paramName] = handlerInfo.parameters[paramName](context)
}
await handlerType.execute(parameters)
}
}

View File

@ -181,8 +181,7 @@ const maketestlib = window => ({
currentProps = Object.assign(currentProps, props) currentProps = Object.assign(currentProps, props)
if (currentProps.onClick) { if (currentProps.onClick) {
node.addEventListener("click", () => { node.addEventListener("click", () => {
const testText = currentProps.testText || "hello" currentProps._bb.call("onClick")
currentProps._bb.call(props.onClick, { testText })
}) })
} }
} }

View File

@ -4,7 +4,7 @@ const newid = require("../../db/newid")
exports.patch = async function(ctx) { exports.patch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = await db.get(ctx.params._id) const record = await db.get(ctx.params.id)
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
const patchfields = ctx.request.body const patchfields = ctx.request.body
@ -13,6 +13,8 @@ exports.patch = async function(ctx) {
record[key] = patchfields[key] record[key] = patchfields[key]
} }
coerceFieldsToCorrectType(record, model)
const validateResult = await validate({ const validateResult = await validate({
record, record,
model, model,
@ -47,6 +49,8 @@ exports.save = async function(ctx) {
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
coerceFieldsToCorrectType(record, model)
const validateResult = await validate({ const validateResult = await validate({
record, record,
model, model,
@ -182,6 +186,32 @@ exports.validate = async function(ctx) {
ctx.body = errors ctx.body = errors
} }
// this function modifies an incoming record, to allow for things like
// "boolField": "true" (instead of mandating "boolField": true)
// this allows us to use mustash templating to send non-string fields in a request
const coerceFieldsToCorrectType = (record, model) => {
for (let fieldName in record) {
const fieldValue = record[fieldName]
if (model.schema[fieldName]) {
if (
model.schema[fieldName].type === "boolean" &&
typeof fieldValue !== "boolean"
) {
if (fieldValue === "true") record[fieldName] = true
if (fieldValue === "false") record[fieldName] = false
continue
}
if (model.schema[fieldName].type === "number") {
const val = parseFloat(fieldValue)
if (!isNaN(val)) {
record[fieldName] = val
}
}
}
}
}
async function validate({ instanceId, modelId, record, model }) { async function validate({ instanceId, modelId, record, model }) {
if (!model) { if (!model) {
const db = new CouchDB(instanceId) const db = new CouchDB(instanceId)

View File

@ -25,7 +25,7 @@ router
.patch( .patch(
"/api/:modelId/records/:id", "/api/:modelId/records/:id",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save recordController.patch
) )
.post( .post(
"/api/:modelId/records/validate", "/api/:modelId/records/validate",

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@
export let className = "default" export let className = "default"
export let disabled = false export let disabled = false
export let text export let text
export let onClick
export let _bb export let _bb
let theButton let theButton
@ -11,7 +10,7 @@
theButton && _bb.attachChildren(theButton) theButton && _bb.attachChildren(theButton)
const clickHandler = () => { const clickHandler = () => {
_bb.call(onClick) _bb.call("onClick")
} }
</script> </script>

View File

@ -14,7 +14,7 @@
if (containerElement) { if (containerElement) {
_bb.attachChildren(containerElement) _bb.attachChildren(containerElement)
if (!hasLoaded) { if (!hasLoaded) {
_bb.call(onLoad) _bb.call("onLoad")
hasLoaded = true hasLoaded = true
} }
} }

View File

@ -28,7 +28,7 @@
if (itemContainer) { if (itemContainer) {
_bb.attachChildren(itemContainer) _bb.attachChildren(itemContainer)
if (!hasLoaded) { if (!hasLoaded) {
_bb.call(onLoad) _bb.call("onLoad")
hasLoaded = true hasLoaded = true
} }
} }

View File

@ -11,7 +11,10 @@
export let _bb export let _bb
const rowClickHandler = row => () => { const rowClickHandler = row => () => {
_bb.call(onRowClick, row) // call currently only accepts one argument, so passing row does nothing
// however, we do not expose this event anyway. I am leaving this
// in for the future, as can and probably should hande this
_bb.call("onRowClick", row)
} }
const cellValue = (colIndex, row) => { const cellValue = (colIndex, row) => {