From 295e36f576bb37af4a5555cdf137e64bd61d0fd0 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 19 Aug 2021 16:54:44 +0100 Subject: [PATCH 01/17] Add ability for user to select 'List' data type for a column --- packages/bbui/src/Table/ArrayRenderer.svelte | 17 +++++++++++++++++ packages/bbui/src/Table/CellRenderer.svelte | 3 ++- .../backend/DataTable/RowFieldControl.svelte | 14 +++++++++++++- .../DataTable/modals/CreateEditColumn.svelte | 13 +++++++++++++ .../TableNavigator/TableDataImport.svelte | 4 ++++ packages/builder/src/constants/backend/index.js | 9 +++++++++ .../server/src/api/controllers/row/utils.js | 16 ++++++++++++++-- packages/server/src/constants/index.js | 1 + packages/server/src/utilities/rowProcessor.js | 5 +++++ 9 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 packages/bbui/src/Table/ArrayRenderer.svelte diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte new file mode 100644 index 0000000000..8281fce78c --- /dev/null +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -0,0 +1,17 @@ + + +{#each badges as badge} + {badge} +{/each} +{#if leftover} +
+{leftover} more
+{/if} diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 2d073b7782..d6a2f3196d 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -4,7 +4,7 @@ import DateTimeRenderer from "./DateTimeRenderer.svelte" import RelationshipRenderer from "./RelationshipRenderer.svelte" import AttachmentRenderer from "./AttachmentRenderer.svelte" - + import ArrayRenderer from "./ArrayRenderer.svelte" export let row export let schema export let value @@ -19,6 +19,7 @@ options: StringRenderer, number: StringRenderer, longform: StringRenderer, + array: ArrayRenderer, } $: type = schema?.type ?? "string" $: customRenderer = customRenderers?.find(x => x.column === schema?.name) diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 078dbf25b2..66879b83cf 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -1,5 +1,12 @@ diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 88152deb0f..2ce88c9648 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -26,6 +26,15 @@ export const FIELDS = { inclusion: [], }, }, + ARRAY: { + name: "List", + type: "array", + constraints: { + type: "array", + presence: false, + inclusion: [], + }, + }, NUMBER: { name: "Number", type: "number", diff --git a/packages/server/src/api/controllers/row/utils.js b/packages/server/src/api/controllers/row/utils.js index 16c48181d1..bdb7e03785 100644 --- a/packages/server/src/api/controllers/row/utils.js +++ b/packages/server/src/api/controllers/row/utils.js @@ -58,12 +58,24 @@ exports.validate = async ({ appId, tableId, row, table }) => { const constraints = cloneDeep(table.schema[fieldName].constraints) // special case for options, need to always allow unselected (null) if ( - table.schema[fieldName].type === FieldTypes.OPTIONS && + table.schema[fieldName].type === + (FieldTypes.OPTIONS || FieldTypes.ARRAY) && constraints.inclusion ) { constraints.inclusion.push(null) } - const res = validateJs.single(row[fieldName], constraints) + let res + + // Validate.js doesn't seem to handle array of array very well + 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 } return { valid: Object.keys(errors).length === 0, errors } diff --git a/packages/server/src/constants/index.js b/packages/server/src/constants/index.js index 703b33deb1..bc7b5b368f 100644 --- a/packages/server/src/constants/index.js +++ b/packages/server/src/constants/index.js @@ -12,6 +12,7 @@ exports.FieldTypes = { OPTIONS: "options", NUMBER: "number", BOOLEAN: "boolean", + ARRAY: "array", DATETIME: "datetime", ATTACHMENT: "attachment", LINK: "link", diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js index c067d4de87..3c43a20409 100644 --- a/packages/server/src/utilities/rowProcessor.js +++ b/packages/server/src/utilities/rowProcessor.js @@ -29,6 +29,11 @@ const TYPE_TRANSFORM_MAP = { [null]: null, [undefined]: undefined, }, + [FieldTypes.ARRAY]: { + "": [], + [null]: [], + [undefined]: undefined, + }, [FieldTypes.STRING]: { "": "", [null]: "", From dbd0d766130f08facdf814322a6b5056b38d5979 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Fri, 20 Aug 2021 15:56:11 +0100 Subject: [PATCH 02/17] add multiselect component as option for form design --- .../screenTemplates/utils/commonComponents.js | 1 + .../backend/DataTable/RowFieldControl.svelte | 1 + .../design/AppPreview/componentStructure.json | 3 +- .../PropertyControls/componentSettings.js | 1 + packages/standard-components/manifest.json | 106 ++++++++++++++++++ .../src/forms/MultiFieldSelect.svelte | 73 ++++++++++++ .../standard-components/src/forms/index.js | 1 + 7 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 packages/standard-components/src/forms/MultiFieldSelect.svelte diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 1a64a8958f..7d40995925 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -131,6 +131,7 @@ const fieldTypeToComponentMap = { string: "stringfield", number: "numberfield", options: "optionsfield", + array: "multifieldselect", boolean: "booleanfield", longform: "longformfield", datetime: "datetimefield", diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 66879b83cf..f2f7a6c687 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -36,6 +36,7 @@ {:else if type === "array"} p.concat(n), [])} /> {:else if type === "link"} diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index c83686158f..881b6baa72 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -20,7 +20,8 @@ "datetimefield", "attachmentfield", "relationshipfield", - "daterangepicker" + "daterangepicker", + "multifieldselect" ] }, { diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js index 64668d05c8..3ca3f7f5c9 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js @@ -43,6 +43,7 @@ const componentMap = { "field/datetime": FormFieldSelect, "field/attachment": FormFieldSelect, "field/link": FormFieldSelect, + "field/array": FormFieldSelect, // Some validation types are the same as others, so not all types are // explicitly listed here. e.g. options uses string validation "validation/string": ValidationEditor, diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 95f41a85ef..8b2bb863a5 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -2005,6 +2005,112 @@ } ] }, + "multifieldselect": { + "name": "MultiField 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, + "dependsOn": { + "setting": "optionsType", + "value": "select" + } + }, + { + "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/string", + "label": "Validation", + "key": "validation" + } + ] + }, "booleanfield": { "name": "Checkbox", "icon": "Checkmark", diff --git a/packages/standard-components/src/forms/MultiFieldSelect.svelte b/packages/standard-components/src/forms/MultiFieldSelect.svelte new file mode 100644 index 0000000000..d44f471304 --- /dev/null +++ b/packages/standard-components/src/forms/MultiFieldSelect.svelte @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/standard-components/src/forms/index.js b/packages/standard-components/src/forms/index.js index fed371278b..cbc278aa03 100644 --- a/packages/standard-components/src/forms/index.js +++ b/packages/standard-components/src/forms/index.js @@ -3,6 +3,7 @@ export { default as fieldgroup } from "./FieldGroup.svelte" export { default as stringfield } from "./StringField.svelte" export { default as numberfield } from "./NumberField.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 longformfield } from "./LongFormField.svelte" export { default as datetimefield } from "./DateTimeField.svelte" From d55218e8135268fda091e6b2d3681f3d8e32e375 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 24 Aug 2021 16:14:38 +0100 Subject: [PATCH 03/17] Add contains option to lucene query builder --- .../screenTemplates/utils/commonComponents.js | 7 +++ .../FilterEditor/FilterDrawer.svelte | 24 ++++++++- packages/builder/src/helpers/lucene.js | 7 +++ .../src/api/controllers/row/internalSearch.js | 23 +++++++- packages/server/src/db/views/staticViews.js | 10 +++- packages/standard-components/manifest.json | 6 +-- .../src/forms/MultiFieldSelect.svelte | 48 +++++------------ .../src/forms/OptionsField.svelte | 38 ++------------ .../src/forms/optionsParser.js | 52 +++++++++++++++++++ packages/standard-components/src/lucene.js | 1 + 10 files changed, 136 insertions(+), 80 deletions(-) create mode 100644 packages/standard-components/src/forms/optionsParser.js diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 7d40995925..5b3bc041ff 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -168,6 +168,13 @@ export function makeDatasourceFormComponents(datasource) { optionsSource: "schema", }) } + if (fieldType === "array") { + component.customProps({ + placeholder: "Choose an option", + optionsSource: "schema", + }) + } + if (fieldType === "link") { let placeholder = fieldSchema.relationshipType === "one-to-many" diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte index d41d28e4e3..cd5cc1661c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte @@ -9,6 +9,7 @@ DrawerContent, Layout, Body, + Multiselect, } from "@budibase/bbui" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import { generate } from "shortid" @@ -59,6 +60,14 @@ expression.operator = validOperators[0] ?? OperatorOptions.Equals.value 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) => { @@ -74,7 +83,12 @@ const getFieldOptions = field => { const schema = schemaFields.find(x => x.name === field) - return schema?.constraints?.inclusion || [] + const opt = + schema.type == "array" + ? schema?.constraints?.inclusion[0] + : schema?.constraints?.inclusion || [] + + return opt } @@ -128,6 +142,14 @@ options={getFieldOptions(filter.field)} bind:value={filter.value} /> + {:else if filter.type === "array"} + x} + getOptionValue={x => x} + /> {:else if filter.type === "boolean"} { @@ -55,6 +60,8 @@ export const getValidOperatorsForType = type => { ] } else if (type === "options") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] + } else if (type === "array") { + return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.Contains] } else if (type === "boolean") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] } else if (type === "longform") { diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index cc35da40ec..e2e7beee27 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -17,6 +17,7 @@ class QueryBuilder { notEqual: {}, empty: {}, notEmpty: {}, + contains: {}, ...base, } this.limit = 50 @@ -104,6 +105,12 @@ class QueryBuilder { return this } + addContains(key, value) { + this.query.contains[key] = value + return this + } + + /** * Preprocesses a value before going into a lucene search. * Transforms strings to lowercase and wraps strings and bools in quotes. @@ -121,7 +128,7 @@ class QueryBuilder { } // Escape characters if (escape && originalType === "string") { - value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") + value = `${value}`.replace(/[ #+\-&|!{}\]^"~*?:\\]/g, "\\$&") } // Wrap in quotes if (hasVersion && wrap) { @@ -212,6 +219,19 @@ class QueryBuilder { build(this.query.notEmpty, key => `${key}:["" TO *]`) } + if (this.query.contains) { + build(this.query.contains, (key, value) => { + if (!value) { + return null + } + let opts = [] + value.forEach(val => opts.push(`${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`)) + const joined = opts.join(' AND ') + return joined + }) + + } + return query } @@ -253,6 +273,7 @@ const runQuery = async (url, body) => { method: "POST", }) const json = await response.json() + console.log(json) let output = { rows: [], } diff --git a/packages/server/src/db/views/staticViews.js b/packages/server/src/db/views/staticViews.js index 23f320d7eb..35ccbd21b2 100644 --- a/packages/server/src/db/views/staticViews.js +++ b/packages/server/src/db/views/staticViews.js @@ -94,12 +94,18 @@ 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 (key === "_id" || key === "_rev" || input[key] == null) { + + + if (Array.isArray(input[key])) { + for (val in input[key]) { + index(`${idxKey}.${input[key][v]}`, input[key][v], { store: true }); + } + } else if (key === "_id" || key === "_rev" || input[key] == null) { continue } if (typeof input[key] === "string") { diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 8b2bb863a5..948ebc91d8 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -2036,11 +2036,7 @@ "type": "boolean", "label": "Autocomplete", "key": "autocomplete", - "defaultValue": false, - "dependsOn": { - "setting": "optionsType", - "value": "select" - } + "defaultValue": false }, { "type": "boolean", diff --git a/packages/standard-components/src/forms/MultiFieldSelect.svelte b/packages/standard-components/src/forms/MultiFieldSelect.svelte index d44f471304..4e6777d96c 100644 --- a/packages/standard-components/src/forms/MultiFieldSelect.svelte +++ b/packages/standard-components/src/forms/MultiFieldSelect.svelte @@ -1,7 +1,7 @@ - + x : x => x.label} + getOptionValue={flatOptions ? x => x : x => x.value} + {placeholder} + {options} + /> diff --git a/packages/standard-components/src/forms/OptionsField.svelte b/packages/standard-components/src/forms/OptionsField.svelte index b43ddb9f36..e240c8f23d 100644 --- a/packages/standard-components/src/forms/OptionsField.svelte +++ b/packages/standard-components/src/forms/OptionsField.svelte @@ -1,7 +1,7 @@ { + 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 ?? [] + } + + 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 [] + } diff --git a/packages/standard-components/src/lucene.js b/packages/standard-components/src/lucene.js index 50aae1f32c..f132086aa2 100644 --- a/packages/standard-components/src/lucene.js +++ b/packages/standard-components/src/lucene.js @@ -11,6 +11,7 @@ export const buildLuceneQuery = filter => { notEqual: {}, empty: {}, notEmpty: {}, + contains: {} } if (Array.isArray(filter)) { filter.forEach(expression => { From 04ce0abd462b1c5b2a52c114d882f23dfee3aab5 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 25 Aug 2021 14:05:00 +0100 Subject: [PATCH 04/17] Add not contains option to lucene query builder --- .../DataTable/modals/CreateEditColumn.svelte | 1 - packages/builder/src/helpers/lucene.js | 7 +++++-- .../src/api/controllers/row/internalSearch.js | 21 ++++++++++++++++++- packages/standard-components/src/lucene.js | 3 ++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 3f3d28bf81..92edca2a08 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -86,7 +86,6 @@ let arr = field.constraints.inclusion let newArr = [] newArr.push(arr) - console.log(newArr) field.constraints.inclusion = newArr } tables.saveField({ diff --git a/packages/builder/src/helpers/lucene.js b/packages/builder/src/helpers/lucene.js index c9890d8d70..5488919c6d 100644 --- a/packages/builder/src/helpers/lucene.js +++ b/packages/builder/src/helpers/lucene.js @@ -35,7 +35,10 @@ export const OperatorOptions = { value: "contains", label: "Contains", }, - + NotContains: { + value: "notContains", + label: "Does Not Contain", + } } export const getValidOperatorsForType = type => { @@ -61,7 +64,7 @@ export const getValidOperatorsForType = type => { } else if (type === "options") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] } else if (type === "array") { - return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.Contains] + return [Op.Contains, Op.NotContains] } else if (type === "boolean") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] } else if (type === "longform") { diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index e2e7beee27..e4552ccaba 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -18,6 +18,7 @@ class QueryBuilder { empty: {}, notEmpty: {}, contains: {}, + notContains: {}, ...base, } this.limit = 50 @@ -110,6 +111,10 @@ class QueryBuilder { return this } + addNotContains(key, value) { + this.query.notContains[key] = value + return this + } /** * Preprocesses a value before going into a lucene search. @@ -232,6 +237,20 @@ class QueryBuilder { } + if (this.query.notContains) { + build(this.query.notContains, (key, value) => { + if (!value) { + return null + } + let opts = [] + value.forEach(val => opts.push(`!${key}.${val}:${builder.preprocess(val, allPreProcessingOpts)}`)) + const joined = opts.join(' AND ') + return joined + }) + + } + + return query } @@ -273,7 +292,7 @@ const runQuery = async (url, body) => { method: "POST", }) const json = await response.json() - console.log(json) + let output = { rows: [], } diff --git a/packages/standard-components/src/lucene.js b/packages/standard-components/src/lucene.js index f132086aa2..36f6026a06 100644 --- a/packages/standard-components/src/lucene.js +++ b/packages/standard-components/src/lucene.js @@ -11,7 +11,8 @@ export const buildLuceneQuery = filter => { notEqual: {}, empty: {}, notEmpty: {}, - contains: {} + contains: {}, + notContains: {}, } if (Array.isArray(filter)) { filter.forEach(expression => { From 84d85664efc345454876fc6303a75e07464aff2a Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 25 Aug 2021 14:05:23 +0100 Subject: [PATCH 05/17] Add validation for array field --- .../ValidationEditor/ValidationDrawer.svelte | 9 +++++-- .../PropertyControls/componentSettings.js | 1 + packages/standard-components/manifest.json | 2 +- .../src/forms/MultiFieldSelect.svelte | 17 +++++++----- .../src/forms/validation.js | 27 ++++++++++++++++--- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte index b4da2e8e6e..e24a779b62 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte @@ -9,6 +9,7 @@ Body, Input, DatePicker, + Multiselect, } from "@budibase/bbui" import { currentAsset, selectedComponent } from "builderStore" import { findClosestMatchingComponent } from "builderStore/storeUtils" @@ -102,6 +103,11 @@ Constraints.MinLength, Constraints.MaxLength, ], + ["array"]: [ + Constraints.Required, + Constraints.MinLength, + Constraints.MaxLength, + ], } $: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent) @@ -109,7 +115,6 @@ $: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {}) $: fieldType = type?.split("/")[1] || "string" $: constraintOptions = getConstraintsForType(fieldType) - const getConstraintsForType = type => { return ConstraintMap[type] } @@ -283,7 +288,7 @@ /> {:else} - {#if ["string", "number", "options", "longform"].includes(rule.type)} + {#if ["string", "number", "options", "longform", "array"].includes(rule.type)} - x : x => x.label} - getOptionValue={flatOptions ? x => x : x => x.value} - {placeholder} - {options} - /> + {#if fieldState} + 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} + /> + {/if} diff --git a/packages/standard-components/src/forms/validation.js b/packages/standard-components/src/forms/validation.js index 30b6fd7ca7..6109d7e2cb 100644 --- a/packages/standard-components/src/forms/validation.js +++ b/packages/standard-components/src/forms/validation.js @@ -25,7 +25,7 @@ export const createValidatorFromConstraints = ( schemaConstraints.presence?.allowEmpty === false ) { rules.push({ - type: "string", + type: schemaConstraints.type == "array" ? "array" : "string", constraint: "required", error: "Required", }) @@ -63,7 +63,7 @@ export const createValidatorFromConstraints = ( } // Inclusion constraint - if (exists(schemaConstraints.inclusion)) { + if (!schemaConstraints.type == "array" ? exists(schemaConstraints.inclusion) : false) { const options = schemaConstraints.inclusion || [] rules.push({ type: "string", @@ -73,6 +73,18 @@ export const createValidatorFromConstraints = ( }) } + // Inclusion constraint + if (schemaConstraints.type == "array" ? exists(schemaConstraints.inclusion[0]) : false ) { + const options = schemaConstraints.inclusion[0] || [] + rules.push({ + type: "array", + constraint: "inclusion", + value: options, + error: "Invalid value", + }) + } + + // Date constraint if (exists(schemaConstraints.datetime?.earliest)) { const limit = schemaConstraints.datetime.earliest @@ -142,7 +154,7 @@ const evaluateRule = (rule, value) => { * in the same format. * @param value the value 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) => { // Treat nulls or empty strings as null @@ -202,6 +214,13 @@ const parseType = (value, type) => { 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 return null } @@ -239,7 +258,7 @@ const maxValueHandler = (value, rule) => { // Evaluates an inclusion constraint const inclusionHandler = (value, rule) => { - return value == null || rule.value.includes(value) + return value == null || rule.type == "array" ? rule.value.map(val => val === value) : rule.value.includes(value) } // Evaluates an equal constraint From c681330793e764ad25697b56d0e899a422357300 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 25 Aug 2021 15:49:04 +0100 Subject: [PATCH 06/17] Add more validation options for array field --- .../ValidationEditor/ValidationDrawer.svelte | 35 +++++-- packages/builder/src/helpers/lucene.js | 2 +- .../src/api/controllers/row/internalSearch.js | 19 ++-- packages/server/src/db/views/staticViews.js | 11 ++- .../src/forms/MultiFieldSelect.svelte | 8 +- .../src/forms/optionsParser.js | 96 +++++++++---------- .../src/forms/validation.js | 39 ++++++-- 7 files changed, 131 insertions(+), 79 deletions(-) diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte index e24a779b62..64938fe75b 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ValidationEditor/ValidationDrawer.svelte @@ -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)} {:else} - {#if ["string", "number", "options", "longform", "array"].includes(rule.type)} + {#if ["string", "number", "options", "longform"].includes(rule.type)} + {:else if rule.type === "array" && ["contains", "notContains"].includes(rule.constraint)} + x} + getOptionValue={x => x} + on:change={e => (rule.value = e.detail)} + bind:value={rule.value} + /> {:else if rule.type === "boolean"}