Add more validation options for array field
This commit is contained in:
parent
84d85664ef
commit
c681330793
|
@ -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"}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const OperatorOptions = {
|
|||
NotContains: {
|
||||
value: "notContains",
|
||||
label: "Does Not Contain",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const getValidOperatorsForType = type => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 []
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue