events hooked up and working

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,21 @@ const apiOpts = {
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 {
authenticate: authenticate(apiOpts),
triggerWorkflow: triggerWorkflow(apiOpts),
createRecord,
updateRecord,
}

View File

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

View File

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

View File

@ -1,22 +1,39 @@
import api from "../api"
import renderTemplateString from "./renderTemplateString"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => {
const handler = (parameters, execute) => ({
execute,
parameters,
})
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),
const handlers = {
"Navigate To": param => routeTo(param && param.url),
"Create Record": api.createRecord,
"Update Record": api.updateRecord,
"Trigger 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 =>
Array.isArray(prop) &&
prop.length > 0 &&
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined
const createParameters = (parameterTemplateObj, state) => {
const parameters = {}
for (let key in parameterTemplateObj) {
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 {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { eventHandlers } from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import renderTemplateString from "./renderTemplateString"
import appStore from "./store"
@ -25,33 +21,23 @@ export const createStateManager = ({
onScreenSlotRendered,
routeTo,
}) => {
let handlerTypes = 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
let runEventActions = eventHandlers(routeTo)
const bb = bbFactory({
getCurrentState,
componentLibraries,
onScreenSlotRendered,
runEventActions,
})
const setup = _setup({ handlerTypes, getCurrentState, bb })
const setup = _setup(bb)
return {
setup,
destroy: () => {},
getCurrentState,
}
}
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
const _setup = bb => node => {
const props = node.props
const initialProps = { ...props }
@ -70,53 +56,10 @@ const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
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)
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)
if (currentProps.onClick) {
node.addEventListener("click", () => {
const testText = currentProps.testText || "hello"
currentProps._bb.call(props.onClick, { testText })
currentProps._bb.call("onClick")
})
}
}

View File

@ -4,7 +4,7 @@ const newid = require("../../db/newid")
exports.patch = async function(ctx) {
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 patchfields = ctx.request.body
@ -13,6 +13,8 @@ exports.patch = async function(ctx) {
record[key] = patchfields[key]
}
coerceFieldsToCorrectType(record, model)
const validateResult = await validate({
record,
model,
@ -47,6 +49,8 @@ exports.save = async function(ctx) {
const model = await db.get(record.modelId)
coerceFieldsToCorrectType(record, model)
const validateResult = await validate({
record,
model,
@ -182,6 +186,32 @@ exports.validate = async function(ctx) {
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 }) {
if (!model) {
const db = new CouchDB(instanceId)

View File

@ -25,7 +25,7 @@ router
.patch(
"/api/:modelId/records/:id",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save
recordController.patch
)
.post(
"/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 disabled = false
export let text
export let onClick
export let _bb
let theButton
@ -11,7 +10,7 @@
theButton && _bb.attachChildren(theButton)
const clickHandler = () => {
_bb.call(onClick)
_bb.call("onClick")
}
</script>

View File

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

View File

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

View File

@ -11,7 +11,10 @@
export let _bb
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) => {