Add more validation options for array field

This commit is contained in:
Peter Clement 2021-08-25 15:49:04 +01:00
parent 12f56744c3
commit 725b9bb6e2
7 changed files with 131 additions and 79 deletions

View File

@ -9,13 +9,13 @@
Body, Body,
Input, Input,
DatePicker, DatePicker,
Multiselect,
} from "@budibase/bbui" } from "@budibase/bbui"
import { currentAsset, selectedComponent } from "builderStore" import { currentAsset, selectedComponent } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils" import { findClosestMatchingComponent } from "builderStore/storeUtils"
import { getSchemaForDatasource } from "builderStore/dataBinding" import { getSchemaForDatasource } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid" import { generate } from "shortid"
import Multiselect from "../../../../../../../bbui/src/Form/Core/Multiselect.svelte"
export let rules = [] export let rules = []
export let bindings = [] export let bindings = []
@ -58,14 +58,22 @@
label: "Must not match regex", label: "Must not match regex",
value: "notRegex", value: "notRegex",
}, },
Contains: { ContainsRowID: {
label: "Must contain row ID", label: "Must contain row ID",
value: "contains", value: "contains",
}, },
NotContains: { NotContainsRowID: {
label: "Must not contain row ID", label: "Must not contain row ID",
value: "notContains", value: "notContains",
}, },
Contains: {
label: "Must contain one of",
value: "contains",
},
NotContains: {
label: "Must not contain one of",
value: "notContains",
},
} }
const ConstraintMap = { const ConstraintMap = {
["string"]: [ ["string"]: [
@ -98,8 +106,8 @@
["attachment"]: [Constraints.Required], ["attachment"]: [Constraints.Required],
["link"]: [ ["link"]: [
Constraints.Required, Constraints.Required,
Constraints.Contains, Constraints.ContainsRowID,
Constraints.NotContains, Constraints.NotContainsRowID,
Constraints.MinLength, Constraints.MinLength,
Constraints.MaxLength, Constraints.MaxLength,
], ],
@ -107,6 +115,8 @@
Constraints.Required, Constraints.Required,
Constraints.MinLength, Constraints.MinLength,
Constraints.MaxLength, Constraints.MaxLength,
Constraints.Contains,
Constraints.NotContains,
], ],
} }
@ -195,6 +205,7 @@
valueType: "Binding", valueType: "Binding",
type: fieldType, type: fieldType,
id: generate(), id: generate(),
value: fieldType == "array" ? [] : null,
}, },
] ]
} }
@ -280,7 +291,7 @@
disabled={rule.constraint === "required"} disabled={rule.constraint === "required"}
on:change={e => (rule.value = e.detail)} on:change={e => (rule.value = e.detail)}
/> />
{:else if ["maxLength", "minLength", "regex", "notRegex", "contains", "notContains"].includes(rule.constraint)} {:else if ["maxLength", "minLength", "regex", "notRegex", "containsRowID", "notContainsRowID"].includes(rule.constraint)}
<!-- Certain constraints always need string values--> <!-- Certain constraints always need string values-->
<Input <Input
bind:value={rule.value} bind:value={rule.value}
@ -288,12 +299,22 @@
/> />
{:else} {:else}
<!-- Otherwise we render a component based on the type --> <!-- Otherwise we render a component based on the type -->
{#if ["string", "number", "options", "longform", "array"].includes(rule.type)} {#if ["string", "number", "options", "longform"].includes(rule.type)}
<Input <Input
disabled={rule.constraint === "required"} disabled={rule.constraint === "required"}
bind:value={rule.value} bind:value={rule.value}
placeholder="Constraint value" placeholder="Constraint value"
/> />
{:else if rule.type === "array" && ["contains", "notContains"].includes(rule.constraint)}
<Multiselect
disabled={rule.constraint === "required"}
options={dataSourceSchema.schema[field].constraints
.inclusion[0]}
getOptionLabel={x => x}
getOptionValue={x => x}
on:change={e => (rule.value = e.detail)}
bind:value={rule.value}
/>
{:else if rule.type === "boolean"} {:else if rule.type === "boolean"}
<Select <Select
disabled={rule.constraint === "required"} disabled={rule.constraint === "required"}

View File

@ -38,7 +38,7 @@ export const OperatorOptions = {
NotContains: { NotContains: {
value: "notContains", value: "notContains",
label: "Does Not Contain", label: "Does Not Contain",
} },
} }
export const getValidOperatorsForType = type => { export const getValidOperatorsForType = type => {

View File

@ -230,11 +230,14 @@ class QueryBuilder {
return null return null
} }
let opts = [] let opts = []
value.forEach(val => opts.push(`${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`)) value.forEach(val =>
const joined = opts.join(' AND ') opts.push(
`${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`
)
)
const joined = opts.join(" AND ")
return joined return joined
}) })
} }
if (this.query.notContains) { if (this.query.notContains) {
@ -243,14 +246,16 @@ class QueryBuilder {
return null return null
} }
let opts = [] let opts = []
value.forEach(val => opts.push(`!${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`)) value.forEach(val =>
const joined = opts.join(' AND ') opts.push(
`!${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`
)
)
const joined = opts.join(" AND ")
return joined return joined
}) })
} }
return query return query
} }

View File

@ -99,11 +99,12 @@ exports.createAllSearchIndex = async appId => {
for (let key of Object.keys(input)) { for (let key of Object.keys(input)) {
let idxKey = prev != null ? `${prev}.${key}` : key let idxKey = prev != null ? `${prev}.${key}` : key
idxKey = idxKey.replace(/ /, "_") idxKey = idxKey.replace(/ /, "_")
if (Array.isArray(input[key])) { if (Array.isArray(input[key])) {
for (val in input[key]) { for (let val in input[key]) {
index(`${idxKey}.${input[key][v]}`, input[key][v], { store: true }); // eslint-disable-next-line no-undef
index(`${idxKey}.${input[key][val]}`, input[key][val], {
store: true,
})
} }
} else if (key === "_id" || key === "_rev" || input[key] == null) { } else if (key === "_id" || key === "_rev" || input[key] == null) {
continue continue

View File

@ -1,5 +1,5 @@
<script> <script>
import { Multiselect } from "@budibase/bbui" import { CoreMultiselect } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { getOptions } from "./optionsParser" import { getOptions } from "./optionsParser"
export let field export let field
@ -13,6 +13,7 @@
export let labelColumn export let labelColumn
export let valueColumn export let valueColumn
export let customOptions export let customOptions
export let autocomplete = false
let fieldState let fieldState
let fieldApi let fieldApi
@ -41,7 +42,9 @@
bind:fieldSchema bind:fieldSchema
> >
{#if fieldState} {#if fieldState}
<Multiselect <CoreMultiselect
value={$fieldState.value}
error={$fieldState.error}
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}
id={$fieldState.fieldId} id={$fieldState.fieldId}
@ -49,6 +52,7 @@
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
{placeholder} {placeholder}
{options} {options}
{autocomplete}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -1,52 +1,50 @@
export const getOptions = ( export const getOptions = (
optionsSource, optionsSource,
fieldSchema, fieldSchema,
dataProvider, dataProvider,
labelColumn, labelColumn,
valueColumn, valueColumn,
customOptions customOptions
) => { ) => {
const isArray = fieldSchema?.type === "array" const isArray = fieldSchema?.type === "array"
// Take options from schema // Take options from schema
if (optionsSource == null || optionsSource === "schema") { if (optionsSource == null || optionsSource === "schema") {
if (isArray) { if (isArray) {
return fieldSchema?.constraints?.inclusion[0] ?? [] return fieldSchema?.constraints?.inclusion[0] ?? []
}
return fieldSchema?.constraints?.inclusion ?? []
} }
return fieldSchema?.constraints?.inclusion ?? []
if (optionsSource === "provider" && isArray) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract options from data provider
if (optionsSource === "provider" && valueColumn) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract custom options
if (optionsSource === "custom" && customOptions) {
return customOptions
}
return []
} }
if (optionsSource === "provider" && isArray) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract options from data provider
if (optionsSource === "provider" && valueColumn) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract custom options
if (optionsSource === "custom" && customOptions) {
return customOptions
}
return []
}

View File

@ -63,7 +63,11 @@ export const createValidatorFromConstraints = (
} }
// Inclusion constraint // Inclusion constraint
if (!schemaConstraints.type == "array" ? exists(schemaConstraints.inclusion) : false) { if (
!schemaConstraints.type == "array"
? exists(schemaConstraints.inclusion)
: false
) {
const options = schemaConstraints.inclusion || [] const options = schemaConstraints.inclusion || []
rules.push({ rules.push({
type: "string", type: "string",
@ -73,8 +77,12 @@ export const createValidatorFromConstraints = (
}) })
} }
// Inclusion constraint // Handle the array type but link also returns as an array, so handle via the inclusion check
if (schemaConstraints.type == "array" ? exists(schemaConstraints.inclusion[0]) : false ) { if (
schemaConstraints.type == "array" && schemaConstraints.inclusion
? exists(schemaConstraints.inclusion[0])
: false
) {
const options = schemaConstraints.inclusion[0] || [] const options = schemaConstraints.inclusion[0] || []
rules.push({ rules.push({
type: "array", type: "array",
@ -84,7 +92,6 @@ export const createValidatorFromConstraints = (
}) })
} }
// Date constraint // Date constraint
if (exists(schemaConstraints.datetime?.earliest)) { if (exists(schemaConstraints.datetime?.earliest)) {
const limit = schemaConstraints.datetime.earliest const limit = schemaConstraints.datetime.earliest
@ -219,7 +226,7 @@ const parseType = (value, type) => {
return null return null
} }
return value return value
} }
// If some unknown type, treat as null to avoid breaking validators // If some unknown type, treat as null to avoid breaking validators
return null return null
@ -258,7 +265,9 @@ const maxValueHandler = (value, rule) => {
// Evaluates an inclusion constraint // Evaluates an inclusion constraint
const inclusionHandler = (value, rule) => { const inclusionHandler = (value, rule) => {
return value == null || rule.type == "array" ? rule.value.map(val => val === value) : rule.value.includes(value) return value == null || rule.type == "array"
? rule.value.map(val => val === value)
: rule.value.includes(value)
} }
// Evaluates an equal constraint // Evaluates an equal constraint
@ -288,16 +297,28 @@ const notRegexHandler = (value, rule) => {
} }
// Evaluates a contains constraint // Evaluates a contains constraint
const containsHandler = (value, rule) => { const containsRowIDHandler = (value, rule) => {
const expectedValue = parseType(rule.value, "string") const expectedValue = parseType(rule.value, "string")
return value && value.includes(expectedValue) return value && value.includes(expectedValue)
} }
// Evaluates a not contains constraint // Evaluates a not contains constraint
const notContainsHandler = (value, rule) => { const notContainsRowIDHandler = (value, rule) => {
return !containsHandler(value, rule) return !containsHandler(value, rule)
} }
// Evaluates a contains constraint
const containsHandler = (value, rule) => {
const ruleValue = parseType(rule.value, "array")
return value && value.some(val => ruleValue.includes(val))
}
// Evaluates a not contains constraint
const notContainsHandler = (value, rule) => {
const ruleValue = parseType(rule.value, "array")
return value && !value.some(val => ruleValue.includes(val))
}
/** /**
* Map of constraint types to handlers. * Map of constraint types to handlers.
*/ */
@ -312,6 +333,8 @@ const handlerMap = {
notEqual: notEqualHandler, notEqual: notEqualHandler,
regex: regexHandler, regex: regexHandler,
notRegex: notRegexHandler, notRegex: notRegexHandler,
containsRowID: containsRowIDHandler,
notContainsRowID: notContainsRowIDHandler,
contains: containsHandler, contains: containsHandler,
notContains: notContainsHandler, notContains: notContainsHandler,
} }