Add validation drawer and simplify HOC's for different field types

This commit is contained in:
Andrew Kingston 2021-08-06 14:54:00 +01:00
parent 9a21cbb8f8
commit 67be24b524
16 changed files with 390 additions and 57 deletions

View File

@ -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);
}

View File

@ -67,6 +67,7 @@
placeholder: setting.placeholder,
}}
{bindings}
{componentDefinition}
/>
{/if}
{/each}

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="attachment" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="boolean" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="datetime" />

View File

@ -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])

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="longform" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="number" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="options" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="link" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="string" />

View File

@ -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>

View File

@ -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>

View File

@ -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 => {

View File

@ -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"
}
]
},

View File

@ -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 => {