Add contains option to lucene query builder

This commit is contained in:
Peter Clement 2021-08-24 16:14:38 +01:00
parent dbd0d76613
commit d55218e813
10 changed files with 136 additions and 80 deletions

View File

@ -168,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"

View File

@ -9,6 +9,7 @@
DrawerContent, DrawerContent,
Layout, Layout,
Body, Body,
Multiselect,
} from "@budibase/bbui" } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid" import { generate } from "shortid"
@ -59,6 +60,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 +83,12 @@
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.type == "array"
? schema?.constraints?.inclusion[0]
: schema?.constraints?.inclusion || []
return opt
} }
</script> </script>
@ -128,6 +142,14 @@
options={getFieldOptions(filter.field)} options={getFieldOptions(filter.field)}
bind:value={filter.value} bind:value={filter.value}
/> />
{:else if filter.type === "array"}
<Multiselect
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
getOptionLabel={x => x}
getOptionValue={x => x}
/>
{:else if filter.type === "boolean"} {:else if filter.type === "boolean"}
<Combobox <Combobox
disabled={filter.noValue} disabled={filter.noValue}

View File

@ -31,6 +31,11 @@ export const OperatorOptions = {
value: "rangeHigh", value: "rangeHigh",
label: "Less than", label: "Less than",
}, },
Contains: {
value: "contains",
label: "Contains",
},
} }
export const getValidOperatorsForType = type => { export const getValidOperatorsForType = type => {
@ -55,6 +60,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.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.Contains]
} 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") {

View File

@ -17,6 +17,7 @@ class QueryBuilder {
notEqual: {}, notEqual: {},
empty: {}, empty: {},
notEmpty: {}, notEmpty: {},
contains: {},
...base, ...base,
} }
this.limit = 50 this.limit = 50
@ -104,6 +105,12 @@ class QueryBuilder {
return this return this
} }
addContains(key, value) {
this.query.contains[key] = value
return this
}
/** /**
* Preprocesses a value before going into a lucene search. * Preprocesses a value before going into a lucene search.
* Transforms strings to lowercase and wraps strings and bools in quotes. * Transforms strings to lowercase and wraps strings and bools in quotes.
@ -121,7 +128,7 @@ class QueryBuilder {
} }
// Escape characters // Escape characters
if (escape && originalType === "string") { if (escape && originalType === "string") {
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") value = `${value}`.replace(/[ #+\-&|!{}\]^"~*?:\\]/g, "\\$&")
} }
// Wrap in quotes // Wrap in quotes
if (hasVersion && wrap) { if (hasVersion && wrap) {
@ -212,6 +219,19 @@ class QueryBuilder {
build(this.query.notEmpty, key => `${key}:["" TO *]`) 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 return query
} }
@ -253,6 +273,7 @@ const runQuery = async (url, body) => {
method: "POST", method: "POST",
}) })
const json = await response.json() const json = await response.json()
console.log(json)
let output = { let output = {
rows: [], rows: [],
} }

View File

@ -94,12 +94,18 @@ exports.createAllSearchIndex = async appId => {
await searchIndex( await searchIndex(
appId, appId,
SearchIndexes.ROWS, SearchIndexes.ROWS,
function (doc) { function (doc) {
function idx(input, prev) { function idx(input, prev) {
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 (val in input[key]) {
index(`${idxKey}.${input[key][v]}`, input[key][v], { store: true });
}
} else if (key === "_id" || key === "_rev" || input[key] == null) {
continue continue
} }
if (typeof input[key] === "string") { if (typeof input[key] === "string") {

View File

@ -2036,11 +2036,7 @@
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
"key": "autocomplete", "key": "autocomplete",
"defaultValue": false, "defaultValue": false
"dependsOn": {
"setting": "optionsType",
"value": "select"
}
}, },
{ {
"type": "boolean", "type": "boolean",

View File

@ -1,7 +1,7 @@
<script> <script>
import { Multiselect } from "@budibase/bbui" import { Multiselect } 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
@ -13,49 +13,20 @@
export let labelColumn export let labelColumn
export let valueColumn export let valueColumn
export let customOptions export let customOptions
console.log(defaultValue)
let fieldState let fieldState
let fieldApi let fieldApi
let fieldSchema let fieldSchema
$: flatOptions = optionsSource == null || optionsSource === "schema"
$: options = getOptions( $: options = getOptions(
optionsSource, optionsSource,
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
@ -69,5 +40,10 @@
bind:fieldApi bind:fieldApi
bind:fieldSchema bind:fieldSchema
> >
<Multiselect {placeholder} options={options[0]} /> <Multiselect
getOptionLabel={flatOptions ? x => x : x => x.label}
getOptionValue={flatOptions ? x => x : x => x.value}
{placeholder}
{options}
/>
</Field> </Field>

View File

@ -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

View File

@ -0,0 +1,52 @@
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 ?? []
}
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

@ -11,6 +11,7 @@ export const buildLuceneQuery = filter => {
notEqual: {}, notEqual: {},
empty: {}, empty: {},
notEmpty: {}, notEmpty: {},
contains: {}
} }
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
filter.forEach(expression => { filter.forEach(expression => {