diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 4a2cfd34e2..d9dddd0097 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -8,19 +8,9 @@ import { SearchParams, WithRequired, } from "@budibase/types" +import { dataFilters } from "@budibase/shared-core" -const QUERY_START_REGEX = /\d[0-9]*:/g - -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 - } -} +export const removeKeyNumbering = dataFilters.removeKeyNumbering /** * Class to build lucene query URLs. diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 48920a3771..7d62a6ef39 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -17,8 +17,8 @@ import { ContextUser, CouchFindOptions, DatabaseQueryOpts, - SearchQuery, - SearchQueryOperators, + SearchFilters, + SearchFilterOperator, SearchUsersRequest, User, } from "@budibase/types" @@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) { return users } -export function isSupportedUserSearch(query: SearchQuery) { +export function isSupportedUserSearch(query: SearchFilters) { const allowed = [ - { op: SearchQueryOperators.STRING, key: "email" }, - { op: SearchQueryOperators.EQUAL, key: "_id" }, - { op: SearchQueryOperators.ONE_OF, key: "_id" }, + { op: SearchFilterOperator.STRING, key: "email" }, + { op: SearchFilterOperator.EQUAL, key: "_id" }, + { op: SearchFilterOperator.ONE_OF, key: "_id" }, ] for (let [key, operation] of Object.entries(query)) { if (typeof operation !== "object") { diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 0632993cf0..6434c7710d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -14,6 +14,7 @@ notifications, Checkbox, DatePicker, + DrawerContent, } from "@budibase/bbui" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import { automationStore, selectedAutomation, tables } from "stores/builder" @@ -37,7 +38,7 @@ hbAutocomplete, EditorModes, } from "components/common/CodeEditor" - import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte" + import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte" import { LuceneUtils, Utils } from "@budibase/frontend-core" import { getSchemaForDatasourcePlus, @@ -442,15 +443,16 @@ - (tempFilters = e.detail)} - /> + + (tempFilters = e.detail)} + /> + {:else if value.customType === "password"} import { createEventDispatcher } from "svelte" import { ActionButton, Modal, ModalContent } from "@budibase/bbui" - import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte" + import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte" export let schema export let filters @@ -40,7 +40,7 @@ onConfirm={() => dispatch("change", tempValue)} >
- diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterBuilder.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterBuilder.svelte new file mode 100644 index 0000000000..0ab67cbada --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterBuilder.svelte @@ -0,0 +1,84 @@ + + + +
+ + { + const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id) + rawFilters[indexToUpdate] = { + ...rawFilters[indexToUpdate], + value: event.detail, + } + }} + /> + diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte index 0f1f08d823..e481bb4381 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte @@ -1,8 +1,14 @@ -
- - - {#if !filters?.length} - Add your first filter expression. - {:else} - Results are filtered to only those which match all of the following - constraints. - {/if} - - {#if filters?.length} -
- {#each filters as filter} - onOperatorChange(filter, e.detail)} - placeholder={null} - /> - {#if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)} - - {:else if ["options", "array"].includes(filter.type)} - - {:else if filter.type === "boolean"} - - {:else if filter.type === "datetime"} - - {:else} - - {/if} -
- duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> -
- {/each} -
- {/if} -
- -
-
-
- - + +
+ Results are filtered to only those which match all of the following + constraints. +
+
diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index 644630810d..3489fd809c 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -58,17 +58,6 @@ } } - const deleteAttachments = async fileList => { - try { - return await API.deleteAttachments({ - keys: fileList, - tableId: formContext?.dataSource?.tableId, - }) - } catch (error) { - return [] - } - } - const handleChange = e => { const value = fieldApiMapper.set(e.detail) const changed = fieldApi.setValue(value) @@ -98,7 +87,6 @@ error={fieldState.error} on:change={handleChange} {processFiles} - {deleteAttachments} {handleFileTooLarge} {handleTooManyFiles} {maximum} diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 4ca88de8f2..3f97573d4a 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -11,6 +11,7 @@ "@budibase/types": "0.0.0", "dayjs": "^1.10.8", "lodash": "4.17.21", + "shortid": "2.2.15", "socket.io-client": "^4.6.1" } } diff --git a/packages/frontend-core/src/api/attachments.js b/packages/frontend-core/src/api/attachments.js index cff466c86f..72f280d99d 100644 --- a/packages/frontend-core/src/api/attachments.js +++ b/packages/frontend-core/src/api/attachments.js @@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => { }) return { publicUrl } }, - - /** - * Deletes attachments from the bucket. - * @param keys the attachments to delete - * @param tableId the associated table ID - */ - deleteAttachments: async ({ keys, tableId }) => { - return await API.post({ - url: `/api/attachments/${tableId}/delete`, - body: { - keys, - }, - }) - }, - - /** - * Deletes attachments from the builder bucket. - * @param keys the attachments to delete - */ - deleteBuilderAttachments: async keys => { - return await API.post({ - url: `/api/attachments/delete`, - body: { - keys, - }, - }) - }, - /** * Download an attachment from a row given its column name. * @param datasourceId the ID of the datasource to download from diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte similarity index 50% rename from packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte rename to packages/frontend-core/src/components/FilterBuilder.svelte index 7f1ee8010d..1b252d5b06 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -4,33 +4,36 @@ Button, Combobox, DatePicker, - DrawerContent, Icon, Input, - Label, Layout, - Multiselect, Select, + Label, + Multiselect, } from "@budibase/bbui" - import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" - import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" + import { FieldType, SearchFilterOperator } from "@budibase/types" import { generate } from "shortid" - import { Constants, LuceneUtils } from "@budibase/frontend-core" - import { getFields } from "helpers/searchFields" - import { FieldType } from "@budibase/types" - import { createEventDispatcher, onMount } from "svelte" + import { LuceneUtils, Constants } from "@budibase/frontend-core" + import { getContext } from "svelte" import FilterUsers from "./FilterUsers.svelte" + const { OperatorOptions } = Constants + export let schemaFields export let filters = [] - export let bindings = [] - export let panel = ClientBindingPanel - export let allowBindings = true export let datasource + export let behaviourFilters = false + export let allowBindings = false + export let filtersLabel = "Filters" + + $: matchAny = filters?.find(filter => filter.operator === "allOr") != null + $: onEmptyFilter = + filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" + + $: fieldFilters = filters.filter( + filter => filter.operator !== "allOr" && !filter.onEmptyFilter + ) - const dispatch = createEventDispatcher() - const { OperatorOptions } = Constants - const KeyedFieldRegex = /\d[0-9]*:/g const behaviourOptions = [ { value: "and", label: "Match all filters" }, { value: "or", label: "Match any filter" }, @@ -40,62 +43,18 @@ { value: "none", label: "Return no rows" }, ] - let rawFilters - let matchAny = false - let onEmptyFilter = "all" + const context = getContext("context") - $: parseFilters(filters) - $: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter)) - $: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true }) - $: fieldOptions = enrichedSchemaFields.map(field => field.name) || [] - $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] - - // Remove field key prefixes and determine which behaviours to use - const parseFilters = filters => { - matchAny = filters?.find(filter => filter.operator === "allOr") != null - onEmptyFilter = - filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" - rawFilters = (filters || []) - .filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter) - .map(filter => { - const { field } = filter - let newFilter = { ...filter } - delete newFilter.allOr - if (typeof field === "string" && field.match(KeyedFieldRegex) != null) { - const parts = field.split(":") - parts.shift() - newFilter.field = parts.join(":") - } - return newFilter - }) - } - - onMount(() => { - parseFilters(filters) - rawFilters.forEach(filter => { - filter.type = - schemaFields.find(field => field.name === filter.field)?.type || - filter.type - }) - }) - - // Add field key prefixes and a special metadata filter object to indicate - // how to handle filter behaviour - const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => { - let count = 1 - return rawFilters - .filter(filter => filter.field) - .map(filter => ({ - ...filter, - field: `${count++}:${filter.field}`, - })) - .concat(matchAny ? [{ operator: "allOr" }] : []) - .concat([{ onEmptyFilter }]) - } + $: fieldOptions = (schemaFields ?? []) + .filter(field => getValidOperatorsForType(field).length) + .map(field => ({ + label: field.displayName || field.name, + value: field.name, + })) const addFilter = () => { - rawFilters = [ - ...rawFilters, + filters = [ + ...(filters || []), { id: generate(), field: null, @@ -107,22 +66,57 @@ } const removeFilter = id => { - rawFilters = rawFilters.filter(field => field.id !== id) + filters = filters.filter(field => field.id !== id) } const duplicateFilter = id => { - const existingFilter = rawFilters.find(filter => filter.id === id) + const existingFilter = filters.find(filter => filter.id === id) const duplicate = { ...existingFilter, id: generate() } - rawFilters = [...rawFilters, duplicate] + filters = [...filters, duplicate] + } + + const onFieldChange = filter => { + const previousType = filter.type + sanitizeTypes(filter) + sanitizeOperator(filter) + sanitizeValue(filter, previousType) + } + + const onOperatorChange = filter => { + sanitizeOperator(filter) + sanitizeValue(filter, filter.type) + } + + const onValueTypeChange = filter => { + sanitizeValue(filter) + } + + const getFieldOptions = field => { + const schema = schemaFields.find(x => x.name === field) + return schema?.constraints?.inclusion || [] } const getSchema = filter => { - return enrichedSchemaFields.find(field => field.name === filter.field) + return schemaFields.find(field => field.name === filter.field) } + const getValidOperatorsForType = filter => { + if (!filter?.field && !filter?.name) { + return [] + } + + return LuceneUtils.getValidOperatorsForType( + filter, + filter.field || filter.name, + datasource + ) + } + + $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] + const sanitizeTypes = filter => { // Update type based on field - const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field) + const fieldSchema = schemaFields.find(x => x.name === filter.field) filter.type = fieldSchema?.type filter.subtype = fieldSchema?.subtype @@ -154,88 +148,79 @@ // Ensure array values are properly set and cleared if (Array.isArray(filter.value)) { - if (filter.valueType !== "Value" || filter.type !== "array") { + if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) { filter.value = null } - } else if (filter.type === "array" && filter.valueType === "Value") { + } else if ( + filter.type === FieldType.ARRAY && + filter.valueType === "Value" + ) { filter.value = [] } else if ( previousType !== filter.type && (previousType === FieldType.BB_REFERENCE || filter.type === FieldType.BB_REFERENCE) ) { - filter.value = filter.type === "array" ? [] : null + filter.value = filter.type === FieldType.ARRAY ? [] : null } } - const onFieldChange = filter => { - const previousType = filter.type - sanitizeTypes(filter) - sanitizeOperator(filter) - sanitizeValue(filter, previousType) - } - - const onOperatorChange = filter => { - sanitizeOperator(filter) - sanitizeValue(filter, filter.type) - } - - const onValueTypeChange = filter => { - sanitizeValue(filter) - } - - const getFieldOptions = field => { - const schema = enrichedSchemaFields.find(x => x.name === field) - return schema?.constraints?.inclusion || [] - } - - const getValidOperatorsForType = filter => { - if (!filter?.field) { - return [] + function handleAllOr(option) { + filters = filters.filter(f => f.operator !== "allOr") + if (option === "or") { + filters.push({ operator: "allOr" }) } + } - return LuceneUtils.getValidOperatorsForType( - { type: filter.type, subtype: filter.subtype }, - filter.field, - datasource - ) + function handleOnEmptyFilter(value) { + filters = filters?.filter(filter => !filter.onEmptyFilter) + filters.push({ onEmptyFilter: value }) } - -
- - {#if !rawFilters?.length} - Add your first filter expression. - {:else} -
- opt.label} - getOptionValue={opt => opt.value} - on:change={e => (onEmptyFilter = e.detail)} - placeholder={null} - /> +
+ + {#if fieldOptions?.length} + + {#if !fieldFilters?.length} + Add your first filter expression. + {:else} + + {#if behaviourFilters} +
+ opt.label} + getOptionValue={opt => opt.value} + on:change={e => handleOnEmptyFilter(e.detail)} + placeholder={null} + /> + {/if} +
{/if} -
+ {/if} + + {#if fieldFilters?.length}
-
- -
-
- {#each rawFilters as filter} + {#if filtersLabel} +
+ +
+ {/if} +
+ {#each fieldFilters as filter} onValueTypeChange(filter)} - placeholder={null} - /> - {#if filter.field && filter.valueType === "Binding"} - (filter.value = event.detail)} + {#if allowBindings} + - {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)} - {:else if filter.type === "options"} + {:else if filter.type === FieldType.OPTIONS} - {:else if filter.type === "boolean"} + {:else if filter.type === FieldType.BOOLEAN} - {:else if filter.type === "datetime"} + {:else if filter.type === FieldType.DATETIME} {:else} - + {/if} - duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> +
+ duplicateFilter(filter.id)} + /> + removeFilter(filter.id)} + /> +
{/each}
{/if} -
+
- -
- + {:else} + None of the table column can be used for filtering. + {/if} + +
diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterUsers.svelte b/packages/frontend-core/src/components/FilterUsers.svelte similarity index 88% rename from packages/builder/src/components/design/settings/controls/FilterEditor/FilterUsers.svelte rename to packages/frontend-core/src/components/FilterUsers.svelte index 88383ba170..1712d7ebdf 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterUsers.svelte +++ b/packages/frontend-core/src/components/FilterUsers.svelte @@ -1,9 +1,9 @@