Add validation drawer and simplify HOC's for different field types
This commit is contained in:
parent
9a21cbb8f8
commit
67be24b524
|
@ -48,6 +48,9 @@
|
|||
padding-top: var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.gap-XXS {
|
||||
grid-gap: var(--spacing-xs);
|
||||
}
|
||||
.gap-XS {
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
placeholder: setting.placeholder,
|
||||
}}
|
||||
{bindings}
|
||||
{componentDefinition}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="attachment" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="boolean" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="datetime" />
|
|
@ -23,6 +23,7 @@
|
|||
const getOptions = (schema, fieldType) => {
|
||||
let entries = Object.entries(schema ?? {})
|
||||
if (fieldType) {
|
||||
fieldType = fieldType.split("/")[1]
|
||||
entries = entries.filter(entry => entry[1].type === fieldType)
|
||||
}
|
||||
return entries.map(entry => entry[0])
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="longform" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="number" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="options" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="link" />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} on:change type="string" />
|
|
@ -0,0 +1,288 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
DrawerContent,
|
||||
Layout,
|
||||
Select,
|
||||
Heading,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { currentAsset, selectedComponent } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let rules = []
|
||||
export let bindings = []
|
||||
export let type
|
||||
|
||||
const Constraints = {
|
||||
Required: {
|
||||
label: "Required",
|
||||
value: "required",
|
||||
},
|
||||
MaxLength: {
|
||||
label: "Max length",
|
||||
value: "maxLength",
|
||||
},
|
||||
MaxValue: {
|
||||
label: "Max value",
|
||||
value: "maxValue",
|
||||
},
|
||||
MinValue: {
|
||||
label: "Min value",
|
||||
value: "minValue",
|
||||
},
|
||||
Equal: {
|
||||
label: "Must equal",
|
||||
value: "equal",
|
||||
},
|
||||
NotEqual: {
|
||||
label: "Must not equal",
|
||||
value: "notEqual",
|
||||
},
|
||||
Regex: {
|
||||
label: "Must match regex",
|
||||
value: "regex",
|
||||
},
|
||||
NotRegex: {
|
||||
label: "Must not match regex",
|
||||
value: "notRegex",
|
||||
},
|
||||
Contains: {
|
||||
label: "Must contain row ID",
|
||||
value: "contains",
|
||||
},
|
||||
NotContains: {
|
||||
label: "Must not contain row ID",
|
||||
value: "notContains",
|
||||
},
|
||||
}
|
||||
const ConstraintMap = {
|
||||
["string"]: [
|
||||
Constraints.Required,
|
||||
Constraints.MaxLength,
|
||||
Constraints.Equal,
|
||||
Constraints.NotEqual,
|
||||
Constraints.Regex,
|
||||
Constraints.NotRegex,
|
||||
],
|
||||
["number"]: [
|
||||
Constraints.Required,
|
||||
Constraints.MaxValue,
|
||||
Constraints.MinValue,
|
||||
Constraints.Equal,
|
||||
Constraints.NotEqual,
|
||||
],
|
||||
["boolean"]: [
|
||||
Constraints.Required,
|
||||
Constraints.Equal,
|
||||
Constraints.NotEqual,
|
||||
],
|
||||
["datetime"]: [
|
||||
Constraints.Required,
|
||||
Constraints.MaxValue,
|
||||
Constraints.MinValue,
|
||||
Constraints.Equal,
|
||||
Constraints.NotEqual,
|
||||
],
|
||||
["attachment"]: [Constraints.Required],
|
||||
["link"]: [
|
||||
Constraints.Required,
|
||||
Constraints.Equal,
|
||||
Constraints.NotEqual,
|
||||
Constraints.Contains,
|
||||
Constraints.NotContains,
|
||||
],
|
||||
}
|
||||
|
||||
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
||||
$: field = $selectedComponent?.field
|
||||
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
||||
$: constraintOptions = getConstraintsForType(type)
|
||||
|
||||
const getConstraintsForType = type => {
|
||||
return ConstraintMap[type?.split("/")[1] || "string"]
|
||||
}
|
||||
|
||||
const getDataSourceSchema = (asset, component) => {
|
||||
if (!asset || !component) {
|
||||
return null
|
||||
}
|
||||
const formParent = findClosestMatchingComponent(
|
||||
asset.props,
|
||||
component._id,
|
||||
component => component._component.endsWith("/form")
|
||||
)
|
||||
return getSchemaForDatasource(asset, formParent?.dataSource)
|
||||
}
|
||||
|
||||
const parseRulesFromSchema = (field, dataSourceSchema) => {
|
||||
if (!field || !dataSourceSchema) {
|
||||
return []
|
||||
}
|
||||
const fieldSchema = dataSourceSchema.schema?.[field]
|
||||
const constraints = fieldSchema?.constraints
|
||||
if (!constraints) {
|
||||
return []
|
||||
}
|
||||
let rules = []
|
||||
|
||||
// Required constraint
|
||||
if (
|
||||
field === dataSourceSchema?.table?.primaryDisplay ||
|
||||
constraints.presence?.allowEmpty === false
|
||||
) {
|
||||
rules.push({
|
||||
constraint: "required",
|
||||
error: "Required field",
|
||||
})
|
||||
}
|
||||
|
||||
// String length constraint
|
||||
if (exists(constraints.length?.maximum)) {
|
||||
const length = constraints.length.maximum
|
||||
rules.push({
|
||||
constraint: "maxLength",
|
||||
constraintValue: length,
|
||||
error: `Maximum ${length} characters`,
|
||||
})
|
||||
}
|
||||
|
||||
// Min / max number constraint
|
||||
if (exists(constraints.numericality?.greaterThanOrEqualTo)) {
|
||||
const min = constraints.numericality.greaterThanOrEqualTo
|
||||
rules.push({
|
||||
constraint: "minValue",
|
||||
constraintValue: min,
|
||||
error: `Minimum value is ${min}`,
|
||||
})
|
||||
}
|
||||
if (exists(constraints.numericality?.lessThanOrEqualTo)) {
|
||||
const max = constraints.numericality.lessThanOrEqualTo
|
||||
rules.push({
|
||||
constraint: "maxValue",
|
||||
constraintValue: max,
|
||||
error: `Maximum value is ${max}`,
|
||||
})
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
const exists = value => {
|
||||
return value != null && value !== ""
|
||||
}
|
||||
|
||||
const addRule = () => {
|
||||
rules = [...(rules || []), {}]
|
||||
}
|
||||
|
||||
const removeRule = id => {
|
||||
rules = rules.filter(link => link.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding gap="M">
|
||||
<Layout noPadding gap={schemaRules?.length ? "S" : "XS"}>
|
||||
<Heading size="XS">Schema validation rules</Heading>
|
||||
{#if schemaRules?.length}
|
||||
<div class="links">
|
||||
{#each schemaRules as rule}
|
||||
<div class="rule schema">
|
||||
<Select
|
||||
placeholder="Constraint"
|
||||
value={rule.constraint}
|
||||
options={constraintOptions}
|
||||
disabled
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Constraint value"
|
||||
value={rule.constraintValue}
|
||||
{bindings}
|
||||
disabled
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Error message"
|
||||
value={rule.error}
|
||||
{bindings}
|
||||
disabled
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Body size="S">
|
||||
There are no built-in validation rules from the schema.
|
||||
</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="XS">Custom validation rules</Heading>
|
||||
{#if rules?.length}
|
||||
<div class="links">
|
||||
{#each rules as rule}
|
||||
<div class="rule">
|
||||
<Select
|
||||
bind:value={rule.constraint}
|
||||
options={constraintOptions}
|
||||
placeholder="Constraint"
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Constraint value"
|
||||
value={rule.constraintValue}
|
||||
{bindings}
|
||||
disabled={rule.constraint === "required"}
|
||||
on:change={e => (rule.constraintValue = e.detail)}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Error message"
|
||||
value={rule.error}
|
||||
{bindings}
|
||||
on:change={e => (rule.error = e.detail)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeRule(rule.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="button">
|
||||
<Button secondary icon="Add" on:click={addRule}>Add Rule</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.rule {
|
||||
gap: var(--spacing-l);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 1fr 1fr 20px;
|
||||
border-radius: var(--border-radius-s);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ValidationDrawer from "./ValidationDrawer.svelte"
|
||||
|
||||
export let value = []
|
||||
export let bindings = []
|
||||
export let componentDefinition
|
||||
export let type
|
||||
|
||||
let drawer
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const save = () => {
|
||||
dispatch("change", value)
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={drawer.show}>Configure Validation</ActionButton>
|
||||
<Drawer bind:this={drawer} title="Validation Rules">
|
||||
<svelte:fragment slot="description">
|
||||
Configure validation rules for this field.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<ValidationDrawer
|
||||
slot="body"
|
||||
bind:rules={value}
|
||||
{type}
|
||||
{bindings}
|
||||
{componentDefinition}
|
||||
/>
|
||||
</Drawer>
|
|
@ -12,14 +12,8 @@ import SectionSelect from "./SectionSelect.svelte"
|
|||
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
|
||||
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
||||
import URLSelect from "./URLSelect.svelte"
|
||||
import StringFieldSelect from "./StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./OptionsFieldSelect.svelte"
|
||||
import BooleanFieldSelect from "./BooleanFieldSelect.svelte"
|
||||
import LongFormFieldSelect from "./LongFormFieldSelect.svelte"
|
||||
import DateTimeFieldSelect from "./DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./RelationshipFieldSelect.svelte"
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: Input,
|
||||
|
@ -39,14 +33,22 @@ const componentMap = {
|
|||
navigation: NavigationEditor,
|
||||
filter: FilterEditor,
|
||||
url: URLSelect,
|
||||
"field/string": StringFieldSelect,
|
||||
"field/number": NumberFieldSelect,
|
||||
"field/options": OptionsFieldSelect,
|
||||
"field/boolean": BooleanFieldSelect,
|
||||
"field/longform": LongFormFieldSelect,
|
||||
"field/datetime": DateTimeFieldSelect,
|
||||
"field/attachment": AttachmentFieldSelect,
|
||||
"field/link": RelationshipFieldSelect,
|
||||
"field/string": FormFieldSelect,
|
||||
"field/number": FormFieldSelect,
|
||||
"field/options": FormFieldSelect,
|
||||
"field/boolean": FormFieldSelect,
|
||||
"field/longform": FormFieldSelect,
|
||||
"field/datetime": FormFieldSelect,
|
||||
"field/attachment": FormFieldSelect,
|
||||
"field/link": FormFieldSelect,
|
||||
// Some validation types are the same as others, so not all types are
|
||||
// explicitly listed here. e.g. options uses string validation
|
||||
"validation/string": ValidationEditor,
|
||||
"validation/number": ValidationEditor,
|
||||
"validation/boolean": ValidationEditor,
|
||||
"validation/datetime": ValidationEditor,
|
||||
"validation/attachment": ValidationEditor,
|
||||
"validation/link": ValidationEditor,
|
||||
}
|
||||
|
||||
export const getComponentForSettingType = type => {
|
||||
|
|
|
@ -1758,6 +1758,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/string",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1787,6 +1792,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/number",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1816,6 +1826,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/string",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1862,6 +1877,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/string",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1891,6 +1911,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/boolean",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1921,6 +1946,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/string",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1956,6 +1986,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/datetime",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1980,6 +2015,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/attachment",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2009,6 +2049,11 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/link",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -66,7 +66,7 @@ const presenceConstraint = value => {
|
|||
} else {
|
||||
invalid = value == null || value === ""
|
||||
}
|
||||
return invalid ? "Required" : null
|
||||
return invalid ? "Required field" : null
|
||||
}
|
||||
|
||||
const lengthConstraint = maxLength => value => {
|
||||
|
|
Loading…
Reference in New Issue