Merge pull request #2441 from Budibase/multistep-forms
Multi-step forms
This commit is contained in:
commit
5ed4ace66b
|
@ -65,6 +65,7 @@
|
||||||
"@spectrum-css/search": "^3.0.2",
|
"@spectrum-css/search": "^3.0.2",
|
||||||
"@spectrum-css/sidenav": "^3.0.2",
|
"@spectrum-css/sidenav": "^3.0.2",
|
||||||
"@spectrum-css/statuslight": "^3.0.2",
|
"@spectrum-css/statuslight": "^3.0.2",
|
||||||
|
"@spectrum-css/stepper": "^3.0.3",
|
||||||
"@spectrum-css/switch": "^1.0.2",
|
"@spectrum-css/switch": "^1.0.2",
|
||||||
"@spectrum-css/table": "^3.0.1",
|
"@spectrum-css/table": "^3.0.1",
|
||||||
"@spectrum-css/tabs": "^3.0.1",
|
"@spectrum-css/tabs": "^3.0.1",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
$: selectedLookupMap = getSelectedLookupMap(value)
|
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||||
|
@ -83,4 +84,5 @@
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
onSelectOption={toggleOption}
|
onSelectOption={toggleOption}
|
||||||
|
{sort}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -25,11 +25,12 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
|
||||||
$: sortedOptions = getSortedOptions(options, getOptionLabel)
|
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
||||||
$: filteredOptions = getFilteredOptions(
|
$: filteredOptions = getFilteredOptions(
|
||||||
sortedOptions,
|
sortedOptions,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
@ -45,10 +46,13 @@
|
||||||
open = true
|
open = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSortedOptions = (options, getLabel) => {
|
const getSortedOptions = (options, getLabel, sort) => {
|
||||||
if (!options?.length || !Array.isArray(options)) {
|
if (!options?.length || !Array.isArray(options)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
if (!sort) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
return options.sort((a, b) => {
|
return options.sort((a, b) => {
|
||||||
const labelA = getLabel(a)
|
const labelA = getLabel(a)
|
||||||
const labelB = getLabel(b)
|
const labelB = getLabel(b)
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let open = false
|
let open = false
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{fieldIcon}
|
{fieldIcon}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
{sort}
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
placeholderOption={placeholder}
|
placeholderOption={placeholder}
|
||||||
isOptionSelected={option => option === value}
|
isOptionSelected={option => option === value}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/stepper/dist/index-vars.css"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let error = null
|
||||||
|
export let id = null
|
||||||
|
export let readonly = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
export let min
|
||||||
|
export let max
|
||||||
|
export let step
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let focus = false
|
||||||
|
|
||||||
|
// We need to keep the field value bound to a different variable in order
|
||||||
|
// to properly handle erroneous values. If we don't do this then it is
|
||||||
|
// possible for the field to show stale text which does not represent the
|
||||||
|
// real value. The reactive statement is to ensure that external changes to
|
||||||
|
// the value prop are reflected.
|
||||||
|
let fieldValue = value
|
||||||
|
$: fieldValue = value
|
||||||
|
|
||||||
|
// Ensure step is always a numeric value defaulting to 1
|
||||||
|
$: step = step == null || isNaN(step) ? 1 : step
|
||||||
|
|
||||||
|
const updateValue = value => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const float = parseFloat(value)
|
||||||
|
value = isNaN(float) ? null : float
|
||||||
|
if (value != null) {
|
||||||
|
if (min != null && value < min) {
|
||||||
|
value = min
|
||||||
|
} else if (max != null && value > max) {
|
||||||
|
value = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch("change", value)
|
||||||
|
fieldValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = event => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = false
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
if (readonly || !updateOnChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValueOnEnter = event => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepUp = () => {
|
||||||
|
if (value == null || isNaN(value)) {
|
||||||
|
updateValue(step)
|
||||||
|
} else {
|
||||||
|
updateValue(value + step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepDown = () => {
|
||||||
|
if (value == null || isNaN(value)) {
|
||||||
|
updateValue(step)
|
||||||
|
} else {
|
||||||
|
updateValue(value - step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="spectrum-Stepper"
|
||||||
|
class:spectrum-Stepper--quiet={quiet}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
class:is-focused={focus}
|
||||||
|
>
|
||||||
|
{#if error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="spectrum-Textfield spectrum-Stepper-textfield">
|
||||||
|
<input
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{id}
|
||||||
|
bind:value={fieldValue}
|
||||||
|
placeholder={placeholder || ""}
|
||||||
|
type="number"
|
||||||
|
class="spectrum-Textfield-input spectrum-Stepper-input"
|
||||||
|
on:click
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
|
on:keyup
|
||||||
|
on:blur={onBlur}
|
||||||
|
on:focus={onFocus}
|
||||||
|
on:input={onInput}
|
||||||
|
on:keyup={updateValueOnEnter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="spectrum-Stepper-buttons">
|
||||||
|
<button
|
||||||
|
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepUp"
|
||||||
|
tabindex="-1"
|
||||||
|
on:click={stepUp}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-ChevronUp75"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Chevron75" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepDown"
|
||||||
|
tabindex="-1"
|
||||||
|
on:click={stepDown}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown75"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Chevron75" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Stepper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.spectrum-Stepper::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,3 +9,4 @@ export { default as CoreSwitch } from "./Switch.svelte"
|
||||||
export { default as CoreSearch } from "./Search.svelte"
|
export { default as CoreSearch } from "./Search.svelte"
|
||||||
export { default as CoreDatePicker } from "./DatePicker.svelte"
|
export { default as CoreDatePicker } from "./DatePicker.svelte"
|
||||||
export { default as CoreDropzone } from "./Dropzone.svelte"
|
export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
export let options = []
|
export let options = []
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
{value}
|
{value}
|
||||||
{options}
|
{options}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
{sort}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
export let getOptionIcon = option => option?.icon
|
export let getOptionIcon = option => option?.icon
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
{options}
|
{options}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
|
{sort}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import Stepper from "./Core/Stepper.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let error = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
export let min = null
|
||||||
|
export let max = null
|
||||||
|
export let step = 1
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<Stepper
|
||||||
|
{updateOnChange}
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{value}
|
||||||
|
{placeholder}
|
||||||
|
{quiet}
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
{step}
|
||||||
|
on:change={onChange}
|
||||||
|
on:click
|
||||||
|
on:input
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:keyup
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<li
|
<li
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
on:click|preventDefault={onClick}
|
on:click|preventDefault={disabled ? null : onClick}
|
||||||
class="spectrum-Menu-item"
|
class="spectrum-Menu-item"
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import "@spectrum-css/icon/dist/index-vars.css"
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
export { default as Input } from "./Form/Input.svelte"
|
export { default as Input } from "./Form/Input.svelte"
|
||||||
|
export { default as Stepper } from "./Form/Stepper.svelte"
|
||||||
export { default as TextArea } from "./Form/TextArea.svelte"
|
export { default as TextArea } from "./Form/TextArea.svelte"
|
||||||
export { default as Select } from "./Form/Select.svelte"
|
export { default as Select } from "./Form/Select.svelte"
|
||||||
export { default as Combobox } from "./Form/Combobox.svelte"
|
export { default as Combobox } from "./Form/Combobox.svelte"
|
||||||
|
|
|
@ -206,6 +206,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
|
||||||
integrity sha512-xodB8g8vGJH20XmUj9ZsPlM1jHrGeRbvmVXkz0q7YvQrYAhim8pP3W+XKKZAletPFAuu8cmUOc6SWn6i4X4z6w==
|
integrity sha512-xodB8g8vGJH20XmUj9ZsPlM1jHrGeRbvmVXkz0q7YvQrYAhim8pP3W+XKKZAletPFAuu8cmUOc6SWn6i4X4z6w==
|
||||||
|
|
||||||
|
"@spectrum-css/stepper@^3.0.3":
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.3.tgz#ae89846886431e3edeee060207b8f81540f73a34"
|
||||||
|
integrity sha512-prAD61ImlOTs9b6PfB3cB08x4lAfxtvnW+RZiTYky0E8GgZdrc/MfCkL5/oqQaIQUtyQv/3Lb7ELAf/0K8QTXw==
|
||||||
|
|
||||||
"@spectrum-css/switch@^1.0.2":
|
"@spectrum-css/switch@^1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44"
|
||||||
|
|
|
@ -120,71 +120,79 @@ const getContextBindings = (asset, componentId) => {
|
||||||
// Create bindings for each data provider
|
// Create bindings for each data provider
|
||||||
dataProviders.forEach(component => {
|
dataProviders.forEach(component => {
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
const contextDefinition = def.context
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
let schema
|
|
||||||
let readablePrefix
|
|
||||||
|
|
||||||
if (contextDefinition.type === "form") {
|
// Create bindings for each context block provided by this data provider
|
||||||
// Forms do not need table schemas
|
contexts.forEach(context => {
|
||||||
// Their schemas are built from their component field names
|
if (!context?.type) {
|
||||||
schema = buildFormSchema(component)
|
|
||||||
readablePrefix = "Fields"
|
|
||||||
} else if (contextDefinition.type === "static") {
|
|
||||||
// Static contexts are fully defined by the components
|
|
||||||
schema = {}
|
|
||||||
const values = contextDefinition.values || []
|
|
||||||
values.forEach(value => {
|
|
||||||
schema[value.key] = { name: value.label, type: "string" }
|
|
||||||
})
|
|
||||||
} else if (contextDefinition.type === "schema") {
|
|
||||||
// Schema contexts are generated dynamically depending on their data
|
|
||||||
const datasource = getDatasourceForProvider(asset, component)
|
|
||||||
if (!datasource) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const info = getSchemaForDatasource(asset, datasource)
|
|
||||||
schema = info.schema
|
|
||||||
readablePrefix = info.table?.name
|
|
||||||
}
|
|
||||||
if (!schema) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(schema).sort()
|
let schema
|
||||||
|
let readablePrefix
|
||||||
|
|
||||||
// Create bindable properties for each schema field
|
if (context.type === "form") {
|
||||||
const safeComponentId = makePropSafe(component._id)
|
// Forms do not need table schemas
|
||||||
keys.forEach(key => {
|
// Their schemas are built from their component field names
|
||||||
const fieldSchema = schema[key]
|
schema = buildFormSchema(component)
|
||||||
|
readablePrefix = "Fields"
|
||||||
// Make safe runtime binding and replace certain bindings with a
|
} else if (context.type === "static") {
|
||||||
// new property to help display components
|
// Static contexts are fully defined by the components
|
||||||
let runtimeBoundKey = key
|
schema = {}
|
||||||
if (fieldSchema.type === "link") {
|
const values = context.values || []
|
||||||
runtimeBoundKey = `${key}_text`
|
values.forEach(value => {
|
||||||
} else if (fieldSchema.type === "attachment") {
|
schema[value.key] = { name: value.label, type: "string" }
|
||||||
runtimeBoundKey = `${key}_first`
|
})
|
||||||
|
} else if (context.type === "schema") {
|
||||||
|
// Schema contexts are generated dynamically depending on their data
|
||||||
|
const datasource = getDatasourceForProvider(asset, component)
|
||||||
|
if (!datasource) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const info = getSchemaForDatasource(asset, datasource)
|
||||||
|
schema = info.schema
|
||||||
|
readablePrefix = info.table?.name
|
||||||
}
|
}
|
||||||
const runtimeBinding = `${safeComponentId}.${makePropSafe(
|
if (!schema) {
|
||||||
runtimeBoundKey
|
return
|
||||||
)}`
|
|
||||||
|
|
||||||
// Optionally use a prefix with readable bindings
|
|
||||||
let readableBinding = component._instanceName
|
|
||||||
if (readablePrefix) {
|
|
||||||
readableBinding += `.${readablePrefix}`
|
|
||||||
}
|
}
|
||||||
readableBinding += `.${fieldSchema.name || key}`
|
|
||||||
|
|
||||||
// Create the binding object
|
const keys = Object.keys(schema).sort()
|
||||||
bindings.push({
|
|
||||||
type: "context",
|
// Create bindable properties for each schema field
|
||||||
runtimeBinding,
|
const safeComponentId = makePropSafe(component._id)
|
||||||
readableBinding,
|
keys.forEach(key => {
|
||||||
// Field schema and provider are required to construct relationship
|
const fieldSchema = schema[key]
|
||||||
// datasource options, based on bindable properties
|
|
||||||
fieldSchema,
|
// Make safe runtime binding and replace certain bindings with a
|
||||||
providerId: component._id,
|
// new property to help display components
|
||||||
|
let runtimeBoundKey = key
|
||||||
|
if (fieldSchema.type === "link") {
|
||||||
|
runtimeBoundKey = `${key}_text`
|
||||||
|
} else if (fieldSchema.type === "attachment") {
|
||||||
|
runtimeBoundKey = `${key}_first`
|
||||||
|
}
|
||||||
|
const runtimeBinding = `${safeComponentId}.${makePropSafe(
|
||||||
|
runtimeBoundKey
|
||||||
|
)}`
|
||||||
|
|
||||||
|
// Optionally use a prefix with readable bindings
|
||||||
|
let readableBinding = component._instanceName
|
||||||
|
if (readablePrefix) {
|
||||||
|
readableBinding += `.${readablePrefix}`
|
||||||
|
}
|
||||||
|
readableBinding += `.${fieldSchema.name || key}`
|
||||||
|
|
||||||
|
// Create the binding object
|
||||||
|
bindings.push({
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding,
|
||||||
|
readableBinding,
|
||||||
|
// Field schema and provider are required to construct relationship
|
||||||
|
// datasource options, based on bindable properties
|
||||||
|
fieldSchema,
|
||||||
|
providerId: component._id,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,7 +20,12 @@ import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
||||||
import api from "../api"
|
import api from "../api"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { findComponentType, findComponentParent } from "../storeUtils"
|
import {
|
||||||
|
findComponentType,
|
||||||
|
findComponentParent,
|
||||||
|
findClosestMatchingComponent,
|
||||||
|
findAllMatchingComponents,
|
||||||
|
} from "../storeUtils"
|
||||||
import { uuid } from "../uuid"
|
import { uuid } from "../uuid"
|
||||||
import { removeBindings } from "../dataBinding"
|
import { removeBindings } from "../dataBinding"
|
||||||
|
|
||||||
|
@ -334,6 +339,18 @@ export const getFrontendStore = () => {
|
||||||
if (definition.hasChildren) {
|
if (definition.hasChildren) {
|
||||||
extras._children = []
|
extras._children = []
|
||||||
}
|
}
|
||||||
|
if (componentName.endsWith("/formstep")) {
|
||||||
|
const parentForm = findClosestMatchingComponent(
|
||||||
|
get(currentAsset).props,
|
||||||
|
get(selectedComponent)._id,
|
||||||
|
component => component._component.endsWith("/form")
|
||||||
|
)
|
||||||
|
const formSteps = findAllMatchingComponents(parentForm, component =>
|
||||||
|
component._component.endsWith("/formstep")
|
||||||
|
)
|
||||||
|
extras.step = formSteps.length + 1
|
||||||
|
extras._instanceName = `Step ${formSteps.length + 1}`
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: uuid(),
|
_id: uuid(),
|
||||||
|
|
|
@ -86,7 +86,7 @@ const createScreen = table => {
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
limit: table.type === "external" ? undefined : 1,
|
limit: 1,
|
||||||
paginate: false,
|
paginate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ const createScreen = table => {
|
||||||
.instanceName("Repeater")
|
.instanceName("Repeater")
|
||||||
.customProps({
|
.customProps({
|
||||||
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
|
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
|
||||||
|
noRowsMessage: "We couldn't find a row to display",
|
||||||
})
|
})
|
||||||
|
|
||||||
const form = makeMainForm()
|
const form = makeMainForm()
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
data-cy="{meta.name}-select"
|
data-cy="{meta.name}-select"
|
||||||
bind:value
|
bind:value
|
||||||
options={meta.constraints.inclusion}
|
options={meta.constraints.inclusion}
|
||||||
|
sort
|
||||||
/>
|
/>
|
||||||
{:else if type === "datetime"}
|
{:else if type === "datetime"}
|
||||||
<DatePicker {label} bind:value />
|
<DatePicker {label} bind:value />
|
||||||
|
|
|
@ -153,6 +153,7 @@
|
||||||
label="Display Column"
|
label="Display Column"
|
||||||
bind:value={primaryDisplay}
|
bind:value={primaryDisplay}
|
||||||
options={fields}
|
options={fields}
|
||||||
|
sort
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
on:change={e => (linkedIds = e.detail ? [e.detail] : [])}
|
on:change={e => (linkedIds = e.detail ? [e.detail] : [])}
|
||||||
{label}
|
{label}
|
||||||
|
sort
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
|
@ -55,5 +56,6 @@
|
||||||
options={rows}
|
options={rows}
|
||||||
getOptionLabel={getPrettyName}
|
getOptionLabel={getPrettyName}
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
|
sort
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<ActionMenu disabled={!item.isCategory}>
|
<ActionMenu disabled={!item.isCategory}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
disabled={isChildAllowed(item, $selectedComponent)}
|
disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)}
|
||||||
quiet
|
quiet
|
||||||
size="S"
|
size="S"
|
||||||
slot="control"
|
slot="control"
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
dataCy={`component-${item.name}`}
|
dataCy={`component-${item.name}`}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
on:click={() => onItemChosen(item)}
|
on:click={() => onItemChosen(item)}
|
||||||
|
disabled={isChildAllowed(item, $selectedComponent)}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
"children": [
|
"children": [
|
||||||
"form",
|
"form",
|
||||||
|
"formstep",
|
||||||
"fieldgroup",
|
"fieldgroup",
|
||||||
"stringfield",
|
"stringfield",
|
||||||
"numberfield",
|
"numberfield",
|
||||||
|
|
|
@ -85,6 +85,8 @@
|
||||||
props={{
|
props={{
|
||||||
options: setting.options || [],
|
options: setting.options || [],
|
||||||
placeholder: setting.placeholder || null,
|
placeholder: setting.placeholder || null,
|
||||||
|
min: setting.min || null,
|
||||||
|
max: setting.max || null,
|
||||||
}}
|
}}
|
||||||
{bindings}
|
{bindings}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
|
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
|
||||||
|
|
||||||
// Select the first action if we delete an action
|
// Select the first action if we delete an action
|
||||||
$: {
|
$: {
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if selectedAction}
|
{#if selectedActionComponent}
|
||||||
<div class="selected-action-container">
|
<div class="selected-action-container">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={selectedActionComponent}
|
this={selectedActionComponent}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label, Stepper } from "@budibase/bbui"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: actionProviders = getActionProviderComponents(
|
||||||
|
$currentAsset,
|
||||||
|
$store.selectedComponentId,
|
||||||
|
"ChangeFormStep"
|
||||||
|
)
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{
|
||||||
|
label: "Next step",
|
||||||
|
value: "next",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Previous step",
|
||||||
|
value: "prev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "First step",
|
||||||
|
value: "first",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Specific step",
|
||||||
|
value: "specific",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "next"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>Form</Label>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={parameters.componentId}
|
||||||
|
options={actionProviders}
|
||||||
|
getOptionLabel={x => x._instanceName}
|
||||||
|
getOptionValue={x => x._id}
|
||||||
|
/>
|
||||||
|
<Label small>Step</Label>
|
||||||
|
<Select bind:value={parameters.type} options={typeOptions} />
|
||||||
|
{#if parameters.type === "specific"}
|
||||||
|
<Label small>Number</Label>
|
||||||
|
<Stepper bind:value={parameters.number} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -29,7 +29,7 @@
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: 60px 1fr;
|
grid-template-columns: 60px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 800px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
max-width: 800px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label, Checkbox } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x._instanceName}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x._id}
|
||||||
/>
|
/>
|
||||||
|
<div />
|
||||||
|
<Checkbox
|
||||||
|
text="Validate only current step"
|
||||||
|
bind:value={parameters.onlyCurrentStep}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -29,7 +34,7 @@
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: 60px 1fr;
|
grid-template-columns: 60px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 800px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ValidateForm from "./ValidateForm.svelte"
|
||||||
import LogOut from "./LogOut.svelte"
|
import LogOut from "./LogOut.svelte"
|
||||||
import ClearForm from "./ClearForm.svelte"
|
import ClearForm from "./ClearForm.svelte"
|
||||||
import CloseScreenModal from "./CloseScreenModal.svelte"
|
import CloseScreenModal from "./CloseScreenModal.svelte"
|
||||||
|
import ChangeFormStep from "./ChangeFormStep.svelte"
|
||||||
|
|
||||||
// Defines which actions are available to configure in the front end.
|
// Defines which actions are available to configure in the front end.
|
||||||
// Unfortunately the "name" property is used as the identifier so please don't
|
// Unfortunately the "name" property is used as the identifier so please don't
|
||||||
|
@ -52,4 +53,8 @@ export default [
|
||||||
name: "Close Screen Modal",
|
name: "Close Screen Modal",
|
||||||
component: CloseScreenModal,
|
component: CloseScreenModal,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Change Form Step",
|
||||||
|
component: ChangeFormStep,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Configure Validation</ActionButton>
|
<ActionButton on:click={drawer.show}>Configure validation</ActionButton>
|
||||||
<Drawer bind:this={drawer} title="Validation Rules">
|
<Drawer bind:this={drawer} title="Validation Rules">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure validation rules for this field.
|
Configure validation rules for this field.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Checkbox, Input, Select } from "@budibase/bbui"
|
import { Checkbox, Input, Select, Stepper } from "@budibase/bbui"
|
||||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||||
import EventsEditor from "./EventsEditor"
|
import EventsEditor from "./EventsEditor"
|
||||||
|
@ -22,7 +22,7 @@ const componentMap = {
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
dataProvider: DataProviderSelect,
|
dataProvider: DataProviderSelect,
|
||||||
boolean: Checkbox,
|
boolean: Checkbox,
|
||||||
number: Input,
|
number: Stepper,
|
||||||
event: EventsEditor,
|
event: EventsEditor,
|
||||||
table: TableSelect,
|
table: TableSelect,
|
||||||
color: ColorPicker,
|
color: ColorPicker,
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
$: selected =
|
$: selected =
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
$builderStore.selectedComponentId === instance._id
|
$builderStore.selectedComponentId === instance._id
|
||||||
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
||||||
$: evaluateConditions(enrichedSettings?._conditions)
|
$: evaluateConditions(enrichedSettings?._conditions)
|
||||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||||
|
@ -74,7 +75,6 @@
|
||||||
styles: { ...instance._styles, id, empty, interactive },
|
styles: { ...instance._styles, id, empty, interactive },
|
||||||
empty,
|
empty,
|
||||||
selected,
|
selected,
|
||||||
props: componentSettings,
|
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -175,13 +175,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key propsHash}
|
{#key propsHash}
|
||||||
{#if constructor && componentSettings && visible}
|
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||||
<div
|
<div
|
||||||
class={`component ${id}`}
|
class={`component ${id}`}
|
||||||
data-type={interactive ? "component" : ""}
|
data-type={interactive ? "component" : ""}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
class:hidden={!visible}
|
|
||||||
>
|
>
|
||||||
<svelte:component this={constructor} {...componentSettings}>
|
<svelte:component this={constructor} {...componentSettings}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
// Sanity limit of 100 active indicators
|
// Sanity limit of 100 active indicators
|
||||||
const children = Array.from(parents)
|
const children = Array.from(parents)
|
||||||
.map(parent => parent?.childNodes?.[0])
|
.map(parent => parent?.childNodes?.[0])
|
||||||
.filter(child => child != null)
|
.filter(node => node?.nodeType === 1)
|
||||||
.slice(0, 100)
|
.slice(0, 100)
|
||||||
|
|
||||||
// If there aren't any nodes then reset
|
// If there aren't any nodes then reset
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const ActionTypes = {
|
||||||
RefreshDatasource: "RefreshDatasource",
|
RefreshDatasource: "RefreshDatasource",
|
||||||
SetDataProviderQuery: "SetDataProviderQuery",
|
SetDataProviderQuery: "SetDataProviderQuery",
|
||||||
ClearForm: "ClearForm",
|
ClearForm: "ClearForm",
|
||||||
|
ChangeFormStep: "ChangeFormStep",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ApiVersion = "1"
|
export const ApiVersion = "1"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import Manifest from "@budibase/standard-components/manifest.json"
|
import Manifest from "@budibase/standard-components/manifest.json"
|
||||||
|
import { findComponentById, findComponentPathById } from "../utils/components"
|
||||||
|
|
||||||
const dispatchEvent = (type, data = {}) => {
|
const dispatchEvent = (type, data = {}) => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
|
@ -9,25 +10,6 @@ const dispatchEvent = (type, data = {}) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const findComponentById = (component, componentId) => {
|
|
||||||
if (!component || !componentId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (component._id === componentId) {
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
if (!component._children?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
for (let child of component._children) {
|
|
||||||
const result = findComponentById(child, componentId)
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const createBuilderStore = () => {
|
const createBuilderStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
inBuilder: false,
|
inBuilder: false,
|
||||||
|
@ -37,9 +19,15 @@ const createBuilderStore = () => {
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
previewId: null,
|
previewId: null,
|
||||||
previewType: null,
|
previewType: null,
|
||||||
|
selectedPath: [],
|
||||||
}
|
}
|
||||||
const writableStore = writable(initialState)
|
const writableStore = writable(initialState)
|
||||||
const derivedStore = derived(writableStore, $state => {
|
const derivedStore = derived(writableStore, $state => {
|
||||||
|
// Avoid any of this logic if we aren't in the builder preview
|
||||||
|
if (!$state.inBuilder) {
|
||||||
|
return $state
|
||||||
|
}
|
||||||
|
|
||||||
// Derive the selected component instance and definition
|
// Derive the selected component instance and definition
|
||||||
const { layout, screen, previewType, selectedComponentId } = $state
|
const { layout, screen, previewType, selectedComponentId } = $state
|
||||||
const asset = previewType === "layout" ? layout : screen
|
const asset = previewType === "layout" ? layout : screen
|
||||||
|
@ -47,10 +35,15 @@ const createBuilderStore = () => {
|
||||||
const prefix = "@budibase/standard-components/"
|
const prefix = "@budibase/standard-components/"
|
||||||
const type = component?._component?.replace(prefix, "")
|
const type = component?._component?.replace(prefix, "")
|
||||||
const definition = type ? Manifest[type] : null
|
const definition = type ? Manifest[type] : null
|
||||||
|
|
||||||
|
// Derive the selected component path
|
||||||
|
const path = findComponentPathById(asset.props, selectedComponentId) || []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...$state,
|
...$state,
|
||||||
selectedComponent: component,
|
selectedComponent: component,
|
||||||
selectedComponentDefinition: definition,
|
selectedComponentDefinition: definition,
|
||||||
|
selectedComponentPath: path?.map(component => component._id),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -67,6 +60,14 @@ const createBuilderStore = () => {
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
|
setSelectedPath: path => {
|
||||||
|
console.log("set to ")
|
||||||
|
console.log(path)
|
||||||
|
writableStore.update(state => {
|
||||||
|
state.selectedPath = path
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...writableStore,
|
...writableStore,
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived, get } from "svelte/store"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import {
|
||||||
|
findComponentPathById,
|
||||||
|
findChildrenByType,
|
||||||
|
findComponentById,
|
||||||
|
} from "../utils/components"
|
||||||
|
|
||||||
const createScreenStore = () => {
|
const createScreenStore = () => {
|
||||||
const store = derived(
|
const store = derived(
|
||||||
|
@ -36,8 +41,39 @@ const createScreenStore = () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Utils to parse component definitions
|
||||||
|
const actions = {
|
||||||
|
findComponentById: componentId => {
|
||||||
|
const { activeScreen, activeLayout } = get(store)
|
||||||
|
let result = findComponentById(activeScreen?.props, componentId)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return findComponentById(activeLayout?.props)
|
||||||
|
},
|
||||||
|
findComponentPathById: componentId => {
|
||||||
|
const { activeScreen, activeLayout } = get(store)
|
||||||
|
let result = findComponentPathById(activeScreen?.props, componentId)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return findComponentPathById(activeLayout?.props)
|
||||||
|
},
|
||||||
|
findChildrenByType: (componentId, type) => {
|
||||||
|
const component = actions.findComponentById(componentId)
|
||||||
|
if (!component || !component._children) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let children = []
|
||||||
|
findChildrenByType(component, type, children)
|
||||||
|
console.log(children)
|
||||||
|
return children
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
|
actions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,15 @@ const queryExecutionHandler = async action => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeActionHandler = async (context, componentId, actionType) => {
|
const executeActionHandler = async (
|
||||||
|
context,
|
||||||
|
componentId,
|
||||||
|
actionType,
|
||||||
|
params
|
||||||
|
) => {
|
||||||
const fn = context[`${componentId}_${actionType}`]
|
const fn = context[`${componentId}_${actionType}`]
|
||||||
if (fn) {
|
if (fn) {
|
||||||
return await fn()
|
return await fn(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +82,8 @@ const validateFormHandler = async (action, context) => {
|
||||||
return await executeActionHandler(
|
return await executeActionHandler(
|
||||||
context,
|
context,
|
||||||
action.parameters.componentId,
|
action.parameters.componentId,
|
||||||
ActionTypes.ValidateForm
|
ActionTypes.ValidateForm,
|
||||||
|
action.parameters.onlyCurrentStep
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +107,15 @@ const clearFormHandler = async (action, context) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeFormStepHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ChangeFormStep,
|
||||||
|
action.parameters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const closeScreenModalHandler = () => {
|
const closeScreenModalHandler = () => {
|
||||||
// Emit this as a window event, so parent screens which are iframing us in
|
// Emit this as a window event, so parent screens which are iframing us in
|
||||||
// can close the modal
|
// can close the modal
|
||||||
|
@ -118,6 +133,7 @@ const handlerMap = {
|
||||||
["Log Out"]: logoutHandler,
|
["Log Out"]: logoutHandler,
|
||||||
["Clear Form"]: clearFormHandler,
|
["Clear Form"]: clearFormHandler,
|
||||||
["Close Screen Modal"]: closeScreenModalHandler,
|
["Close Screen Modal"]: closeScreenModalHandler,
|
||||||
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
|
@ -51,6 +51,12 @@ export const enrichProps = (props, context) => {
|
||||||
condition.settingValue,
|
condition.settingValue,
|
||||||
totalContext
|
totalContext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If there is an onclick function in here then it won't be serialised
|
||||||
|
// properly, and therefore will not be updated properly.
|
||||||
|
// The solution to this is add a rand which will ensure diffs happen
|
||||||
|
// every time.
|
||||||
|
condition.rand = Math.random()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Finds a component instance by ID
|
||||||
|
*/
|
||||||
|
export const findComponentById = (component, componentId) => {
|
||||||
|
if (!component || !componentId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (component._id === componentId) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
if (!component._children?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (let child of component._children) {
|
||||||
|
const result = findComponentById(child, componentId)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the component path to a component
|
||||||
|
*/
|
||||||
|
export const findComponentPathById = (component, componentId, path = []) => {
|
||||||
|
if (!component || !componentId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
path = [...path, component]
|
||||||
|
if (component._id === componentId) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (!component._children?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (let child of component._children) {
|
||||||
|
const result = findComponentPathById(child, componentId, path)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all children instances of a certain component type of a given component
|
||||||
|
*/
|
||||||
|
export const findChildrenByType = (component, type, children = []) => {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (component._component.endsWith(`/${type}`)) {
|
||||||
|
children.push(component)
|
||||||
|
}
|
||||||
|
if (!component._children?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component._children.forEach(child => {
|
||||||
|
findChildrenByType(child, type, children)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1702,10 +1702,11 @@
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section", "form"],
|
||||||
"actions": [
|
"actions": [
|
||||||
"ValidateForm",
|
"ValidateForm",
|
||||||
"ClearForm"
|
"ClearForm",
|
||||||
|
"ChangeFormStep"
|
||||||
],
|
],
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"settings": [
|
"settings": [
|
||||||
|
@ -1732,9 +1733,44 @@
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": [
|
||||||
"type": "form"
|
{
|
||||||
}
|
"type": "static",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Valid",
|
||||||
|
"key": "__valid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Current Step",
|
||||||
|
"key": "__currentStep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Current Step Valid",
|
||||||
|
"key": "__currentStepValid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formstep": {
|
||||||
|
"name": "Form Step",
|
||||||
|
"icon": "AssetsAdded",
|
||||||
|
"hasChildren": true,
|
||||||
|
"illegalChildren": ["section", "form", "form step"],
|
||||||
|
"styles": ["size"],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Step",
|
||||||
|
"key": "step",
|
||||||
|
"defaultValue": 1,
|
||||||
|
"min": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"fieldgroup": {
|
"fieldgroup": {
|
||||||
"name": "Field Group",
|
"name": "Field Group",
|
||||||
|
|
|
@ -42,11 +42,11 @@
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
>
|
>
|
||||||
{#if $fieldState}
|
{#if fieldState}
|
||||||
<CoreDropzone
|
<CoreDropzone
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
fieldApi.setValue(e.detail)
|
fieldApi.setValue(e.detail)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -39,10 +39,10 @@
|
||||||
>
|
>
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreCheckbox
|
<CoreCheckbox
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{size}
|
{size}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -51,11 +51,11 @@
|
||||||
>
|
>
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreDatePicker
|
<CoreDatePicker
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Placeholder from "../Placeholder.svelte"
|
import Placeholder from "../Placeholder.svelte"
|
||||||
import FieldGroupFallback from "./FieldGroupFallback.svelte"
|
import FieldGroupFallback from "./FieldGroupFallback.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
|
|
||||||
export let label
|
export let label
|
||||||
export let field
|
export let field
|
||||||
|
@ -15,7 +15,8 @@
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
const fieldGroupContext = getContext("fieldGroup")
|
const formStepContext = getContext("form-step")
|
||||||
|
const fieldGroupContext = getContext("field-group")
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
@ -26,16 +27,23 @@
|
||||||
field,
|
field,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
disabled,
|
disabled,
|
||||||
validation
|
validation,
|
||||||
|
formStepContext || 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expose field properties to parent component
|
// Update form properties in parent component on every store change
|
||||||
fieldState = formField?.fieldState
|
const unsubscribe = formField?.subscribe(value => {
|
||||||
fieldApi = formField?.fieldApi
|
fieldState = value?.fieldState
|
||||||
fieldSchema = formField?.fieldSchema
|
fieldApi = value?.fieldApi
|
||||||
|
fieldSchema = value?.fieldSchema
|
||||||
|
})
|
||||||
|
onDestroy(() => unsubscribe && unsubscribe())
|
||||||
|
|
||||||
// Keep validation rules up to date
|
// Keep validation rules up to date
|
||||||
$: fieldApi?.updateValidation(validation)
|
$: updateValidation(validation)
|
||||||
|
const updateValidation = validation => {
|
||||||
|
fieldApi?.updateValidation(validation)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract label position from field group context
|
// Extract label position from field group context
|
||||||
$: labelPositionClass =
|
$: labelPositionClass =
|
||||||
|
@ -46,7 +54,7 @@
|
||||||
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
||||||
<label
|
<label
|
||||||
class:hidden={!label}
|
class:hidden={!label}
|
||||||
for={$fieldState?.fieldId}
|
for={fieldState?.fieldId}
|
||||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}
|
||||||
>
|
>
|
||||||
{label || ""}
|
{label || ""}
|
||||||
|
@ -64,8 +72,8 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<slot />
|
<slot />
|
||||||
{#if $fieldState.error}
|
{#if fieldState.error}
|
||||||
<div class="error">{$fieldState.error}</div>
|
<div class="error">{fieldState.error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
setContext("fieldGroup", { labelPosition })
|
setContext("field-group", { labelPosition })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" use:styleable={$component.styles}>
|
<div class="wrapper" use:styleable={$component.styles}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const fieldGroupContext = getContext("fieldGroup")
|
const fieldGroupContext = getContext("field-group")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if fieldGroupContext}
|
{#if fieldGroupContext}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import InnerForm from "./InnerForm.svelte"
|
import InnerForm from "./InnerForm.svelte"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -9,6 +9,11 @@
|
||||||
export let actionType = "Create"
|
export let actionType = "Create"
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
let schema
|
||||||
|
let table
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, context) => {
|
const getInitialValues = (type, dataSource, context) => {
|
||||||
|
@ -32,19 +37,48 @@
|
||||||
return closestContext
|
return closestContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetches the form schema from this form's dataSource, if one exists
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
if (!dataSource?.tableId) {
|
||||||
|
schema = {}
|
||||||
|
table = null
|
||||||
|
} else {
|
||||||
|
table = await API.fetchTableDefinition(dataSource?.tableId)
|
||||||
|
if (table) {
|
||||||
|
if (dataSource?.type === "query") {
|
||||||
|
schema = {}
|
||||||
|
const params = table.parameters || []
|
||||||
|
params.forEach(param => {
|
||||||
|
schema[param.name] = { ...param, type: "string" }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
schema = table.schema || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||||
$: resetKey = JSON.stringify(initialValues)
|
$: resetKey = JSON.stringify(initialValues)
|
||||||
|
|
||||||
|
// Load the form schema on mount
|
||||||
|
onMount(fetchSchema)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key resetKey}
|
{#if loaded}
|
||||||
<InnerForm
|
{#key resetKey}
|
||||||
{dataSource}
|
<InnerForm
|
||||||
{theme}
|
{dataSource}
|
||||||
{size}
|
{theme}
|
||||||
{disabled}
|
{size}
|
||||||
{actionType}
|
{disabled}
|
||||||
{initialValues}
|
{actionType}
|
||||||
>
|
{schema}
|
||||||
<slot />
|
{table}
|
||||||
</InnerForm>
|
{initialValues}
|
||||||
{/key}
|
>
|
||||||
|
<slot />
|
||||||
|
</InnerForm>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import Placeholder from "../Placeholder.svelte"
|
||||||
|
|
||||||
|
export let step = 1
|
||||||
|
|
||||||
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
const formContext = getContext("form")
|
||||||
|
|
||||||
|
// Set form step context so fields know what step they are within
|
||||||
|
setContext("form-step", step || 1)
|
||||||
|
|
||||||
|
$: formState = formContext?.formState
|
||||||
|
$: currentStep = $formState?.currentStep
|
||||||
|
|
||||||
|
// If in the builder preview, show this step if a child is selected
|
||||||
|
$: {
|
||||||
|
if (
|
||||||
|
formContext &&
|
||||||
|
$builderStore.inBuilder &&
|
||||||
|
$builderStore.selectedComponentPath?.includes($component.id)
|
||||||
|
) {
|
||||||
|
formContext.formApi.setStep(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !formContext}
|
||||||
|
<Placeholder text="Form steps need to be wrapped in a form" />
|
||||||
|
{:else if step === currentStep}
|
||||||
|
<div use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -1,36 +1,89 @@
|
||||||
<script>
|
<script>
|
||||||
import { setContext, getContext, onMount } from "svelte"
|
import { setContext, getContext } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { derived, get, writable } from "svelte/store"
|
||||||
import { createValidatorFromConstraints } from "./validation"
|
import { createValidatorFromConstraints } from "./validation"
|
||||||
import { generateID } from "../helpers"
|
import { generateID } from "../helpers"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let initialValues
|
export let initialValues
|
||||||
|
export let schema
|
||||||
|
export let table
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
||||||
|
|
||||||
let loaded = false
|
let fields = []
|
||||||
let schema
|
const currentStep = writable(1)
|
||||||
let table
|
const formState = writable({
|
||||||
let fieldMap = {}
|
values: {},
|
||||||
|
errors: {},
|
||||||
|
valid: true,
|
||||||
|
currentStep: 1,
|
||||||
|
})
|
||||||
|
|
||||||
// Form state contains observable data about the form
|
// Reactive derived stores to derive form state from field array
|
||||||
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
$: values = deriveFieldProperty(fields, f => f.fieldState.value)
|
||||||
|
$: errors = deriveFieldProperty(fields, f => f.fieldState.error)
|
||||||
|
$: valid = !Object.values($errors).some(error => error != null)
|
||||||
|
|
||||||
|
// Derive whether the current form step is valid
|
||||||
|
$: currentStepValid = derived(
|
||||||
|
[currentStep, ...fields],
|
||||||
|
([currentStepValue, ...fieldsValue]) => {
|
||||||
|
return !fieldsValue
|
||||||
|
.filter(f => f.step === currentStepValue)
|
||||||
|
.some(f => f.fieldState.error != null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update form state store from derived stores
|
||||||
|
$: {
|
||||||
|
formState.set({
|
||||||
|
values: $values,
|
||||||
|
errors: $errors,
|
||||||
|
valid,
|
||||||
|
currentStep: $currentStep,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a derived store from an array of fields, comprised of a map of
|
||||||
|
// extracted values from the field array
|
||||||
|
const deriveFieldProperty = (fieldStores, getProp) => {
|
||||||
|
return derived(fieldStores, fieldValues => {
|
||||||
|
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) })
|
||||||
|
return fieldValues.reduce(reducer, {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searches the field array for a certain field
|
||||||
|
const getField = name => {
|
||||||
|
return fields.find(field => get(field).name === name)
|
||||||
|
}
|
||||||
|
|
||||||
// Form API contains functions to control the form
|
|
||||||
const formApi = {
|
const formApi = {
|
||||||
registerField: (
|
registerField: (
|
||||||
field,
|
field,
|
||||||
defaultValue = null,
|
defaultValue = null,
|
||||||
fieldDisabled = false,
|
fieldDisabled = false,
|
||||||
validationRules
|
validationRules,
|
||||||
|
step = 1
|
||||||
) => {
|
) => {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we've already registered this field then wipe any errors and
|
||||||
|
// return the existing field
|
||||||
|
const existingField = getField(field)
|
||||||
|
if (existingField) {
|
||||||
|
existingField.update(state => {
|
||||||
|
state.fieldState.error = null
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
return existingField
|
||||||
|
}
|
||||||
|
|
||||||
// Auto columns are always disabled
|
// Auto columns are always disabled
|
||||||
const isAutoColumn = !!schema?.[field]?.autocolumn
|
const isAutoColumn = !!schema?.[field]?.autocolumn
|
||||||
|
|
||||||
|
@ -43,85 +96,86 @@
|
||||||
table
|
table
|
||||||
)
|
)
|
||||||
|
|
||||||
// Construct field object
|
// Construct field info
|
||||||
fieldMap[field] = {
|
const fieldInfo = writable({
|
||||||
fieldState: makeFieldState(
|
name: field,
|
||||||
field,
|
step: step || 1,
|
||||||
validator,
|
fieldState: {
|
||||||
|
fieldId: `id-${generateID()}`,
|
||||||
|
value: initialValues[field] ?? defaultValue,
|
||||||
|
error: null,
|
||||||
|
disabled: disabled || fieldDisabled || isAutoColumn,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
disabled || fieldDisabled || isAutoColumn
|
validator,
|
||||||
),
|
},
|
||||||
fieldApi: makeFieldApi(field, defaultValue),
|
fieldApi: makeFieldApi(field, defaultValue),
|
||||||
fieldSchema: schema?.[field] ?? {},
|
fieldSchema: schema?.[field] ?? {},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add this field
|
||||||
|
fields = [...fields, fieldInfo]
|
||||||
|
|
||||||
|
return fieldInfo
|
||||||
|
},
|
||||||
|
validate: (onlyCurrentStep = false) => {
|
||||||
|
let valid = true
|
||||||
|
let validationFields = fields
|
||||||
|
|
||||||
|
// Reduce fields to only the current step if required
|
||||||
|
if (onlyCurrentStep) {
|
||||||
|
validationFields = fields.filter(f => get(f).step === get(currentStep))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set initial value
|
// Validate fields and check if any are invalid
|
||||||
const initialValue = get(fieldMap[field].fieldState).value
|
validationFields.forEach(field => {
|
||||||
formState.update(state => ({
|
if (!get(field).fieldApi.validate()) {
|
||||||
...state,
|
valid = false
|
||||||
values: {
|
}
|
||||||
...state.values,
|
|
||||||
[field]: initialValue,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
return fieldMap[field]
|
|
||||||
},
|
|
||||||
validate: () => {
|
|
||||||
const fields = Object.keys(fieldMap)
|
|
||||||
fields.forEach(field => {
|
|
||||||
const { fieldApi } = fieldMap[field]
|
|
||||||
fieldApi.validate()
|
|
||||||
})
|
})
|
||||||
return get(formState).valid
|
return valid
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
const fields = Object.keys(fieldMap)
|
// Clear the form by clearing each individual field
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const { fieldApi } = fieldMap[field]
|
get(field).fieldApi.clearValue()
|
||||||
fieldApi.clearValue()
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
changeStep: ({ type, number }) => {
|
||||||
|
if (type === "next") {
|
||||||
|
currentStep.update(step => step + 1)
|
||||||
|
} else if (type === "prev") {
|
||||||
|
currentStep.update(step => Math.max(1, step - 1))
|
||||||
|
} else if (type === "first") {
|
||||||
|
currentStep.set(1)
|
||||||
|
} else if (type === "specific" && number && !isNaN(number)) {
|
||||||
|
currentStep.set(number)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setStep: step => {
|
||||||
|
if (step) {
|
||||||
|
currentStep.set(step)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide both form API and state to children
|
|
||||||
setContext("form", { formApi, formState, dataSource })
|
|
||||||
|
|
||||||
// Action context to pass to children
|
|
||||||
const actions = [
|
|
||||||
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
|
||||||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Creates an API for a specific field
|
// Creates an API for a specific field
|
||||||
const makeFieldApi = field => {
|
const makeFieldApi = field => {
|
||||||
// Sets the value for a certain field and invokes validation
|
// Sets the value for a certain field and invokes validation
|
||||||
const setValue = (value, skipCheck = false) => {
|
const setValue = (value, skipCheck = false) => {
|
||||||
const { fieldState } = fieldMap[field]
|
const fieldInfo = getField(field)
|
||||||
const { validator } = get(fieldState)
|
const { fieldState } = get(fieldInfo)
|
||||||
|
const { validator } = fieldState
|
||||||
|
|
||||||
// Skip if the value is the same
|
// Skip if the value is the same
|
||||||
if (!skipCheck && get(fieldState).value === value) {
|
if (!skipCheck && fieldState.value === value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update field state
|
// Update field state
|
||||||
const error = validator ? validator(value) : null
|
const error = validator ? validator(value) : null
|
||||||
fieldState.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.value = value
|
state.fieldState.value = value
|
||||||
state.error = error
|
state.fieldState.error = error
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update form state
|
|
||||||
formState.update(state => {
|
|
||||||
state.values = { ...state.values, [field]: value }
|
|
||||||
if (error) {
|
|
||||||
state.errors = { ...state.errors, [field]: error }
|
|
||||||
} else {
|
|
||||||
delete state.errors[field]
|
|
||||||
}
|
|
||||||
state.valid = Object.keys(state.errors).length === 0
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -130,30 +184,23 @@
|
||||||
|
|
||||||
// Clears the value of a certain field back to the initial value
|
// Clears the value of a certain field back to the initial value
|
||||||
const clearValue = () => {
|
const clearValue = () => {
|
||||||
const { fieldState } = fieldMap[field]
|
const fieldInfo = getField(field)
|
||||||
const { defaultValue } = get(fieldState)
|
const { fieldState } = get(fieldInfo)
|
||||||
const newValue = initialValues[field] ?? defaultValue
|
const newValue = initialValues[field] ?? fieldState.defaultValue
|
||||||
|
|
||||||
// Update field state
|
// Update field state
|
||||||
fieldState.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.value = newValue
|
state.fieldState.value = newValue
|
||||||
state.error = null
|
state.fieldState.error = null
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update form state
|
|
||||||
formState.update(state => {
|
|
||||||
state.values = { ...state.values, [field]: newValue }
|
|
||||||
delete state.errors[field]
|
|
||||||
state.valid = Object.keys(state.errors).length === 0
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the validator rules for a certain field
|
// Updates the validator rules for a certain field
|
||||||
const updateValidation = validationRules => {
|
const updateValidation = validationRules => {
|
||||||
const { fieldState } = fieldMap[field]
|
const fieldInfo = getField(field)
|
||||||
const { value, error } = get(fieldState)
|
const { fieldState } = get(fieldInfo)
|
||||||
|
const { value, error } = fieldState
|
||||||
|
|
||||||
// Create new validator
|
// Create new validator
|
||||||
const schemaConstraints = schema?.[field]?.constraints
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
|
@ -165,8 +212,8 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update validator
|
// Update validator
|
||||||
fieldState.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.validator = validator
|
state.fieldState.validator = validator
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -182,58 +229,48 @@
|
||||||
clearValue,
|
clearValue,
|
||||||
updateValidation,
|
updateValidation,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
const { fieldState } = fieldMap[field]
|
// Validate the field by force setting the same value again
|
||||||
setValue(get(fieldState).value, true)
|
const { fieldState } = get(getField(field))
|
||||||
|
return setValue(fieldState.value, true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates observable state data about a specific field
|
// Provide form state and api for full control by children
|
||||||
const makeFieldState = (field, validator, defaultValue, fieldDisabled) => {
|
setContext("form", {
|
||||||
return writable({
|
formState,
|
||||||
field,
|
formApi,
|
||||||
fieldId: `id-${generateID()}`,
|
|
||||||
value: initialValues[field] ?? defaultValue,
|
|
||||||
error: null,
|
|
||||||
disabled: fieldDisabled,
|
|
||||||
defaultValue,
|
|
||||||
validator,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource, if one exists
|
// Data source is needed by attachment fields to be able to upload files
|
||||||
const fetchSchema = async () => {
|
// to the correct table ID
|
||||||
if (!dataSource?.tableId) {
|
dataSource,
|
||||||
schema = {}
|
})
|
||||||
table = null
|
|
||||||
} else {
|
|
||||||
table = await API.fetchTableDefinition(dataSource?.tableId)
|
|
||||||
if (table) {
|
|
||||||
if (dataSource?.type === "query") {
|
|
||||||
schema = {}
|
|
||||||
const params = table.parameters || []
|
|
||||||
params.forEach(param => {
|
|
||||||
schema[param.name] = { ...param, type: "string" }
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
schema = table.schema || {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loaded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the form schema on mount
|
// Provide form step context so that forms without any step components
|
||||||
onMount(fetchSchema)
|
// register their fields to step 1
|
||||||
|
setContext("form-step", 1)
|
||||||
|
|
||||||
|
// Action context to pass to children
|
||||||
|
const actions = [
|
||||||
|
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
||||||
|
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
||||||
|
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create data context to provide
|
||||||
|
$: dataContext = {
|
||||||
|
...initialValues,
|
||||||
|
...$values,
|
||||||
|
|
||||||
|
// These static values are prefixed to avoid clashes with actual columns
|
||||||
|
__valid: valid,
|
||||||
|
__currentStep: $currentStep,
|
||||||
|
__currentStepValid: $currentStepValid,
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider
|
<Provider {actions} data={dataContext}>
|
||||||
{actions}
|
|
||||||
data={{ ...$formState.values, tableId: dataSource?.tableId }}
|
|
||||||
>
|
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
{#if loaded}
|
<slot />
|
||||||
<slot />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
|
@ -25,11 +25,11 @@
|
||||||
>
|
>
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreTextArea
|
<CoreTextArea
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -77,23 +77,24 @@
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
{#if !optionsType || optionsType === "select"}
|
{#if !optionsType || optionsType === "select"}
|
||||||
<CoreSelect
|
<CoreSelect
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
{options}
|
{options}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
sort={true}
|
||||||
/>
|
/>
|
||||||
{:else if optionsType === "radio"}
|
{:else if optionsType === "radio"}
|
||||||
<CoreRadioGroup
|
<CoreRadioGroup
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
{options}
|
{options}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
$: fetchRows(linkedTableId)
|
$: fetchRows(linkedTableId)
|
||||||
$: fetchTable(linkedTableId)
|
$: fetchTable(linkedTableId)
|
||||||
$: singleValue = flatten($fieldState?.value)?.[0]
|
$: singleValue = flatten(fieldState?.value)?.[0]
|
||||||
$: multiValue = flatten($fieldState?.value) ?? []
|
$: multiValue = flatten(fieldState?.value) ?? []
|
||||||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||||
|
|
||||||
const fetchTable = async id => {
|
const fetchTable = async id => {
|
||||||
|
@ -81,12 +81,13 @@
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
value={multiselect ? multiValue : singleValue}
|
value={multiselect ? multiValue : singleValue}
|
||||||
on:change={multiselect ? multiHandler : singleHandler}
|
on:change={multiselect ? multiHandler : singleHandler}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
getOptionLabel={getDisplayName}
|
getOptionLabel={getDisplayName}
|
||||||
getOptionValue={option => option._id}
|
getOptionValue={option => option._id}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
sort={true}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -27,11 +27,11 @@
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreTextField
|
<CoreTextField
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
value={$fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
disabled={$fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={$fieldState.error}
|
error={fieldState.error}
|
||||||
id={$fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{type}
|
{type}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,3 +9,4 @@ export { default as datetimefield } from "./DateTimeField.svelte"
|
||||||
export { default as attachmentfield } from "./AttachmentField.svelte"
|
export { default as attachmentfield } from "./AttachmentField.svelte"
|
||||||
export { default as relationshipfield } from "./RelationshipField.svelte"
|
export { default as relationshipfield } from "./RelationshipField.svelte"
|
||||||
export { default as passwordfield } from "./PasswordField.svelte"
|
export { default as passwordfield } from "./PasswordField.svelte"
|
||||||
|
export { default as formstep } from "./FormStep.svelte"
|
||||||
|
|
Loading…
Reference in New Issue