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,
Input,
DatePicker,
Multiselect,
} 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"
import { generate } from "shortid"
import Multiselect from "../../../../../../../bbui/src/Form/Core/Multiselect.svelte"
export let rules = []
export let bindings = []
@ -58,14 +58,22 @@
label: "Must not match regex",
value: "notRegex",
},
Contains: {
ContainsRowID: {
label: "Must contain row ID",
value: "contains",
},
NotContains: {
NotContainsRowID: {
label: "Must not contain row ID",
value: "notContains",
},
Contains: {
label: "Must contain one of",
value: "contains",
},
NotContains: {
label: "Must not contain one of",
value: "notContains",
},
}
const ConstraintMap = {
["string"]: [
@ -98,8 +106,8 @@
["attachment"]: [Constraints.Required],
["link"]: [
Constraints.Required,
Constraints.Contains,
Constraints.NotContains,
Constraints.ContainsRowID,
Constraints.NotContainsRowID,
Constraints.MinLength,
Constraints.MaxLength,
],
@ -107,6 +115,8 @@
Constraints.Required,
Constraints.MinLength,
Constraints.MaxLength,
Constraints.Contains,
Constraints.NotContains,
],
}
@ -195,6 +205,7 @@
valueType: "Binding",
type: fieldType,
id: generate(),
value: fieldType == "array" ? [] : null,
},
]
}
@ -280,7 +291,7 @@
disabled={rule.constraint === "required"}
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-->
<Input
bind:value={rule.value}
@ -288,12 +299,22 @@
/>
{:else}
<!-- 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
disabled={rule.constraint === "required"}
bind:value={rule.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"}
<Select
disabled={rule.constraint === "required"}

View File

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

View File

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

View File

@ -94,16 +94,17 @@ exports.createAllSearchIndex = async appId => {
await searchIndex(
appId,
SearchIndexes.ROWS,
function (doc) {
function (doc) {
function idx(input, prev) {
for (let key of Object.keys(input)) {
let idxKey = prev != null ? `${prev}.${key}` : key
idxKey = idxKey.replace(/ /, "_")
if (Array.isArray(input[key])) {
for (val in input[key]) {
index(`${idxKey}.${input[key][v]}`, input[key][v], { store: true });
for (let val in input[key]) {
// 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) {
continue

View File

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

View File

@ -1,52 +1,50 @@
export const getOptions = (
optionsSource,
fieldSchema,
dataProvider,
labelColumn,
valueColumn,
customOptions
) => {
const isArray = fieldSchema?.type === "array"
// Take options from schema
if (optionsSource == null || optionsSource === "schema") {
if (isArray) {
return fieldSchema?.constraints?.inclusion[0] ?? []
}
return fieldSchema?.constraints?.inclusion ?? []
optionsSource,
fieldSchema,
dataProvider,
labelColumn,
valueColumn,
customOptions
) => {
const isArray = fieldSchema?.type === "array"
// Take options from schema
if (optionsSource == null || optionsSource === "schema") {
if (isArray) {
return fieldSchema?.constraints?.inclusion[0] ?? []
}
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 []
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 []
}

View File

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