Add button actions, simplify contexts and tidy up

This commit is contained in:
Andrew Kingston 2020-11-25 09:50:51 +00:00
parent ad5fc0e780
commit 907c0fcfda
23 changed files with 156 additions and 97 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import * as API from "./api" import * as API from "./api"
import { authStore, routeStore, screenStore, bindingStore } from "./store" 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 { link as linkable } from "svelte-spa-router"
import DataProvider from "./components/DataProvider.svelte" import DataProvider from "./components/DataProvider.svelte"

View File

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

View File

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

View File

@ -5,4 +5,4 @@ export { builderStore } from "./builder"
export { bindingStore } from "./binding" export { bindingStore } from "./binding"
// Data stores are layered and duplicated, so it is not a singleton // 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 { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import * as API from "../api" import * as API from "../api"
import { getAppId } from "../utils" import { getAppId } from "../utils/getAppId"
const createScreenStore = () => { const createScreenStore = () => {
const config = writable({ 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) 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 className = "default"
export let disabled = false export let disabled = false
export let text export let text
export let onClick
</script> </script>
<button <button
class="default" class="default"
disabled={disabled || false} disabled={disabled || false}
use:styleable={$component.styles}> use:styleable={$component.styles}
on:click={onClick}>
{text} {text}
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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