Browse Source

Add contains option to lucene query builder

gh-pages
Peter Clement 5 years ago
parent
commit
d55218e813
  1. 7
      packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js
  2. 24
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
  3. 7
      packages/builder/src/helpers/lucene.js
  4. 23
      packages/server/src/api/controllers/row/internalSearch.js
  5. 10
      packages/server/src/db/views/staticViews.js
  6. 6
      packages/standard-components/manifest.json
  7. 48
      packages/standard-components/src/forms/MultiFieldSelect.svelte
  8. 38
      packages/standard-components/src/forms/OptionsField.svelte
  9. 52
      packages/standard-components/src/forms/optionsParser.js
  10. 1
      packages/standard-components/src/lucene.js

7
packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js

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

24
packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte

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

7
packages/builder/src/helpers/lucene.js

@ -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") {

23
packages/server/src/api/controllers/row/internalSearch.js

@ -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: [],
} }

10
packages/server/src/db/views/staticViews.js

@ -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") {

6
packages/standard-components/manifest.json

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

48
packages/standard-components/src/forms/MultiFieldSelect.svelte

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

38
packages/standard-components/src/forms/OptionsField.svelte

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

52
packages/standard-components/src/forms/optionsParser.js

@ -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 []
}

1
packages/standard-components/src/lucene.js

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

Loading…
Cancel
Save