From 365c503224d3fd788c1edb8985d3e137bea12015 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 26 Jan 2021 14:40:44 +0000 Subject: [PATCH] Add automatic schema validation to forms and add builder settings for specific field types --- .../builder/src/builderStore/storeUtils.js | 24 +++++- .../design/AppPreview/componentStructure.json | 4 +- .../PropertyControls/FormFieldSelect.svelte | 33 ++++++++ .../PropertyControls/NumberFieldSelect.svelte | 5 ++ .../PropertyControls/OptionSelect.svelte | 13 +++- .../OptionsFieldSelect.svelte | 5 ++ .../PropertyControls/StringFieldSelect.svelte | 5 ++ .../PropertiesPanel/SettingsView.svelte | 6 ++ packages/client/src/store/builder.js | 8 +- packages/client/src/utils/styleable.js | 14 ++-- packages/standard-components/manifest.json | 53 +++++++++++-- .../standard-components/src/forms/Form.svelte | 18 +++-- .../src/forms/NumberField.svelte | 5 ++ .../src/forms/OptionsField.svelte | 1 + .../src/forms/Placeholder.svelte | 17 ++++ .../{Input.svelte => StringField.svelte} | 9 ++- .../standard-components/src/forms/index.js | 4 +- .../src/forms/validation.js | 78 +++++++++++++++++++ 18 files changed, 271 insertions(+), 31 deletions(-) create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte create mode 100644 packages/standard-components/src/forms/NumberField.svelte create mode 100644 packages/standard-components/src/forms/OptionsField.svelte create mode 100644 packages/standard-components/src/forms/Placeholder.svelte rename packages/standard-components/src/forms/{Input.svelte => StringField.svelte} (87%) create mode 100644 packages/standard-components/src/forms/validation.js diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 00f5a209a3..6d0f0beab0 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -59,8 +59,8 @@ export const findComponentPath = (rootComponent, id, path = []) => { } /** - * Recurses through the component tree and finds all components of a certain - * type. + * Recurses through the component tree and finds all components which match + * a certain selector */ export const findAllMatchingComponents = (rootComponent, selector) => { if (!rootComponent || !selector) { @@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => { return components.reverse() } +/** + * Finds the closes parent component which matches certain criteria + */ +export const findClosestMatchingComponent = ( + rootComponent, + componentId, + selector +) => { + if (!selector) { + return null + } + const componentPath = findComponentPath(rootComponent, componentId).reverse() + for (let component of componentPath) { + if (selector(component)) { + return component + } + } + return null +} + /** * Recurses through a component tree evaluating a matching function against * components until a match is found diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index e0881821c7..9193a566b5 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -8,7 +8,9 @@ "icon": "ri-file-edit-line", "children": [ "form", - "input", + "stringfield", + "numberfield", + "optionsfield", "richtext", "datepicker" ] diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte new file mode 100644 index 0000000000..ca40946bcb --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte @@ -0,0 +1,33 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte new file mode 100644 index 0000000000..ce2569cf91 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte index 3ee839fbd0..c464ed84e0 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionSelect.svelte @@ -106,7 +106,9 @@ } $: displayLabel = - selectedOption && selectedOption.label ? selectedOption.label : value || "" + selectedOption && selectedOption.label + ? selectedOption.label + : value || "Choose option"
    +
  • handleClick(null)} + class:selected={value == null || value === ''}> + Choose option +
  • {#if isOptionsObject} {#each options as { value: v, label }}
  • handleClick(v)} class:selected={value === v}> {label}
  • @@ -142,7 +149,7 @@ {#each options as v}
  • handleClick(v)} class:selected={value === v}> {v}
  • diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte new file mode 100644 index 0000000000..526372e3d8 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte new file mode 100644 index 0000000000..27815af91f --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte index 6d3c0d07d3..da6ffc0f11 100644 --- a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte @@ -17,6 +17,9 @@ import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte" import { IconSelect } from "./PropertyControls/IconSelect" import ColorPicker from "./PropertyControls/ColorPicker.svelte" + import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte" + import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte" + import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte" export let componentDefinition = {} export let componentInstance = {} @@ -58,6 +61,9 @@ icon: IconSelect, field: TableViewFieldSelect, multifield: MultiTableViewFieldSelect, + "field/string": StringFieldSelect, + "field/number": NumberFieldSelect, + "field/options": OptionsFieldSelect, } const getControl = type => { diff --git a/packages/client/src/store/builder.js b/packages/client/src/store/builder.js index c7599e5254..295bb6ccc9 100644 --- a/packages/client/src/store/builder.js +++ b/packages/client/src/store/builder.js @@ -13,9 +13,11 @@ const createBuilderStore = () => { const store = writable(initialState) const actions = { selectComponent: id => { - window.dispatchEvent( - new CustomEvent("bb-select-component", { detail: id }) - ) + if (id) { + window.dispatchEvent( + new CustomEvent("bb-select-component", { detail: id }) + ) + } }, } return { diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index 3e934016f6..fbd3ccb053 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -9,7 +9,7 @@ const selectedComponentColor = "#4285f4" */ const buildStyleString = (styleObject, customStyles) => { let str = "" - Object.entries(styleObject).forEach(([style, value]) => { + Object.entries(styleObject || {}).forEach(([style, value]) => { if (style && value != null) { str += `${style}: ${value}; ` } @@ -60,14 +60,14 @@ export const styleable = (node, styles = {}) => { } // Creates event listeners and applies initial styles - const setupStyles = newStyles => { + const setupStyles = (newStyles = {}) => { const componentId = newStyles.id - const selectable = newStyles.allowSelection - const customStyles = newStyles.custom - const normalStyles = newStyles.normal + const selectable = !!newStyles.allowSelection + const customStyles = newStyles.custom || "" + const normalStyles = newStyles.normal || {} const hoverStyles = { ...normalStyles, - ...newStyles.hover, + ...(newStyles.hover || {}), } // Applies a style string to a DOM node, enriching it for the builder @@ -89,7 +89,7 @@ export const styleable = (node, styles = {}) => { // Handler to select a component in the builder when clicking it in the // builder preview selectComponent = event => { - builderStore.actions.selectComponent(newStyles.id) + builderStore.actions.selectComponent(componentId) return blockEvent(event) } diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index e66f467a00..a21a66992b 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1121,7 +1121,7 @@ } ] }, - "input": { + "stringfield": { "name": "Text Field", "description": "A textfield component that allows the user to input text.", "icon": "ri-edit-box-line", @@ -1129,7 +1129,7 @@ "bindable": true, "settings": [ { - "type": "text", + "type": "field/string", "label": "Field", "key": "field" }, @@ -1142,11 +1142,54 @@ "type": "text", "label": "Placeholder", "key": "placeholder" + } + ] + }, + "numberfield": { + "name": "Number Field", + "description": "A textfield component that allows the user to input numbers.", + "icon": "ri-edit-box-line", + "styleable": true, + "bindable": true, + "settings": [ + { + "type": "field/number", + "label": "Field", + "key": "field" }, { - "type": "boolean", - "label": "Required", - "key": "required" + "type": "text", + "label": "Label", + "key": "label" + }, + { + "type": "text", + "label": "Placeholder", + "key": "placeholder" + } + ] + }, + "optionsfield": { + "name": "Options Picker", + "description": "A textfield component that allows the user to input text.", + "icon": "ri-edit-box-line", + "styleable": true, + "bindable": true, + "settings": [ + { + "type": "field/options", + "label": "Field", + "key": "field" + }, + { + "type": "text", + "label": "Label", + "key": "label" + }, + { + "type": "text", + "label": "Placeholder", + "key": "placeholder" } ] } diff --git a/packages/standard-components/src/forms/Form.svelte b/packages/standard-components/src/forms/Form.svelte index 19cc11bc83..a4a2d46c86 100644 --- a/packages/standard-components/src/forms/Form.svelte +++ b/packages/standard-components/src/forms/Form.svelte @@ -1,6 +1,7 @@ + + diff --git a/packages/standard-components/src/forms/OptionsField.svelte b/packages/standard-components/src/forms/OptionsField.svelte new file mode 100644 index 0000000000..f728c685bb --- /dev/null +++ b/packages/standard-components/src/forms/OptionsField.svelte @@ -0,0 +1 @@ +Select diff --git a/packages/standard-components/src/forms/Placeholder.svelte b/packages/standard-components/src/forms/Placeholder.svelte new file mode 100644 index 0000000000..4c088e6038 --- /dev/null +++ b/packages/standard-components/src/forms/Placeholder.svelte @@ -0,0 +1,17 @@ + + +{#if $builderStore.inBuilder} +
    + +
    +{/if} diff --git a/packages/standard-components/src/forms/Input.svelte b/packages/standard-components/src/forms/StringField.svelte similarity index 87% rename from packages/standard-components/src/forms/Input.svelte rename to packages/standard-components/src/forms/StringField.svelte index 30f2790e67..b51b431e24 100644 --- a/packages/standard-components/src/forms/Input.svelte +++ b/packages/standard-components/src/forms/StringField.svelte @@ -2,11 +2,12 @@ import "@spectrum-css/textfield/dist/index-vars.css" import { Label } from "@budibase/bbui" import { getContext } from "svelte" + import Placeholder from "./Placeholder.svelte" export let field export let label export let placeholder - export let validate = value => (value ? null : "Required") + export let type = "text" const { styleable } = getContext("sdk") const component = getContext("component") @@ -23,9 +24,9 @@ {#if !field} -
    Add the Field setting to start using your component!
    + Add the Field setting to start using your component {:else if !fieldState} -
    Form components need to be wrapped in a Form.
    + Form components need to be wrapped in a Form {:else}
    {#if label} @@ -44,7 +45,7 @@ value={$fieldState.value || ''} placeholder={placeholder || ''} on:blur={onBlur} - type="text" + {type} class="spectrum-Textfield-input" />
    {#if $fieldState.error} diff --git a/packages/standard-components/src/forms/index.js b/packages/standard-components/src/forms/index.js index 16e243a802..5f83601526 100644 --- a/packages/standard-components/src/forms/index.js +++ b/packages/standard-components/src/forms/index.js @@ -1,2 +1,4 @@ export { default as form } from "./Form.svelte" -export { default as input } from "./Input.svelte" +export { default as stringfield } from "./StringField.svelte" +export { default as numberfield } from "./NumberField.svelte" +export { default as optionsfield } from "./OptionsField.svelte" diff --git a/packages/standard-components/src/forms/validation.js b/packages/standard-components/src/forms/validation.js new file mode 100644 index 0000000000..f07a0e755f --- /dev/null +++ b/packages/standard-components/src/forms/validation.js @@ -0,0 +1,78 @@ +export const createValidatorFromConstraints = (constraints, field, table) => { + let checks = [] + + if (constraints) { + // Required constraint + if ( + field === table?.primaryDisplay || + constraints.presence?.allowEmpty === false + ) { + checks.push(presenceConstraint) + } + + // String length constraint + if (constraints.length?.maximum) { + const length = constraints.length?.maximum + checks.push(lengthConstraint(length)) + } + + // Min / max number constraint + if (constraints.numericality?.greaterThanOrEqualTo != null) { + const min = constraints.numericality.greaterThanOrEqualTo + checks.push(numericalConstraint(x => x >= min, `Minimum value is ${min}`)) + } + if (constraints.numericality?.lessThanOrEqualTo != null) { + const max = constraints.numericality.lessThanOrEqualTo + checks.push(numericalConstraint(x => x <= max, `Maximum value is ${max}`)) + } + + // Inclusion constraint + if (constraints.inclusion !== undefined) { + const options = constraints.inclusion + checks.push(inclusionConstraint(options)) + } + } + + // Evaluate each constraint + return value => { + for (let check of checks) { + const error = check(value) + if (error) { + return error + } + } + return null + } +} + +const presenceConstraint = value => { + return value == null || value === "" ? "Required" : null +} + +const lengthConstraint = maxLength => value => { + if (value && value.length > maxLength) { + ;`Maximum ${maxLength} characters` + } + return null +} + +const numericalConstraint = (constraint, error) => value => { + if (isNaN(value)) { + return "Must be a number" + } + const number = parseFloat(value) + if (!constraint(number)) { + return error + } + return null +} + +const inclusionConstraint = (options = []) => value => { + if (value == null || value === "") { + return null + } + if (!options.includes(value)) { + return "Invalid value" + } + return null +}