Add button actions, simplify contexts and tidy up
This commit is contained in:
parent
199c3409c9
commit
1e857f101a
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getAppId } from "../utils"
|
||||
import { getAppId } from "../utils/getAppId"
|
||||
|
||||
/**
|
||||
* API cache for cached request responses.
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export { getAppId } from "./getAppId"
|
||||
export { styleable } from "./styleable"
|
||||
export { enrichDataBinding } from "./enrichDataBinding"
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue