Merge pull request #2470 from Budibase/multi-option-datatype
Multi-Option Data Type
This commit is contained in:
commit
58b966c0cc
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/label/dist/index-vars.css"
|
||||||
|
import Badge from "../Badge/Badge.svelte"
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const displayLimit = 5
|
||||||
|
|
||||||
|
$: badges = value?.slice(0, displayLimit) ?? []
|
||||||
|
$: leftover = (value?.length ?? 0) - badges.length
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each badges as badge}
|
||||||
|
<Badge size="S" grey>{badge}</Badge>
|
||||||
|
{/each}
|
||||||
|
{#if leftover}
|
||||||
|
<div>+{leftover} more</div>
|
||||||
|
{/if}
|
|
@ -4,7 +4,7 @@
|
||||||
import DateTimeRenderer from "./DateTimeRenderer.svelte"
|
import DateTimeRenderer from "./DateTimeRenderer.svelte"
|
||||||
import RelationshipRenderer from "./RelationshipRenderer.svelte"
|
import RelationshipRenderer from "./RelationshipRenderer.svelte"
|
||||||
import AttachmentRenderer from "./AttachmentRenderer.svelte"
|
import AttachmentRenderer from "./AttachmentRenderer.svelte"
|
||||||
|
import ArrayRenderer from "./ArrayRenderer.svelte"
|
||||||
export let row
|
export let row
|
||||||
export let schema
|
export let schema
|
||||||
export let value
|
export let value
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
options: StringRenderer,
|
options: StringRenderer,
|
||||||
number: StringRenderer,
|
number: StringRenderer,
|
||||||
longform: StringRenderer,
|
longform: StringRenderer,
|
||||||
|
array: ArrayRenderer,
|
||||||
}
|
}
|
||||||
$: type = schema?.type ?? "string"
|
$: type = schema?.type ?? "string"
|
||||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||||
|
|
|
@ -131,6 +131,7 @@ const fieldTypeToComponentMap = {
|
||||||
string: "stringfield",
|
string: "stringfield",
|
||||||
number: "numberfield",
|
number: "numberfield",
|
||||||
options: "optionsfield",
|
options: "optionsfield",
|
||||||
|
array: "multifieldselect",
|
||||||
boolean: "booleanfield",
|
boolean: "booleanfield",
|
||||||
longform: "longformfield",
|
longform: "longformfield",
|
||||||
datetime: "datetimefield",
|
datetime: "datetimefield",
|
||||||
|
@ -167,6 +168,13 @@ export function makeDatasourceFormComponents(datasource) {
|
||||||
optionsSource: "schema",
|
optionsSource: "schema",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (fieldType === "array") {
|
||||||
|
component.customProps({
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
optionsSource: "schema",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (fieldType === "link") {
|
if (fieldType === "link") {
|
||||||
let placeholder =
|
let placeholder =
|
||||||
fieldSchema.relationshipType === "one-to-many"
|
fieldSchema.relationshipType === "one-to-many"
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui"
|
import {
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Toggle,
|
||||||
|
TextArea,
|
||||||
|
Multiselect,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
|
@ -27,6 +34,8 @@
|
||||||
<Dropzone {label} bind:value />
|
<Dropzone {label} bind:value />
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
||||||
|
{:else if type === "array"}
|
||||||
|
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
{:else if type === "longform"}
|
{:else if type === "longform"}
|
||||||
|
|
|
@ -262,6 +262,11 @@
|
||||||
label="Options (one per line)"
|
label="Options (one per line)"
|
||||||
bind:values={field.constraints.inclusion}
|
bind:values={field.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
|
{:else if field.type === "array"}
|
||||||
|
<ValuesList
|
||||||
|
label="Options (one per line)"
|
||||||
|
bind:values={field.constraints.inclusion}
|
||||||
|
/>
|
||||||
{:else if field.type === "datetime"}
|
{:else if field.type === "datetime"}
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Earliest"
|
label="Earliest"
|
||||||
|
|
|
@ -113,6 +113,10 @@
|
||||||
label: "Options",
|
label: "Options",
|
||||||
value: FIELDS.OPTIONS.type,
|
value: FIELDS.OPTIONS.type,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Multi-select",
|
||||||
|
value: FIELDS.ARRAY.type,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
"datetimefield",
|
"datetimefield",
|
||||||
"attachmentfield",
|
"attachmentfield",
|
||||||
"relationshipfield",
|
"relationshipfield",
|
||||||
"daterangepicker"
|
"daterangepicker",
|
||||||
|
"multifieldselect"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,6 +59,14 @@
|
||||||
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value
|
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value
|
||||||
onOperatorChange(expression, expression.operator)
|
onOperatorChange(expression, expression.operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if changed to an array, change default value to empty array
|
||||||
|
const idx = filters.findIndex(x => x.field === field)
|
||||||
|
if (expression.type === "array") {
|
||||||
|
filters[idx].value = []
|
||||||
|
} else {
|
||||||
|
filters[idx].value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOperatorChange = (expression, operator) => {
|
const onOperatorChange = (expression, operator) => {
|
||||||
|
@ -74,7 +82,9 @@
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
const getFieldOptions = field => {
|
||||||
const schema = schemaFields.find(x => x.name === field)
|
const schema = schemaFields.find(x => x.name === field)
|
||||||
return schema?.constraints?.inclusion || []
|
const opt = schema?.constraints?.inclusion || []
|
||||||
|
|
||||||
|
return opt
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -122,7 +132,7 @@
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number"].includes(filter.type)}
|
{:else if ["string", "longform", "number"].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if filter.type === "options"}
|
{:else if filter.type === "options" || "array"}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
|
|
|
@ -58,11 +58,11 @@
|
||||||
value: "notRegex",
|
value: "notRegex",
|
||||||
},
|
},
|
||||||
Contains: {
|
Contains: {
|
||||||
label: "Must contain row ID",
|
label: "Must contain",
|
||||||
value: "contains",
|
value: "contains",
|
||||||
},
|
},
|
||||||
NotContains: {
|
NotContains: {
|
||||||
label: "Must not contain row ID",
|
label: "Must not contain",
|
||||||
value: "notContains",
|
value: "notContains",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,13 @@
|
||||||
Constraints.MinLength,
|
Constraints.MinLength,
|
||||||
Constraints.MaxLength,
|
Constraints.MaxLength,
|
||||||
],
|
],
|
||||||
|
["array"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.MinLength,
|
||||||
|
Constraints.MaxLength,
|
||||||
|
Constraints.Contains,
|
||||||
|
Constraints.NotContains,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
||||||
|
@ -109,7 +116,6 @@
|
||||||
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
||||||
$: fieldType = type?.split("/")[1] || "string"
|
$: fieldType = type?.split("/")[1] || "string"
|
||||||
$: constraintOptions = getConstraintsForType(fieldType)
|
$: constraintOptions = getConstraintsForType(fieldType)
|
||||||
|
|
||||||
const getConstraintsForType = type => {
|
const getConstraintsForType = type => {
|
||||||
return ConstraintMap[type]
|
return ConstraintMap[type]
|
||||||
}
|
}
|
||||||
|
@ -190,6 +196,7 @@
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
type: fieldType,
|
type: fieldType,
|
||||||
id: generate(),
|
id: generate(),
|
||||||
|
value: fieldType == "array" ? [] : null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -275,7 +282,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 rule.type !== "array" && ["maxLength", "minLength", "regex", "notRegex", "contains", "notContains"].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}
|
||||||
|
@ -289,6 +296,15 @@
|
||||||
bind:value={rule.value}
|
bind:value={rule.value}
|
||||||
placeholder="Constraint value"
|
placeholder="Constraint value"
|
||||||
/>
|
/>
|
||||||
|
{:else if rule.type === "array"}
|
||||||
|
<Select
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
options={dataSourceSchema.schema[field].constraints
|
||||||
|
.inclusion}
|
||||||
|
getOptionLabel={x => x}
|
||||||
|
getOptionValue={x => x}
|
||||||
|
value={rule.value}
|
||||||
|
/>
|
||||||
{:else if rule.type === "boolean"}
|
{:else if rule.type === "boolean"}
|
||||||
<Select
|
<Select
|
||||||
disabled={rule.constraint === "required"}
|
disabled={rule.constraint === "required"}
|
||||||
|
|
|
@ -43,9 +43,11 @@ const componentMap = {
|
||||||
"field/datetime": FormFieldSelect,
|
"field/datetime": FormFieldSelect,
|
||||||
"field/attachment": FormFieldSelect,
|
"field/attachment": FormFieldSelect,
|
||||||
"field/link": FormFieldSelect,
|
"field/link": FormFieldSelect,
|
||||||
|
"field/array": FormFieldSelect,
|
||||||
// Some validation types are the same as others, so not all types are
|
// Some validation types are the same as others, so not all types are
|
||||||
// explicitly listed here. e.g. options uses string validation
|
// explicitly listed here. e.g. options uses string validation
|
||||||
"validation/string": ValidationEditor,
|
"validation/string": ValidationEditor,
|
||||||
|
"validation/array": ValidationEditor,
|
||||||
"validation/number": ValidationEditor,
|
"validation/number": ValidationEditor,
|
||||||
"validation/boolean": ValidationEditor,
|
"validation/boolean": ValidationEditor,
|
||||||
"validation/datetime": ValidationEditor,
|
"validation/datetime": ValidationEditor,
|
||||||
|
|
|
@ -26,6 +26,15 @@ export const FIELDS = {
|
||||||
inclusion: [],
|
inclusion: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ARRAY: {
|
||||||
|
name: "Multi-select",
|
||||||
|
type: "array",
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: false,
|
||||||
|
inclusion: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
NUMBER: {
|
NUMBER: {
|
||||||
name: "Number",
|
name: "Number",
|
||||||
type: "number",
|
type: "number",
|
||||||
|
|
|
@ -31,6 +31,14 @@ export const OperatorOptions = {
|
||||||
value: "rangeHigh",
|
value: "rangeHigh",
|
||||||
label: "Less than",
|
label: "Less than",
|
||||||
},
|
},
|
||||||
|
Contains: {
|
||||||
|
value: "equal",
|
||||||
|
label: "Contains",
|
||||||
|
},
|
||||||
|
NotContains: {
|
||||||
|
value: "notEqual",
|
||||||
|
label: "Does Not Contain",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getValidOperatorsForType = type => {
|
export const getValidOperatorsForType = type => {
|
||||||
|
@ -55,6 +63,8 @@ export const getValidOperatorsForType = type => {
|
||||||
]
|
]
|
||||||
} else if (type === "options") {
|
} else if (type === "options") {
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "array") {
|
||||||
|
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
|
||||||
} else if (type === "boolean") {
|
} else if (type === "boolean") {
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
} else if (type === "longform") {
|
} else if (type === "longform") {
|
||||||
|
|
|
@ -211,7 +211,6 @@ class QueryBuilder {
|
||||||
if (this.query.notEmpty) {
|
if (this.query.notEmpty) {
|
||||||
build(this.query.notEmpty, key => `${key}:["" TO *]`)
|
build(this.query.notEmpty, key => `${key}:["" TO *]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +252,7 @@ const runQuery = async (url, body) => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
let output = {
|
let output = {
|
||||||
rows: [],
|
rows: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,12 +58,24 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
||||||
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
||||||
// special case for options, need to always allow unselected (null)
|
// special case for options, need to always allow unselected (null)
|
||||||
if (
|
if (
|
||||||
table.schema[fieldName].type === FieldTypes.OPTIONS &&
|
table.schema[fieldName].type ===
|
||||||
|
(FieldTypes.OPTIONS || FieldTypes.ARRAY) &&
|
||||||
constraints.inclusion
|
constraints.inclusion
|
||||||
) {
|
) {
|
||||||
constraints.inclusion.push(null)
|
constraints.inclusion.push(null)
|
||||||
}
|
}
|
||||||
const res = validateJs.single(row[fieldName], constraints)
|
let res
|
||||||
|
|
||||||
|
// Validate.js doesn't seem to handle array
|
||||||
|
if (table.schema[fieldName].type === FieldTypes.ARRAY) {
|
||||||
|
row[fieldName].map(val => {
|
||||||
|
if (!constraints.inclusion.includes(val)) {
|
||||||
|
errors[fieldName] = "Field not in list"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res = validateJs.single(row[fieldName], constraints)
|
||||||
|
}
|
||||||
if (res) errors[fieldName] = res
|
if (res) errors[fieldName] = res
|
||||||
}
|
}
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
|
|
|
@ -12,6 +12,7 @@ exports.FieldTypes = {
|
||||||
OPTIONS: "options",
|
OPTIONS: "options",
|
||||||
NUMBER: "number",
|
NUMBER: "number",
|
||||||
BOOLEAN: "boolean",
|
BOOLEAN: "boolean",
|
||||||
|
ARRAY: "array",
|
||||||
DATETIME: "datetime",
|
DATETIME: "datetime",
|
||||||
ATTACHMENT: "attachment",
|
ATTACHMENT: "attachment",
|
||||||
LINK: "link",
|
LINK: "link",
|
||||||
|
|
|
@ -99,7 +99,14 @@ 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 (key === "_id" || key === "_rev" || input[key] == null) {
|
if (Array.isArray(input[key])) {
|
||||||
|
for (let val in input[key]) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
index(idxKey, input[key][val], {
|
||||||
|
store: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (key === "_id" || key === "_rev" || input[key] == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (typeof input[key] === "string") {
|
if (typeof input[key] === "string") {
|
||||||
|
|
|
@ -29,6 +29,11 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
[null]: null,
|
[null]: null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
|
[FieldTypes.ARRAY]: {
|
||||||
|
"": [],
|
||||||
|
[null]: [],
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
[FieldTypes.STRING]: {
|
[FieldTypes.STRING]: {
|
||||||
"": "",
|
"": "",
|
||||||
[null]: "",
|
[null]: "",
|
||||||
|
|
|
@ -2041,6 +2041,108 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"multifieldselect": {
|
||||||
|
"name": "Multi-select Picker",
|
||||||
|
"icon": "ViewList",
|
||||||
|
"styles": ["size"],
|
||||||
|
"illegalChildren": ["section"],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/array",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder",
|
||||||
|
"placeholder": "Choose an option"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Default value",
|
||||||
|
"key": "defaultValue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Autocomplete",
|
||||||
|
"key": "autocomplete",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Options source",
|
||||||
|
"key": "optionsSource",
|
||||||
|
"defaultValue": "schema",
|
||||||
|
"placeholder": "Pick an options source",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Schema",
|
||||||
|
"value": "schema"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Data provider",
|
||||||
|
"value": "provider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Custom",
|
||||||
|
"value": "custom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dataProvider",
|
||||||
|
"label": "Options Provider",
|
||||||
|
"key": "dataProvider",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "optionsSource",
|
||||||
|
"value": "provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Column",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "optionsSource",
|
||||||
|
"value": "provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Value Column",
|
||||||
|
"key": "valueColumn",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "optionsSource",
|
||||||
|
"value": "provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "options",
|
||||||
|
"key": "customOptions",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "optionsSource",
|
||||||
|
"value": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/array",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"booleanfield": {
|
"booleanfield": {
|
||||||
"name": "Checkbox",
|
"name": "Checkbox",
|
||||||
"icon": "Checkmark",
|
"icon": "Checkmark",
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { CoreMultiselect } from "@budibase/bbui"
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import { getOptions } from "./optionsParser"
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let placeholder
|
||||||
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
|
export let defaultValue
|
||||||
|
export let optionsSource = "schema"
|
||||||
|
export let dataProvider
|
||||||
|
export let labelColumn
|
||||||
|
export let valueColumn
|
||||||
|
export let customOptions
|
||||||
|
export let autocomplete = false
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
let fieldSchema
|
||||||
|
|
||||||
|
$: flatOptions = optionsSource == null || optionsSource === "schema"
|
||||||
|
$: options = getOptions(
|
||||||
|
optionsSource,
|
||||||
|
fieldSchema,
|
||||||
|
dataProvider,
|
||||||
|
labelColumn,
|
||||||
|
valueColumn,
|
||||||
|
customOptions
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{field}
|
||||||
|
{label}
|
||||||
|
{disabled}
|
||||||
|
{validation}
|
||||||
|
{defaultValue}
|
||||||
|
type="array"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
bind:fieldSchema
|
||||||
|
>
|
||||||
|
{#if fieldState}
|
||||||
|
<CoreMultiselect
|
||||||
|
value={fieldState.value || []}
|
||||||
|
error={fieldState.error}
|
||||||
|
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||||
|
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||||
|
id={fieldState.fieldId}
|
||||||
|
disabled={fieldState.disabled}
|
||||||
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
|
{placeholder}
|
||||||
|
{options}
|
||||||
|
{autocomplete}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Field>
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { CoreSelect, CoreRadioGroup } from "@budibase/bbui"
|
import { CoreSelect, CoreRadioGroup } from "@budibase/bbui"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
|
import { getOptions } from "./optionsParser"
|
||||||
export let field
|
export let field
|
||||||
export let label
|
export let label
|
||||||
export let placeholder
|
export let placeholder
|
||||||
|
@ -26,41 +26,9 @@
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
dataProvider,
|
dataProvider,
|
||||||
labelColumn,
|
labelColumn,
|
||||||
valueColumn
|
valueColumn,
|
||||||
|
customOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
const getOptions = (
|
|
||||||
optionsSource,
|
|
||||||
fieldSchema,
|
|
||||||
dataProvider,
|
|
||||||
labelColumn,
|
|
||||||
valueColumn
|
|
||||||
) => {
|
|
||||||
// Take options from schema
|
|
||||||
if (optionsSource == null || optionsSource === "schema") {
|
|
||||||
return fieldSchema?.constraints?.inclusion ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 []
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
|
|
@ -3,6 +3,7 @@ export { default as fieldgroup } from "./FieldGroup.svelte"
|
||||||
export { default as stringfield } from "./StringField.svelte"
|
export { default as stringfield } from "./StringField.svelte"
|
||||||
export { default as numberfield } from "./NumberField.svelte"
|
export { default as numberfield } from "./NumberField.svelte"
|
||||||
export { default as optionsfield } from "./OptionsField.svelte"
|
export { default as optionsfield } from "./OptionsField.svelte"
|
||||||
|
export { default as multifieldselect } from "./MultiFieldSelect.svelte"
|
||||||
export { default as booleanfield } from "./BooleanField.svelte"
|
export { default as booleanfield } from "./BooleanField.svelte"
|
||||||
export { default as longformfield } from "./LongFormField.svelte"
|
export { default as longformfield } from "./LongFormField.svelte"
|
||||||
export { default as datetimefield } from "./DateTimeField.svelte"
|
export { default as datetimefield } from "./DateTimeField.svelte"
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
export const getOptions = (
|
||||||
|
optionsSource,
|
||||||
|
fieldSchema,
|
||||||
|
dataProvider,
|
||||||
|
labelColumn,
|
||||||
|
valueColumn,
|
||||||
|
customOptions
|
||||||
|
) => {
|
||||||
|
const isArray = fieldSchema?.type === "array"
|
||||||
|
// Take options from schema
|
||||||
|
if (optionsSource == null || optionsSource === "schema") {
|
||||||
|
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 []
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ export const createValidatorFromConstraints = (
|
||||||
schemaConstraints.presence?.allowEmpty === false
|
schemaConstraints.presence?.allowEmpty === false
|
||||||
) {
|
) {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: "string",
|
type: schemaConstraints.type == "array" ? "array" : "string",
|
||||||
constraint: "required",
|
constraint: "required",
|
||||||
error: "Required",
|
error: "Required",
|
||||||
})
|
})
|
||||||
|
@ -63,7 +63,10 @@ export const createValidatorFromConstraints = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inclusion constraint
|
// Inclusion constraint
|
||||||
if (exists(schemaConstraints.inclusion)) {
|
if (
|
||||||
|
exists(schemaConstraints.inclusion) &&
|
||||||
|
schemaConstraints.type !== "array"
|
||||||
|
) {
|
||||||
const options = schemaConstraints.inclusion || []
|
const options = schemaConstraints.inclusion || []
|
||||||
rules.push({
|
rules.push({
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -142,7 +145,7 @@ const evaluateRule = (rule, value) => {
|
||||||
* in the same format.
|
* in the same format.
|
||||||
* @param value the value to parse
|
* @param value the value to parse
|
||||||
* @param type the type to parse
|
* @param type the type to parse
|
||||||
* @returns {boolean|string|*|number|null} the parsed value, or null if invalid
|
* @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid
|
||||||
*/
|
*/
|
||||||
const parseType = (value, type) => {
|
const parseType = (value, type) => {
|
||||||
// Treat nulls or empty strings as null
|
// Treat nulls or empty strings as null
|
||||||
|
@ -202,6 +205,13 @@ const parseType = (value, type) => {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "array") {
|
||||||
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const buildLuceneQuery = filter => {
|
||||||
notEqual: {},
|
notEqual: {},
|
||||||
empty: {},
|
empty: {},
|
||||||
notEmpty: {},
|
notEmpty: {},
|
||||||
|
contains: {},
|
||||||
|
notContains: {},
|
||||||
}
|
}
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
filter.forEach(expression => {
|
filter.forEach(expression => {
|
||||||
|
|
Loading…
Reference in New Issue