Add button actions, simplify contexts and tidy up

This commit is contained in:
Andrew Kingston 2020-11-25 09:50:51 +00:00
parent 199c3409c9
commit 1e857f101a
23 changed files with 156 additions and 97 deletions

View File

@ -40,13 +40,16 @@
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => ({
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}))
.map(property => {
return {
providerId: property.instance._id,
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}
})
</script>
<div

View File

@ -1,4 +1,4 @@
import { getAppId } from "../utils"
import { getAppId } from "../utils/getAppId"
/**
* API cache for cached request responses.

View File

@ -19,9 +19,10 @@ export const fetchDatasource = async (datasource, dataContext) => {
} else if (type === "view") {
rows = await fetchViewData(datasource)
} else if (type === "link") {
const row = dataContext[datasource.providerId]
rows = await fetchRelationshipData({
rowId: dataContext?._id,
tableId: dataContext?.tableId,
rowId: row?._id,
tableId: row?.tableId,
fieldName,
})
}

View File

@ -1,11 +1,13 @@
<script>
import { writable } from "svelte/store"
import { setContext, onMount } from "svelte"
import Component from "./Component.svelte"
import SDK from "../sdk"
import { routeStore, screenStore, createDataStore } from "../store"
import { createDataStore, routeStore, screenStore } from "../store"
// Provide contexts
setContext("sdk", SDK)
setContext("component", writable({}))
setContext("data", createDataStore())
let loaded = false

View File

@ -3,69 +3,35 @@
import { writable } from "svelte/store"
import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte"
import { enrichDataBinding } from "../utils"
import { enrichProps } from "../utils/componentProps"
import { bindingStore } from "../store"
export let definition = {}
// Extracts the actual component name from the library name
const extractComponentName = name => {
const split = name?.split("/")
return split?.[split.length - 1]
}
// Get local data binding context
const dataStore = getContext("data")
// Extracts valid props to pass to the real svelte component
const extractValidProps = component => {
let props = {}
Object.entries(component)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
props[key] = value
})
return props
}
// Enriches data bindings to real values based on data context
const enrichDataBindings = (dataContexts, dataBindings, props) => {
const state = {
...dataContexts,
...dataBindings,
}
let enrichedProps = {}
Object.entries(props).forEach(([key, value]) => {
enrichedProps[key] = enrichDataBinding(value, state)
})
return enrichedProps
}
// Gets the component constructor for the specified component
const getComponentConstructor = name => {
return name === "screenslot" ? Router : ComponentLibrary[componentName]
}
// Create component context
const componentStore = writable({})
setContext("component", componentStore)
// Extract component definition info
$: componentName = extractComponentName(definition._component)
$: constructor = getComponentConstructor(componentName)
$: componentProps = extractValidProps(definition)
$: constructor = getComponentConstructor(definition._component)
$: children = definition._children
$: id = definition._id
$: dataContext = getContext("data")
$: enrichedProps = enrichDataBindings(
$dataContext,
$bindingStore,
componentProps
)
$: enrichedProps = enrichProps(definition, $dataStore, $bindingStore)
// Update component context
// ID is duplicated inside style so that the "styleable" helper can set
// an ID data tag for unique reference to components
const componentStore = writable({})
setContext("component", componentStore)
$: componentStore.set({
id,
styles: { ...definition._styles, id },
dataContext: $dataContext.data,
})
$: componentStore.set({ id, styles: { ...definition._styles, id } })
// Gets the component constructor for the specified component
const getComponentConstructor = component => {
const split = component?.split("/")
const name = split?.[split.length - 1]
return name === "screenslot" ? Router : ComponentLibrary[name]
}
</script>
{#if constructor}

View File

@ -4,15 +4,11 @@
export let row
// Get current contexts
// Clone and create new data context for this component tree
const data = getContext("data")
const component = getContext("component")
// Clone current context to this context
const newData = createDataStore($data)
setContext("data", newData)
// Add additional layer to context
$: newData.actions.addContext(row, $component.id)
</script>

View File

@ -1,6 +1,7 @@
import * as API from "./api"
import { authStore, routeStore, screenStore, bindingStore } from "./store"
import { styleable, getAppId } from "./utils"
import { styleable } from "./utils/styleable"
import { getAppId } from "./utils/getAppId"
import { link as linkable } from "svelte-spa-router"
import DataProvider from "./components/DataProvider.svelte"

View File

@ -1,5 +1,5 @@
import * as API from "../api"
import { getAppId } from "../utils"
import { getAppId } from "../utils/getAppId"
import { writable } from "svelte/store"
const createAuthStore = () => {

View File

@ -1,20 +1,16 @@
import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
const initialValue = {
data: null,
}
export const createDataStore = existingContext => {
const initial = existingContext ? cloneDeep(existingContext) : initialValue
const store = writable(initial)
const store = writable({ ...existingContext })
// Adds a context layer to the data context tree
const addContext = (row, componentId) => {
store.update(state => {
if (componentId) {
state[componentId] = row
state.data = row
state[`${componentId}_draft`] = cloneDeep(row)
state.closestComponentId = componentId
}
return state
})
@ -22,6 +18,9 @@ export const createDataStore = existingContext => {
return {
subscribe: store.subscribe,
update: store.update,
actions: { addContext },
}
}
export const dataStore = createDataStore()

View File

@ -5,4 +5,4 @@ export { builderStore } from "./builder"
export { bindingStore } from "./binding"
// Data stores are layered and duplicated, so it is not a singleton
export { createDataStore } from "./data"
export { createDataStore, dataStore } from "./data"

View File

@ -2,7 +2,7 @@ import { writable, derived } from "svelte/store"
import { routeStore } from "./routes"
import { builderStore } from "./builder"
import * as API from "../api"
import { getAppId } from "../utils"
import { getAppId } from "../utils/getAppId"
const createScreenStore = () => {
const config = writable({

View File

@ -0,0 +1,45 @@
import { enrichDataBinding } from "./enrichDataBinding"
import { routeStore } from "../store"
import { saveRow, deleteRow } from "../api"
const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`]
if (action.parameters.fields) {
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
draft[key] = enrichDataBinding(entry.value, context)
})
}
await saveRow(draft)
}
const deleteRowHandler = async (action, context) => {
const { tableId, revId, rowId } = action.parameters
await deleteRow({
tableId: enrichDataBinding(tableId, context),
rowId: enrichDataBinding(rowId, context),
revId: enrichDataBinding(revId, context),
})
}
const navigationHandler = action => {
routeStore.actions.navigate(action.parameters.url)
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
}
/**
* Parses an array of actions and returns a function which will execute the
* actions in the current context.
*/
export const enrichButtonActions = (actions, context) => {
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => {
for (let i = 0; i < handlers.length; i++) {
await handlers[i](actions[i], context)
}
}
}

View File

@ -0,0 +1,35 @@
import { enrichDataBindings } from "./enrichDataBinding"
import { enrichButtonActions } from "./buttonActions"
/**
* Enriches component props.
* Data bindings are enriched, and button actions are enriched.
*/
export const enrichProps = (props, dataContexts, dataBindings) => {
// Exclude all private props that start with an underscore
let validProps = {}
Object.entries(props)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
validProps[key] = value
})
// Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires
const context = {
...dataContexts,
...dataBindings,
data: dataContexts[dataContexts.closestComponentId],
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
}
// Enrich all data bindings in top level props
let enrichedProps = enrichDataBindings(validProps, context)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
}
return enrichedProps
}

View File

@ -33,3 +33,14 @@ export const enrichDataBinding = (input, context) => {
}
return mustache.render(input, context)
}
/**
* Enriches each prop in a props object
*/
export const enrichDataBindings = (props, context) => {
let enrichedProps = {}
Object.entries(props).forEach(([key, value]) => {
enrichedProps[key] = enrichDataBinding(value, context)
})
return enrichedProps
}

View File

@ -1,3 +0,0 @@
export { getAppId } from "./getAppId"
export { styleable } from "./styleable"
export { enrichDataBinding } from "./enrichDataBinding"

View File

@ -7,12 +7,14 @@
export let className = "default"
export let disabled = false
export let text
export let onClick
</script>
<button
class="default"
disabled={disabled || false}
use:styleable={$component.styles}>
use:styleable={$component.styles}
on:click={onClick}>
{text}
</button>

View File

@ -5,8 +5,9 @@
import LinkedRowSelector from "./LinkedRowSelector.svelte"
import { capitalise } from "./helpers"
const { styleable, screenStore, API } = getContext("sdk")
const { styleable, API } = getContext("sdk")
const component = getContext("component")
const data = getContext("data")
export let wide = false
@ -14,14 +15,17 @@
let schema
let fields = []
$: getContextDetails($component.dataContext)
// Fetch info about the closest data context
$: getFormData($data[$data.closestComponentId])
const getContextDetails = async dataContext => {
if (dataContext) {
row = dataContext
const tableDefinition = await API.fetchTableDefinition(row.tableId)
const getFormData = async context => {
if (context) {
const tableDefinition = await API.fetchTableDefinition(context.tableId)
schema = tableDefinition.schema
fields = Object.keys(schema)
// Use the draft version for editing
row = $data[`${$data.closestComponentId}_draft`]
}
}
</script>

View File

@ -4,6 +4,7 @@
const { API, styleable, DataProvider } = getContext("sdk")
const component = getContext("component")
const data = getContext("data")
export let datasource = []
@ -11,7 +12,7 @@
onMount(async () => {
if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource, $component.dataContext)
rows = await API.fetchDatasource(datasource, $data)
}
})
</script>

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
const component = getContext("component")
export let title
export let datasource
@ -35,7 +34,7 @@
// Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $component.dataContext)
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
const component = getContext("component")
export let title
export let datasource
@ -33,7 +32,7 @@
// Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $component.dataContext)
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
const component = getContext("component")
// Common props
export let title
@ -41,7 +40,7 @@
// Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $component.dataContext)
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
const component = getContext("component")
export let title
export let datasource
@ -31,7 +30,7 @@
// Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $component.dataContext)
const result = await API.fetchDatasource(datasource)
const data = result
.filter(row => row[labelColumn] != null && row[valueColumn] != null)
.slice(0, 20)

View File

@ -58,7 +58,7 @@
onMount(async () => {
if (!isEmpty(datasource)) {
data = await API.fetchDatasource(datasource, $component.dataContext)
data = await API.fetchDatasource(datasource)
let schema
// Get schema for datasource