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 { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { backendUiStore, store } from "builderStore"
|
import { backendUiStore, store } from "builderStore"
|
||||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
import { findComponentPath } from "./storeUtils"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// 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.
|
* Gets all bindable data context fields and instance fields.
|
||||||
*/
|
*/
|
||||||
export const getBindableProperties = (rootComponent, componentId) => {
|
export const getBindableProperties = (rootComponent, componentId) => {
|
||||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
return getContextBindings(rootComponent, componentId)
|
||||||
const componentBindings = getComponentBindings(rootComponent)
|
|
||||||
return [...contextBindings, ...componentBindings]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
* Gets a datasource object for a certain data provider component
|
||||||
*/
|
*/
|
||||||
|
@ -149,30 +171,6 @@ export const getContextBindings = (rootComponent, componentId) => {
|
||||||
return contextBindings
|
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.
|
* Gets a schema for a datasource object.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
|
||||||
Button,
|
|
||||||
Body,
|
|
||||||
DropdownMenu,
|
|
||||||
ModalContent,
|
|
||||||
Spacer,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
|
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
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 DeleteRow from "./DeleteRow.svelte"
|
||||||
import ExecuteQuery from "./ExecuteQuery.svelte"
|
import ExecuteQuery from "./ExecuteQuery.svelte"
|
||||||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||||
|
import ValidateForm from "./ValidateForm.svelte"
|
||||||
|
|
||||||
// defines what actions are available, when adding a new one
|
// defines what actions are available, when adding a new one
|
||||||
// the component is the setup panel for the action
|
// the component is the setup panel for the action
|
||||||
|
@ -30,4 +31,8 @@ export default [
|
||||||
name: "Trigger Automation",
|
name: "Trigger Automation",
|
||||||
component: TriggerAutomation,
|
component: TriggerAutomation,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Validate Form",
|
||||||
|
component: ValidateForm,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,12 +4,17 @@
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
import NotificationDisplay from "./NotificationDisplay.svelte"
|
import NotificationDisplay from "./NotificationDisplay.svelte"
|
||||||
import SDK from "../sdk"
|
import SDK from "../sdk"
|
||||||
import { createDataStore, initialise, screenStore, authStore } from "../store"
|
import {
|
||||||
|
createContextStore,
|
||||||
|
initialise,
|
||||||
|
screenStore,
|
||||||
|
authStore,
|
||||||
|
} from "../store"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
setContext("component", writable({}))
|
setContext("component", writable({}))
|
||||||
setContext("data", createDataStore())
|
setContext("context", createContextStore())
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
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 { enrichProps, propsAreSame } from "../utils/componentProps"
|
import { enrichProps, propsAreSame } from "../utils/componentProps"
|
||||||
import { authStore, bindingStore, builderStore } from "../store"
|
import { authStore, builderStore } from "../store"
|
||||||
import { hashString } from "../utils/hash"
|
import { hashString } from "../utils/hash"
|
||||||
|
|
||||||
export let definition = {}
|
export let definition = {}
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
let latestUpdateTime
|
let latestUpdateTime
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const dataContext = getContext("data")
|
const context = getContext("context")
|
||||||
|
|
||||||
// Create component context
|
// Create component context
|
||||||
const componentStore = writable({})
|
const componentStore = writable({})
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
$: constructor = getComponentConstructor(definition._component)
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
$: children = definition._children || []
|
$: children = definition._children || []
|
||||||
$: id = definition._id
|
$: id = definition._id
|
||||||
$: updateComponentProps(definition, $dataContext, $bindingStore, $authStore)
|
$: updateComponentProps(definition, $context, $authStore)
|
||||||
$: styles = definition._styles
|
$: styles = definition._styles
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
|
@ -53,13 +53,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// 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
|
// Record the timestamp so we can reference it after enrichment
|
||||||
latestUpdateTime = Date.now()
|
latestUpdateTime = Date.now()
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
|
||||||
// Enrich props with context
|
// 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
|
// Abandon this update if a newer update has started
|
||||||
if (enrichmentTime !== latestUpdateTime) {
|
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 = {
|
export const TableNames = {
|
||||||
USERS: "ta_users",
|
USERS: "ta_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ActionTypes = {
|
||||||
|
ValidateForm: "ValidateForm",
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {
|
||||||
notificationStore,
|
notificationStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
screenStore,
|
screenStore,
|
||||||
bindingStore,
|
|
||||||
builderStore,
|
builderStore,
|
||||||
} from "./store"
|
} from "./store"
|
||||||
import { styleable } from "./utils/styleable"
|
import { styleable } from "./utils/styleable"
|
||||||
import { linkable } from "./utils/linkable"
|
import { linkable } from "./utils/linkable"
|
||||||
import DataProvider from "./components/DataProvider.svelte"
|
import Provider from "./components/Provider.svelte"
|
||||||
|
import { ActionTypes } from "./constants"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
API,
|
API,
|
||||||
|
@ -20,6 +20,6 @@ export default {
|
||||||
builderStore,
|
builderStore,
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
DataProvider,
|
Provider,
|
||||||
setBindableValue: bindingStore.actions.setBindableValue,
|
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 { routeStore } from "./routes"
|
||||||
export { screenStore } from "./screens"
|
export { screenStore } from "./screens"
|
||||||
export { builderStore } from "./builder"
|
export { builderStore } from "./builder"
|
||||||
export { bindingStore } from "./binding"
|
|
||||||
|
|
||||||
// Data stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createDataStore, dataStore } from "./data"
|
export { createContextStore } from "./context"
|
||||||
|
|
||||||
// Initialises an app by loading screens and routes
|
// Initialises an app by loading screens and routes
|
||||||
export { initialise } from "./initialise"
|
export { initialise } from "./initialise"
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { get } from "svelte/store"
|
||||||
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
|
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
|
||||||
import { routeStore, builderStore } from "../store"
|
import { routeStore, builderStore } from "../store"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||||
|
import { ActionTypes } from "../constants"
|
||||||
|
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
const { fields, providerId } = action.parameters
|
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 = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
["Navigate To"]: navigationHandler,
|
["Navigate To"]: navigationHandler,
|
||||||
["Execute Query"]: queryExecutionHandler,
|
["Execute Query"]: queryExecutionHandler,
|
||||||
["Trigger Automation"]: triggerAutomationHandler,
|
["Trigger Automation"]: triggerAutomationHandler,
|
||||||
|
["Validate Form"]: validateFormHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,7 +89,18 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
return async () => {
|
return async () => {
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
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.
|
* Enriches component props.
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* 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
|
// Exclude all private props that start with an underscore
|
||||||
let validProps = {}
|
let validProps = {}
|
||||||
Object.entries(props)
|
Object.entries(props)
|
||||||
|
@ -32,20 +32,22 @@ export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
||||||
|
|
||||||
// Create context of all bindings and data contexts
|
// Create context of all bindings and data contexts
|
||||||
// Duplicate the closest context as "data" which the builder requires
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
const context = {
|
const totalContext = {
|
||||||
...dataContexts,
|
...context,
|
||||||
...dataBindings,
|
|
||||||
user,
|
user,
|
||||||
data: dataContexts[dataContexts.closestComponentId],
|
data: context[context.closestComponentId],
|
||||||
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
data_draft: context[`${context.closestComponentId}_draft`],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich all data bindings in top level props
|
// 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
|
// Enrich button actions if they exist
|
||||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||||
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
enrichedProps.onClick = enrichButtonActions(
|
||||||
|
enrichedProps.onClick,
|
||||||
|
totalContext
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return enrichedProps
|
return enrichedProps
|
||||||
|
|
|
@ -1051,11 +1051,12 @@
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "ri-file-edit-line",
|
"icon": "ri-file-text-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"dataProvider": true,
|
"dataProvider": true,
|
||||||
"datasourceSetting": "datasource",
|
"datasourceSetting": "datasource",
|
||||||
|
"actions": ["ValidateForm"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
|
@ -1102,7 +1103,7 @@
|
||||||
},
|
},
|
||||||
"fieldgroup": {
|
"fieldgroup": {
|
||||||
"name": "Field Group",
|
"name": "Field Group",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-layout-row-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
|
@ -1130,7 +1131,7 @@
|
||||||
},
|
},
|
||||||
"stringfield": {
|
"stringfield": {
|
||||||
"name": "Text Field",
|
"name": "Text Field",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-t-box-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1174,7 +1175,7 @@
|
||||||
},
|
},
|
||||||
"optionsfield": {
|
"optionsfield": {
|
||||||
"name": "Options Picker",
|
"name": "Options Picker",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-file-list-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1197,7 +1198,7 @@
|
||||||
},
|
},
|
||||||
"booleanfield": {
|
"booleanfield": {
|
||||||
"name": "Checkbox",
|
"name": "Checkbox",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-checkbox-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1219,7 +1220,7 @@
|
||||||
},
|
},
|
||||||
"longformfield": {
|
"longformfield": {
|
||||||
"name": "Rich Text",
|
"name": "Rich Text",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-file-edit-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1270,7 +1271,7 @@
|
||||||
},
|
},
|
||||||
"attachmentfield": {
|
"attachmentfield": {
|
||||||
"name": "Attachment",
|
"name": "Attachment",
|
||||||
"icon": "ri-calendar-line",
|
"icon": "ri-image-edit-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1287,7 +1288,7 @@
|
||||||
},
|
},
|
||||||
"relationshipfield": {
|
"relationshipfield": {
|
||||||
"name": "Relationship Picker",
|
"name": "Relationship Picker",
|
||||||
"icon": "ri-edit-box-line",
|
"icon": "ri-links-line",
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
const { styleable, API } = getContext("sdk")
|
const { styleable, API } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const dataContext = getContext("data")
|
const context = getContext("context")
|
||||||
|
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
let fields = []
|
let fields = []
|
||||||
|
|
||||||
// Fetch info about the closest data context
|
// Fetch info about the closest data context
|
||||||
$: getFormData($dataContext[$dataContext.closestComponentId])
|
$: getFormData($context[$context.closestComponentId])
|
||||||
|
|
||||||
const getFormData = async context => {
|
const getFormData = async context => {
|
||||||
if (context) {
|
if (context) {
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
fields = Object.keys(schema ?? {})
|
fields = Object.keys(schema ?? {})
|
||||||
|
|
||||||
// Use the draft version for editing
|
// Use the draft version for editing
|
||||||
row = $dataContext[`${$dataContext.closestComponentId}_draft`]
|
row = $context[`${$context.closestComponentId}_draft`]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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 { getContext } from "svelte"
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API, styleable, DataProvider, builderStore } = getContext("sdk")
|
const { API, styleable, Provider, builderStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let datasource = []
|
export let datasource = []
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
<p>Add some components too</p>
|
<p>Add some components too</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each rows as row}
|
{#each rows as row}
|
||||||
<DataProvider {row}>
|
<Provider data={row}>
|
||||||
<slot />
|
<slot />
|
||||||
</DataProvider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if loaded && $builderStore.inBuilder}
|
{:else if loaded && $builderStore.inBuilder}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { DataProvider, styleable } = getContext("sdk")
|
const { Provider, styleable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
<DataProvider row={{ tableId: table }}>
|
<Provider data={{ tableId: table }}>
|
||||||
<slot />
|
<slot />
|
||||||
</DataProvider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
|
|
||||||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext(
|
const { API, screenStore, routeStore, Provider, styleable } = getContext(
|
||||||
"sdk"
|
"sdk"
|
||||||
)
|
)
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -39,8 +39,8 @@
|
||||||
|
|
||||||
{#if row}
|
{#if row}
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
<DataProvider {row}>
|
<Provider data={row}>
|
||||||
<slot />
|
<slot />
|
||||||
</DataProvider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
// Common props
|
// Common props
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
export let theme
|
export let theme
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
const { styleable, API, setBindableValue, DataProvider } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const dataContext = getContext("data")
|
const context = getContext("context")
|
||||||
|
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let schema
|
let schema
|
||||||
|
@ -26,12 +26,11 @@
|
||||||
|
|
||||||
// Use the closest data context as the initial form values if it matches
|
// Use the closest data context as the initial form values if it matches
|
||||||
const initialValues = getInitialValues(
|
const initialValues = getInitialValues(
|
||||||
$dataContext[$dataContext.closestComponentId]
|
$context[`${$context.closestComponentId}`]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Form state contains observable data about the form
|
// Form state contains observable data about the form
|
||||||
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
||||||
$: updateFormState(fieldMap)
|
|
||||||
|
|
||||||
// Form API contains functions to control the form
|
// Form API contains functions to control the form
|
||||||
const formApi = {
|
const formApi = {
|
||||||
|
@ -52,29 +51,65 @@
|
||||||
fieldApi: makeFieldApi(field, defaultValue, validate),
|
fieldApi: makeFieldApi(field, defaultValue, validate),
|
||||||
fieldSchema: schema?.[field] ?? {},
|
fieldSchema: schema?.[field] ?? {},
|
||||||
}
|
}
|
||||||
fieldMap = fieldMap
|
|
||||||
return fieldMap[field]
|
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
|
// Provide both form API and state to children
|
||||||
setContext("form", { formApi, formState })
|
setContext("form", { formApi, formState })
|
||||||
|
|
||||||
|
// Action context to pass to children
|
||||||
|
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }]
|
||||||
|
|
||||||
// Creates an API for a specific field
|
// Creates an API for a specific field
|
||||||
const makeFieldApi = (field, defaultValue, validate) => {
|
const makeFieldApi = (field, defaultValue, validate) => {
|
||||||
|
const setValue = (value, skipCheck = false) => {
|
||||||
|
const { fieldState } = fieldMap[field]
|
||||||
|
|
||||||
|
// Skip if the value is the same
|
||||||
|
if (!skipCheck && get(fieldState).value === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
setValue: value => {
|
setValue,
|
||||||
|
validate: () => {
|
||||||
const { fieldState } = fieldMap[field]
|
const { fieldState } = fieldMap[field]
|
||||||
fieldState.update(state => {
|
setValue(get(fieldState).value, true)
|
||||||
if (state.value === value) {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
state.value = value == null ? defaultValue : value
|
|
||||||
state.error = validate ? validate(state.value) : null
|
|
||||||
state.valid = !state.error
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
fieldMap = fieldMap
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
// Fetches the form schema from this form's datasource, if one exists
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
if (!datasource?.tableId) {
|
if (!datasource?.tableId) {
|
||||||
|
@ -128,7 +148,9 @@
|
||||||
onMount(fetchSchema)
|
onMount(fetchSchema)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DataProvider row={{ ...$formState.values, tableId: datasource?.tableId }}>
|
<Provider
|
||||||
|
{actions}
|
||||||
|
data={{ ...$formState.values, tableId: datasource?.tableId }}>
|
||||||
<div
|
<div
|
||||||
lang="en"
|
lang="en"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
@ -138,7 +160,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</DataProvider>
|
</Provider>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
const setters = new Map([["number", number]])
|
const setters = new Map([["number", number]])
|
||||||
const SDK = getContext("sdk")
|
const SDK = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const dataContext = getContext("data")
|
|
||||||
const { API, styleable } = SDK
|
const { API, styleable } = SDK
|
||||||
|
|
||||||
export let datasource = {}
|
export let datasource = {}
|
||||||
|
|
Loading…
Reference in New Issue