Add custom component actions. Simplify client context. Add form validation action
This commit is contained in:
parent
dbe7e8d4b7
commit
cf43cf765c
|
@ -1,7 +1,7 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
||||
import { findComponentPath } from "./storeUtils"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
// Regex to match all instances of template strings
|
||||
|
@ -11,9 +11,7 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
|||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
export const getBindableProperties = (rootComponent, componentId) => {
|
||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
||||
const componentBindings = getComponentBindings(rootComponent)
|
||||
return [...contextBindings, ...componentBindings]
|
||||
return getContextBindings(rootComponent, componentId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,6 +34,30 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getActionProviderComponents = (
|
||||
rootComponent,
|
||||
componentId,
|
||||
actionType
|
||||
) => {
|
||||
if (!rootComponent || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(rootComponent, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
return path.filter(component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
return def?.actions?.includes(actionType)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a datasource object for a certain data provider component
|
||||
*/
|
||||
|
@ -149,30 +171,6 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
return contextBindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable components. These are form components which allow their
|
||||
* values to be bound to.
|
||||
*/
|
||||
export const getComponentBindings = rootComponent => {
|
||||
if (!rootComponent) {
|
||||
return []
|
||||
}
|
||||
const componentSelector = component => {
|
||||
const type = component._component
|
||||
const definition = store.actions.components.getDefinition(type)
|
||||
return definition?.bindable
|
||||
}
|
||||
const components = findAllMatchingComponents(rootComponent, componentSelector)
|
||||
return components.map(component => {
|
||||
return {
|
||||
type: "instance",
|
||||
providerId: component._id,
|
||||
runtimeBinding: `${component._id}`,
|
||||
readableBinding: `${component._instanceName}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a schema for a datasource object.
|
||||
*/
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Body,
|
||||
DropdownMenu,
|
||||
ModalContent,
|
||||
Spacer,
|
||||
} from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { DataList, Label } from "@budibase/bbui"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset.props,
|
||||
$store.selectedComponentId,
|
||||
"ValidateForm"
|
||||
)
|
||||
$: console.log(actionProviders)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Form</Label>
|
||||
<DataList secondary bind:value={parameters.componentId} label="asd">
|
||||
<option value="" />
|
||||
{#if actionProviders}
|
||||
{#each actionProviders as component}
|
||||
<option value={component._id}>{component._instanceName}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</DataList>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(> div) {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-l);
|
||||
}
|
||||
</style>
|
|
@ -3,6 +3,7 @@ import SaveRow from "./SaveRow.svelte"
|
|||
import DeleteRow from "./DeleteRow.svelte"
|
||||
import ExecuteQuery from "./ExecuteQuery.svelte"
|
||||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||
import ValidateForm from "./ValidateForm.svelte"
|
||||
|
||||
// defines what actions are available, when adding a new one
|
||||
// the component is the setup panel for the action
|
||||
|
@ -30,4 +31,8 @@ export default [
|
|||
name: "Trigger Automation",
|
||||
component: TriggerAutomation,
|
||||
},
|
||||
{
|
||||
name: "Validate Form",
|
||||
component: ValidateForm,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -4,12 +4,17 @@
|
|||
import Component from "./Component.svelte"
|
||||
import NotificationDisplay from "./NotificationDisplay.svelte"
|
||||
import SDK from "../sdk"
|
||||
import { createDataStore, initialise, screenStore, authStore } from "../store"
|
||||
import {
|
||||
createContextStore,
|
||||
initialise,
|
||||
screenStore,
|
||||
authStore,
|
||||
} from "../store"
|
||||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
setContext("component", writable({}))
|
||||
setContext("data", createDataStore())
|
||||
setContext("context", createContextStore())
|
||||
|
||||
let loaded = false
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import * as ComponentLibrary from "@budibase/standard-components"
|
||||
import Router from "./Router.svelte"
|
||||
import { enrichProps, propsAreSame } from "../utils/componentProps"
|
||||
import { authStore, bindingStore, builderStore } from "../store"
|
||||
import { authStore, builderStore } from "../store"
|
||||
import { hashString } from "../utils/hash"
|
||||
|
||||
export let definition = {}
|
||||
|
@ -22,7 +22,7 @@
|
|||
let latestUpdateTime
|
||||
|
||||
// Get contexts
|
||||
const dataContext = getContext("data")
|
||||
const context = getContext("context")
|
||||
|
||||
// Create component context
|
||||
const componentStore = writable({})
|
||||
|
@ -32,7 +32,7 @@
|
|||
$: constructor = getComponentConstructor(definition._component)
|
||||
$: children = definition._children || []
|
||||
$: id = definition._id
|
||||
$: updateComponentProps(definition, $dataContext, $bindingStore, $authStore)
|
||||
$: updateComponentProps(definition, $context, $authStore)
|
||||
$: styles = definition._styles
|
||||
|
||||
// Update component context
|
||||
|
@ -53,13 +53,13 @@
|
|||
}
|
||||
|
||||
// Enriches any string component props using handlebars
|
||||
const updateComponentProps = async (definition, context, bindings, user) => {
|
||||
const updateComponentProps = async (definition, context, user) => {
|
||||
// Record the timestamp so we can reference it after enrichment
|
||||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich props with context
|
||||
const enrichedProps = await enrichProps(definition, context, bindings, user)
|
||||
const enrichedProps = await enrichProps(definition, context, user)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { createDataStore } from "../store"
|
||||
|
||||
export let row
|
||||
|
||||
// Clone and create new data context for this component tree
|
||||
const dataContext = getContext("data")
|
||||
const component = getContext("component")
|
||||
const newData = createDataStore($dataContext)
|
||||
setContext("data", newData)
|
||||
$: newData.actions.addContext(row, $component.id)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { createContextStore } from "../store"
|
||||
|
||||
export let data
|
||||
export let actions
|
||||
|
||||
// Clone and create new data context for this component tree
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const newContext = createContextStore($context)
|
||||
setContext("context", newContext)
|
||||
|
||||
// Add data context
|
||||
$: {
|
||||
if (data !== undefined) {
|
||||
newContext.actions.provideData($component.id, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Add actions context
|
||||
$: {
|
||||
actions?.forEach(({ type, callback }) => {
|
||||
newContext.actions.provideAction($component.id, type, callback)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,3 +1,7 @@
|
|||
export const TableNames = {
|
||||
USERS: "ta_users",
|
||||
}
|
||||
|
||||
export const ActionTypes = {
|
||||
ValidateForm: "ValidateForm",
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import {
|
|||
notificationStore,
|
||||
routeStore,
|
||||
screenStore,
|
||||
bindingStore,
|
||||
builderStore,
|
||||
} from "./store"
|
||||
import { styleable } from "./utils/styleable"
|
||||
import { linkable } from "./utils/linkable"
|
||||
import DataProvider from "./components/DataProvider.svelte"
|
||||
import Provider from "./components/Provider.svelte"
|
||||
import { ActionTypes } from "./constants"
|
||||
|
||||
export default {
|
||||
API,
|
||||
|
@ -20,6 +20,6 @@ export default {
|
|||
builderStore,
|
||||
styleable,
|
||||
linkable,
|
||||
DataProvider,
|
||||
setBindableValue: bindingStore.actions.setBindableValue,
|
||||
Provider,
|
||||
ActionTypes,
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const createBindingStore = () => {
|
||||
const store = writable({})
|
||||
|
||||
const setBindableValue = (componentId, value) => {
|
||||
store.update(state => {
|
||||
if (componentId) {
|
||||
state[componentId] = value
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { setBindableValue },
|
||||
}
|
||||
}
|
||||
|
||||
export const bindingStore = createBindingStore()
|
|
@ -0,0 +1,34 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export const createContextStore = existingContext => {
|
||||
const store = writable({ ...existingContext })
|
||||
|
||||
// Adds a data context layer to the tree
|
||||
const provideData = (componentId, data) => {
|
||||
store.update(state => {
|
||||
if (componentId) {
|
||||
state[componentId] = data
|
||||
state[`${componentId}_draft`] = cloneDeep(data)
|
||||
state.closestComponentId = componentId
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Adds an action context layer to the tree
|
||||
const provideAction = (componentId, actionType, callback) => {
|
||||
store.update(state => {
|
||||
if (actionType && componentId) {
|
||||
state[`${componentId}_${actionType}`] = callback
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
actions: { provideData, provideAction },
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export const createDataStore = existingContext => {
|
||||
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[`${componentId}_draft`] = cloneDeep(row)
|
||||
state.closestComponentId = componentId
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
actions: { addContext },
|
||||
}
|
||||
}
|
||||
|
||||
export const dataStore = createDataStore()
|
|
@ -3,10 +3,9 @@ export { notificationStore } from "./notification"
|
|||
export { routeStore } from "./routes"
|
||||
export { screenStore } from "./screens"
|
||||
export { builderStore } from "./builder"
|
||||
export { bindingStore } from "./binding"
|
||||
|
||||
// Data stores are layered and duplicated, so it is not a singleton
|
||||
export { createDataStore, dataStore } from "./data"
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
||||
// Initialises an app by loading screens and routes
|
||||
export { initialise } from "./initialise"
|
||||
|
|
|
@ -2,6 +2,7 @@ import { get } from "svelte/store"
|
|||
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
|
||||
import { routeStore, builderStore } from "../store"
|
||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||
import { ActionTypes } from "../constants"
|
||||
|
||||
const saveRowHandler = async (action, context) => {
|
||||
const { fields, providerId } = action.parameters
|
||||
|
@ -59,12 +60,21 @@ const queryExecutionHandler = async (action, context) => {
|
|||
})
|
||||
}
|
||||
|
||||
const validateFormHandler = async (action, context) => {
|
||||
const { componentId } = action.parameters
|
||||
const fn = context[`${componentId}_${ActionTypes.ValidateForm}`]
|
||||
if (fn) {
|
||||
return await fn()
|
||||
}
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
["Navigate To"]: navigationHandler,
|
||||
["Execute Query"]: queryExecutionHandler,
|
||||
["Trigger Automation"]: triggerAutomationHandler,
|
||||
["Validate Form"]: validateFormHandler,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,7 +89,18 @@ 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)
|
||||
try {
|
||||
const result = await handlers[i](actions[i], context)
|
||||
// A handler returning `false` is a flag to stop execution of handlers
|
||||
if (result === false) {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error while executing button handler")
|
||||
console.error(error)
|
||||
// Stop executing on an error
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export const propsAreSame = (a, b) => {
|
|||
* Enriches component props.
|
||||
* Data bindings are enriched, and button actions are enriched.
|
||||
*/
|
||||
export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
||||
export const enrichProps = async (props, context, user) => {
|
||||
// Exclude all private props that start with an underscore
|
||||
let validProps = {}
|
||||
Object.entries(props)
|
||||
|
@ -32,20 +32,22 @@ export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
|||
|
||||
// Create context of all bindings and data contexts
|
||||
// Duplicate the closest context as "data" which the builder requires
|
||||
const context = {
|
||||
...dataContexts,
|
||||
...dataBindings,
|
||||
const totalContext = {
|
||||
...context,
|
||||
user,
|
||||
data: dataContexts[dataContexts.closestComponentId],
|
||||
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
||||
data: context[context.closestComponentId],
|
||||
data_draft: context[`${context.closestComponentId}_draft`],
|
||||
}
|
||||
|
||||
// Enrich all data bindings in top level props
|
||||
let enrichedProps = await enrichDataBindings(validProps, context)
|
||||
let enrichedProps = await enrichDataBindings(validProps, totalContext)
|
||||
|
||||
// Enrich button actions if they exist
|
||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
||||
enrichedProps.onClick = enrichButtonActions(
|
||||
enrichedProps.onClick,
|
||||
totalContext
|
||||
)
|
||||
}
|
||||
|
||||
return enrichedProps
|
||||
|
|
|
@ -1051,11 +1051,12 @@
|
|||
},
|
||||
"form": {
|
||||
"name": "Form",
|
||||
"icon": "ri-file-edit-line",
|
||||
"icon": "ri-file-text-line",
|
||||
"styleable": true,
|
||||
"hasChildren": true,
|
||||
"dataProvider": true,
|
||||
"datasourceSetting": "datasource",
|
||||
"actions": ["ValidateForm"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "datasource",
|
||||
|
@ -1102,7 +1103,7 @@
|
|||
},
|
||||
"fieldgroup": {
|
||||
"name": "Field Group",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-layout-row-line",
|
||||
"styleable": true,
|
||||
"hasChildren": true,
|
||||
"settings": [
|
||||
|
@ -1130,7 +1131,7 @@
|
|||
},
|
||||
"stringfield": {
|
||||
"name": "Text Field",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-t-box-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1174,7 +1175,7 @@
|
|||
},
|
||||
"optionsfield": {
|
||||
"name": "Options Picker",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-file-list-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1197,7 +1198,7 @@
|
|||
},
|
||||
"booleanfield": {
|
||||
"name": "Checkbox",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-checkbox-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1219,7 +1220,7 @@
|
|||
},
|
||||
"longformfield": {
|
||||
"name": "Rich Text",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-file-edit-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1270,7 +1271,7 @@
|
|||
},
|
||||
"attachmentfield": {
|
||||
"name": "Attachment",
|
||||
"icon": "ri-calendar-line",
|
||||
"icon": "ri-image-edit-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1287,7 +1288,7 @@
|
|||
},
|
||||
"relationshipfield": {
|
||||
"name": "Relationship Picker",
|
||||
"icon": "ri-edit-box-line",
|
||||
"icon": "ri-links-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
const { styleable, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const dataContext = getContext("data")
|
||||
const context = getContext("context")
|
||||
|
||||
export let wide = false
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
|||
let fields = []
|
||||
|
||||
// Fetch info about the closest data context
|
||||
$: getFormData($dataContext[$dataContext.closestComponentId])
|
||||
$: getFormData($context[$context.closestComponentId])
|
||||
|
||||
const getFormData = async context => {
|
||||
if (context) {
|
||||
|
@ -32,7 +32,7 @@
|
|||
fields = Object.keys(schema ?? {})
|
||||
|
||||
// Use the draft version for editing
|
||||
row = $dataContext[`${$dataContext.closestComponentId}_draft`]
|
||||
row = $context[`${$context.closestComponentId}_draft`]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { styleable, setBindableValue } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
let value
|
||||
|
||||
function onBlur() {
|
||||
setBindableValue($component.id, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value on:blur={onBlur} use:styleable={$component.styles} />
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API, styleable, DataProvider, builderStore } = getContext("sdk")
|
||||
const { API, styleable, Provider, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let datasource = []
|
||||
|
@ -26,9 +26,9 @@
|
|||
<p>Add some components too</p>
|
||||
{:else}
|
||||
{#each rows as row}
|
||||
<DataProvider {row}>
|
||||
<Provider data={row}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if loaded && $builderStore.inBuilder}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { DataProvider, styleable } = getContext("sdk")
|
||||
const { Provider, styleable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let table
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<DataProvider row={{ tableId: table }}>
|
||||
<Provider data={{ tableId: table }}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { onMount, getContext } from "svelte"
|
||||
|
||||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext(
|
||||
const { API, screenStore, routeStore, Provider, styleable } = getContext(
|
||||
"sdk"
|
||||
)
|
||||
const component = getContext("component")
|
||||
|
@ -39,8 +39,8 @@
|
|||
|
||||
{#if row}
|
||||
<div use:styleable={$component.styles}>
|
||||
<DataProvider {row}>
|
||||
<Provider data={row}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
export let datasource
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
export let datasource
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
// Common props
|
||||
export let title
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
export let datasource
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
export let theme
|
||||
export let size
|
||||
|
||||
const { styleable, API, setBindableValue, DataProvider } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const dataContext = getContext("data")
|
||||
const context = getContext("context")
|
||||
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
||||
|
||||
let loaded = false
|
||||
let schema
|
||||
|
@ -26,12 +26,11 @@
|
|||
|
||||
// Use the closest data context as the initial form values if it matches
|
||||
const initialValues = getInitialValues(
|
||||
$dataContext[$dataContext.closestComponentId]
|
||||
$context[`${$context.closestComponentId}`]
|
||||
)
|
||||
|
||||
// Form state contains observable data about the form
|
||||
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
||||
$: updateFormState(fieldMap)
|
||||
|
||||
// Form API contains functions to control the form
|
||||
const formApi = {
|
||||
|
@ -52,29 +51,65 @@
|
|||
fieldApi: makeFieldApi(field, defaultValue, validate),
|
||||
fieldSchema: schema?.[field] ?? {},
|
||||
}
|
||||
fieldMap = fieldMap
|
||||
return fieldMap[field]
|
||||
},
|
||||
validate: () => {
|
||||
const fields = Object.keys(fieldMap)
|
||||
fields.forEach(field => {
|
||||
const { fieldApi } = fieldMap[field]
|
||||
fieldApi.validate()
|
||||
})
|
||||
return get(formState).valid
|
||||
},
|
||||
}
|
||||
|
||||
// Provide both form API and state to children
|
||||
setContext("form", { formApi, formState })
|
||||
|
||||
// Action context to pass to children
|
||||
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }]
|
||||
|
||||
// Creates an API for a specific field
|
||||
const makeFieldApi = (field, defaultValue, validate) => {
|
||||
return {
|
||||
setValue: value => {
|
||||
const setValue = (value, skipCheck = false) => {
|
||||
const { fieldState } = fieldMap[field]
|
||||
fieldState.update(state => {
|
||||
if (state.value === value) {
|
||||
return state
|
||||
|
||||
// Skip if the value is the same
|
||||
if (!skipCheck && get(fieldState).value === value) {
|
||||
return
|
||||
}
|
||||
state.value = value == null ? defaultValue : value
|
||||
state.error = validate ? validate(state.value) : null
|
||||
state.valid = !state.error
|
||||
|
||||
const newValue = value == null ? defaultValue : value
|
||||
const newError = validate ? validate(newValue) : null
|
||||
const newValid = !newError
|
||||
|
||||
// Update field state
|
||||
fieldState.update(state => {
|
||||
state.value = newValue
|
||||
state.error = newError
|
||||
state.valid = newValid
|
||||
return state
|
||||
})
|
||||
fieldMap = fieldMap
|
||||
|
||||
// Update form state
|
||||
formState.update(state => {
|
||||
state.values = { ...state.values, [field]: newValue }
|
||||
if (newError) {
|
||||
state.errors = { ...state.errors, [field]: newError }
|
||||
} else {
|
||||
delete state.errors[field]
|
||||
}
|
||||
state.valid = Object.keys(state.errors).length === 0
|
||||
return state
|
||||
})
|
||||
|
||||
return newValid
|
||||
}
|
||||
return {
|
||||
setValue,
|
||||
validate: () => {
|
||||
const { fieldState } = fieldMap[field]
|
||||
setValue(get(fieldState).value, true)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -90,21 +125,6 @@
|
|||
})
|
||||
}
|
||||
|
||||
// Updates the form states from the field data
|
||||
const updateFormState = fieldMap => {
|
||||
let values = { ...initialValues }
|
||||
let errors = {}
|
||||
Object.entries(fieldMap).forEach(([field, formField]) => {
|
||||
const fieldState = get(formField.fieldState)
|
||||
values[field] = fieldState.value
|
||||
if (fieldState.error) {
|
||||
errors[field] = fieldState.error
|
||||
}
|
||||
})
|
||||
const valid = Object.keys(errors).length === 0
|
||||
formState.set({ values, errors, valid })
|
||||
}
|
||||
|
||||
// Fetches the form schema from this form's datasource, if one exists
|
||||
const fetchSchema = async () => {
|
||||
if (!datasource?.tableId) {
|
||||
|
@ -128,7 +148,9 @@
|
|||
onMount(fetchSchema)
|
||||
</script>
|
||||
|
||||
<DataProvider row={{ ...$formState.values, tableId: datasource?.tableId }}>
|
||||
<Provider
|
||||
{actions}
|
||||
data={{ ...$formState.values, tableId: datasource?.tableId }}>
|
||||
<div
|
||||
lang="en"
|
||||
dir="ltr"
|
||||
|
@ -138,7 +160,7 @@
|
|||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
const setters = new Map([["number", number]])
|
||||
const SDK = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const dataContext = getContext("data")
|
||||
const { API, styleable } = SDK
|
||||
|
||||
export let datasource = {}
|
||||
|
|
Loading…
Reference in New Issue