Add more validation options for array field
This commit is contained in:
parent
12f56744c3
commit
725b9bb6e2
|
@ -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"}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 []
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue