Browse Source

Updating the filter system to allow adding multiple filter properties of the same name at once, as well as enabling the use of the allOr property from within the UI - resolves an old issue #2585.

master
mike12345567 4 years ago
parent
commit
5eeb2ea600
  1. 29
      packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte
  2. 66
      packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte
  3. 4
      packages/frontend-core/src/utils/lucene.js
  4. 3
      packages/server/src/api/controllers/row/internalSearch.js
  5. 7
      packages/server/src/api/controllers/row/utils.js
  6. 8
      packages/server/src/integrations/base/sql.ts
  7. 38
      packages/server/src/integrations/base/utils.ts
  8. 1
      packages/server/src/integrations/queries/sql.ts

29
packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte

@ -9,19 +9,26 @@
Input, Input,
Layout, Layout,
Select, Select,
Toggle,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { LuceneUtils, Constants } from "@budibase/frontend-core" import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields" import { getFields } from "helpers/searchFields"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let schemaFields export let schemaFields
export let filters = [] export let filters = []
export let bindings = [] export let bindings = []
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let allowBindings = true export let allowBindings = true
export let allOr = false
$: dispatch("change", filters)
$: enrichedSchemaFields = getFields(schemaFields || []) $: enrichedSchemaFields = getFields(schemaFields || [])
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || [] $: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
@ -69,7 +76,7 @@
} }
// if changed to an array, change default value to empty array // if changed to an array, change default value to empty array
const idx = filters.findIndex(x => x.field === field) const idx = filters.findIndex(x => x.id === expression.id)
if (expression.type === "array") { if (expression.type === "array") {
filters[idx].value = [] filters[idx].value = []
} else { } else {
@ -179,10 +186,16 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<div> <div class="bottom">
<Button icon="AddCircle" size="M" secondary on:click={addFilter}> <Button icon="AddCircle" size="M" secondary on:click={addFilter}>
Add filter Add filter
</Button> </Button>
<div class="toggle">
<Toggle
value={allOr}
on:change={event => (allOr = event.detail)}
/><Label size="L">OR conditions</Label>
</div>
</div> </div>
</Layout> </Layout>
</div> </div>
@ -202,4 +215,16 @@
align-items: center; align-items: center;
grid-template-columns: 1fr 120px 120px 1fr auto auto; grid-template-columns: 1fr 120px 120px 1fr auto auto;
} }
.toggle {
display: flex;
align-items: center;
padding-right: var(--spacing-s);
}
.bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
</style> </style>

66
packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte

@ -8,21 +8,73 @@
import FilterDrawer from "./FilterDrawer.svelte" import FilterDrawer from "./FilterDrawer.svelte"
import { currentAsset } from "builderStore" import { currentAsset } from "builderStore"
const QUERY_START_REGEX = /\d[0-9]*:/g
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value = [] export let value = []
export let componentInstance export let componentInstance
export let bindings = [] export let bindings = []
let drawer let drawer,
let tempValue = value || [] toSaveFilters = null,
allOr,
initialAllOr
$: initialFilters = correctFilters(value || [])
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance) $: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema $: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
const saveFilter = async () => { function addNumbering(filters) {
dispatch("change", tempValue) let count = 1
for (let value of filters) {
if (value.field && value.field?.match(QUERY_START_REGEX) == null) {
value.field = `${count++}:${value.field}`
}
}
return filters
}
function correctFilters(filters) {
const corrected = []
for (let filter of filters) {
let field = filter.field
if (filter.operator === "allOr") {
initialAllOr = allOr = true
continue
}
if (
typeof filter.field === "string" &&
filter.field.match(QUERY_START_REGEX) != null
) {
const parts = field.split(":")
const number = parts[0]
// it's the new format, remove number
if (!isNaN(parseInt(number))) {
parts.shift()
field = parts.join(":")
}
}
corrected.push({
...filter,
field,
})
}
return corrected
}
async function saveFilter() {
if (!toSaveFilters && allOr !== initialAllOr) {
toSaveFilters = initialFilters
}
const filters = toSaveFilters?.filter(filter => filter.operator !== "allOr")
if (allOr && filters) {
filters.push({ operator: "allOr" })
}
// only save if anything was updated
if (filters) {
dispatch("change", addNumbering(filters))
}
notifications.success("Filters saved.") notifications.success("Filters saved.")
drawer.hide() drawer.hide()
} }
@ -33,8 +85,12 @@
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> <Button cta slot="buttons" on:click={saveFilter}>Save</Button>
<FilterDrawer <FilterDrawer
slot="body" slot="body"
bind:filters={tempValue} filters={initialFilters}
{bindings} {bindings}
{schemaFields} {schemaFields}
bind:allOr
on:change={event => {
toSaveFilters = event.detail
}}
/> />
</Drawer> </Drawer>

4
packages/frontend-core/src/utils/lucene.js

@ -103,6 +103,10 @@ export const buildLuceneQuery = filter => {
const isHbs = const isHbs =
typeof value === "string" && value.match(HBS_REGEX)?.length > 0 typeof value === "string" && value.match(HBS_REGEX)?.length > 0
// Parse all values into correct types // Parse all values into correct types
if (operator === "allOr") {
query.allOr = true
return
}
if (type === "datetime") { if (type === "datetime") {
// Ensure date value is a valid date and parse into correct format // Ensure date value is a valid date and parse into correct format
if (!value) { if (!value) {

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

@ -1,4 +1,5 @@
const { SearchIndexes } = require("../../../db/utils") const { SearchIndexes } = require("../../../db/utils")
const { removeKeyNumbering } = require("./utils")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const { getCouchInfo } = require("@budibase/backend-core/db") const { getCouchInfo } = require("@budibase/backend-core/db")
const { getAppId } = require("@budibase/backend-core/context") const { getAppId } = require("@budibase/backend-core/context")
@ -197,6 +198,8 @@ class QueryBuilder {
function build(structure, queryFn) { function build(structure, queryFn) {
for (let [key, value] of Object.entries(structure)) { for (let [key, value] of Object.entries(structure)) {
// check for new format - remove numbering if needed
key = removeKeyNumbering(key)
key = builder.preprocess(key.replace(/ /g, "_"), { key = builder.preprocess(key.replace(/ /g, "_"), {
escape: true, escape: true,
}) })

7
packages/server/src/api/controllers/row/utils.js

@ -3,8 +3,11 @@ const { cloneDeep } = require("lodash/fp")
const { InternalTables } = require("../../../db/utils") const { InternalTables } = require("../../../db/utils")
const userController = require("../user") const userController = require("../user")
const { FieldTypes } = require("../../../constants") const { FieldTypes } = require("../../../constants")
const { makeExternalQuery } = require("../../../integrations/base/utils")
const { getAppDB } = require("@budibase/backend-core/context") const { getAppDB } = require("@budibase/backend-core/context")
const {
makeExternalQuery,
removeKeyNumbering,
} = require("../../../integrations/base/utils")
validateJs.extend(validateJs.validators.datetime, { validateJs.extend(validateJs.validators.datetime, {
parse: function (value) { parse: function (value) {
@ -16,6 +19,8 @@ validateJs.extend(validateJs.validators.datetime, {
}, },
}) })
exports.removeKeyNumbering = removeKeyNumbering
exports.getDatasourceAndQuery = async json => { exports.getDatasourceAndQuery = async json => {
const datasourceId = json.endpoint.datasourceId const datasourceId = json.endpoint.datasourceId
const db = getAppDB() const db = getAppDB()

8
packages/server/src/integrations/base/sql.ts

@ -10,6 +10,7 @@ import {
import { isIsoDateString, SqlClients } from "../utils" import { isIsoDateString, SqlClients } from "../utils"
import SqlTableQueryBuilder from "./sqlTable" import SqlTableQueryBuilder from "./sqlTable"
import environment from "../../environment" import environment from "../../environment"
import { removeKeyNumbering } from "./utils"
const envLimit = environment.SQL_MAX_ROWS const envLimit = environment.SQL_MAX_ROWS
? parseInt(environment.SQL_MAX_ROWS) ? parseInt(environment.SQL_MAX_ROWS)
@ -133,12 +134,13 @@ class InternalBuilder {
fn: (key: string, value: any) => void fn: (key: string, value: any) => void
) { ) {
for (let [key, value] of Object.entries(structure)) { for (let [key, value] of Object.entries(structure)) {
const isRelationshipField = key.includes(".") const updatedKey = removeKeyNumbering(key)
const isRelationshipField = updatedKey.includes(".")
if (!opts.relationship && !isRelationshipField) { if (!opts.relationship && !isRelationshipField) {
fn(`${opts.tableName}.${key}`, value) fn(`${opts.tableName}.${updatedKey}`, value)
} }
if (opts.relationship && isRelationshipField) { if (opts.relationship && isRelationshipField) {
fn(key, value) fn(updatedKey, value)
} }
} }
} }

38
packages/server/src/integrations/base/utils.ts

@ -1,22 +1,30 @@
import { QueryJson } from "../../definitions/datasource" import { QueryJson } from "../../definitions/datasource"
import { Datasource } from "../../definitions/common" import { Datasource } from "../../definitions/common"
const { integrations } = require("../index")
module DatasourceUtils { const QUERY_START_REGEX = /\d[0-9]*:/g
const { integrations } = require("../index")
export async function makeExternalQuery( export async function makeExternalQuery(
datasource: Datasource, datasource: Datasource,
json: QueryJson json: QueryJson
) { ) {
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
// query is the opinionated function // query is the opinionated function
if (Integration.prototype.query) { if (Integration.prototype.query) {
const integration = new Integration(datasource.config) const integration = new Integration(datasource.config)
return integration.query(json) return integration.query(json)
} else { } else {
throw "Datasource does not support query." throw "Datasource does not support query."
}
} }
}
module.exports.makeExternalQuery = makeExternalQuery export function removeKeyNumbering(key: any): string {
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
const parts = key.split(":")
// remove the number
parts.shift()
return parts.join(":")
} else {
return key
}
} }

1
packages/server/src/integrations/queries/sql.ts

@ -1,5 +1,4 @@
import { findHBSBlocks, processStringSync } from "@budibase/string-templates" import { findHBSBlocks, processStringSync } from "@budibase/string-templates"
import { Integration } from "../../definitions/datasource"
import { DatasourcePlus } from "../base/datasourcePlus" import { DatasourcePlus } from "../base/datasourcePlus"
const CONST_CHAR_REGEX = new RegExp("'[^']*'", "g") const CONST_CHAR_REGEX = new RegExp("'[^']*'", "g")

Loading…
Cancel
Save