From 3ffba9faf8bda98bc16e606bf8650f3e88bbc4ed Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Mar 2024 09:49:05 +0100 Subject: [PATCH 001/112] Add download file option --- .../actions/DownloadFile.svelte | 28 +++++++++++++++++++ .../ButtonActionEditor/actions/index.js | 1 + .../controls/ButtonActionEditor/manifest.json | 5 ++++ 3 files changed, 34 insertions(+) create mode 100644 packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte new file mode 100644 index 0000000000..1c1974155b --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte @@ -0,0 +1,28 @@ + + +
+ + (parameters.value = e.detail)} + /> +
+ + diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js index eb354d6557..587993377d 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js @@ -22,3 +22,4 @@ export { default as PromptUser } from "./PromptUser.svelte" export { default as OpenSidePanel } from "./OpenSidePanel.svelte" export { default as CloseSidePanel } from "./CloseSidePanel.svelte" export { default as ClearRowSelection } from "./ClearRowSelection.svelte" +export { default as DownloadFile } from "./DownloadFile.svelte" diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 9391baf3dc..6d1794c991 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -161,6 +161,11 @@ "name": "Clear Row Selection", "type": "data", "component": "ClearRowSelection" + }, + { + "name": "Download file", + "type": "data", + "component": "DownloadFile" } ] } From 496679f3ebcf3b60b03573e47ec0f76900b12e40 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Mar 2024 10:38:09 +0100 Subject: [PATCH 002/112] Download config --- .../ButtonActionEditor/actions/DownloadFile.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte index 1c1974155b..741b21a768 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte @@ -1,17 +1,23 @@
(parameters.value = e.detail)} />
From 349b22ba252ac93450bc549463ce748352a67910 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Mar 2024 11:34:05 +0100 Subject: [PATCH 003/112] Move downloadfile to frontend-core --- .../components/start/ExportAppModal.svelte | 38 ++----------------- packages/frontend-core/src/utils/download.js | 31 +++++++++++++++ 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/builder/src/components/start/ExportAppModal.svelte b/packages/builder/src/components/start/ExportAppModal.svelte index ec0cf42fe0..8cf2025c39 100644 --- a/packages/builder/src/components/start/ExportAppModal.svelte +++ b/packages/builder/src/components/start/ExportAppModal.svelte @@ -8,6 +8,7 @@ Input, notifications, } from "@budibase/bbui" + import { downloadFile } from "@budibase/frontend-core" import { createValidationStore } from "helpers/validation/yup" export let app @@ -55,42 +56,11 @@ const exportApp = async () => { const id = published ? app.prodId : app.devId const url = `/api/backups/export?appId=${id}` - await downloadFile(url, { - excludeRows: !includeInternalTablesRows, - encryptPassword: password, - }) - } - - async function downloadFile(url, body) { try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), + await downloadFile(url, { + excludeRows: !includeInternalTablesRows, + encryptPassword: password, }) - - if (response.ok) { - const contentDisposition = response.headers.get("Content-Disposition") - - const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( - contentDisposition - ) - - const filename = matches[1].replace(/['"]/g, "") - - const url = URL.createObjectURL(await response.blob()) - - const link = document.createElement("a") - link.href = url - link.download = filename - link.click() - - URL.revokeObjectURL(url) - } else { - notifications.error("Error exporting the app.") - } } catch (error) { notifications.error(error.message || "Error downloading the exported app") } diff --git a/packages/frontend-core/src/utils/download.js b/packages/frontend-core/src/utils/download.js index 89c8572253..f3701b357b 100644 --- a/packages/frontend-core/src/utils/download.js +++ b/packages/frontend-core/src/utils/download.js @@ -34,3 +34,34 @@ export async function downloadStream(streamResponse) { URL.revokeObjectURL(blobUrl) } + +export async function downloadFile(url, body) { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }) + + if (response.ok) { + const contentDisposition = response.headers.get("Content-Disposition") + + const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( + contentDisposition + ) + + const filename = matches[1].replace(/['"]/g, "") + + const url = URL.createObjectURL(await response.blob()) + + const link = document.createElement("a") + link.href = url + link.download = filename + link.click() + + URL.revokeObjectURL(url) + } else { + notifications.error("Error exporting the app.") + } +} From 1a7e845c5648df17bf42e4ae997c213e36f18b70 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Mar 2024 11:40:27 +0100 Subject: [PATCH 004/112] Remove notifications from frontend-core --- .../components/start/ExportAppModal.svelte | 6 ++- packages/frontend-core/src/utils/download.js | 39 ++++++++++--------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/builder/src/components/start/ExportAppModal.svelte b/packages/builder/src/components/start/ExportAppModal.svelte index 8cf2025c39..3a995be7ae 100644 --- a/packages/builder/src/components/start/ExportAppModal.svelte +++ b/packages/builder/src/components/start/ExportAppModal.svelte @@ -56,11 +56,15 @@ const exportApp = async () => { const id = published ? app.prodId : app.devId const url = `/api/backups/export?appId=${id}` + try { - await downloadFile(url, { + const downloaded = await downloadFile(url, { excludeRows: !includeInternalTablesRows, encryptPassword: password, }) + if (!downloaded) { + notifications.error("Error exporting the app.") + } } catch (error) { notifications.error(error.message || "Error downloading the exported app") } diff --git a/packages/frontend-core/src/utils/download.js b/packages/frontend-core/src/utils/download.js index f3701b357b..b887122969 100644 --- a/packages/frontend-core/src/utils/download.js +++ b/packages/frontend-core/src/utils/download.js @@ -44,24 +44,25 @@ export async function downloadFile(url, body) { body: JSON.stringify(body), }) - if (response.ok) { - const contentDisposition = response.headers.get("Content-Disposition") - - const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( - contentDisposition - ) - - const filename = matches[1].replace(/['"]/g, "") - - const url = URL.createObjectURL(await response.blob()) - - const link = document.createElement("a") - link.href = url - link.download = filename - link.click() - - URL.revokeObjectURL(url) - } else { - notifications.error("Error exporting the app.") + if (!response.ok) { + return false } + + const contentDisposition = response.headers.get("Content-Disposition") + + const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( + contentDisposition + ) + + const filename = matches[1].replace(/['"]/g, "") + + const url = URL.createObjectURL(await response.blob()) + + const link = document.createElement("a") + link.href = url + link.download = filename + link.click() + + URL.revokeObjectURL(url) + return true } From 19caf3cddfb6e93890f52aad5ace602c542ad754 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Mar 2024 11:44:32 +0100 Subject: [PATCH 005/112] Copy --- .../controls/ButtonActionEditor/manifest.json | 2 +- packages/client/src/utils/buttonActions.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 6d1794c991..2840a0d662 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -163,7 +163,7 @@ "component": "ClearRowSelection" }, { - "name": "Download file", + "name": "Download File", "type": "data", "component": "DownloadFile" } diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 68478b76ac..a4471ac4aa 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -400,6 +400,23 @@ const closeSidePanelHandler = () => { sidePanelStore.actions.close() } +const downloadFileHandler = (action, context) => { + download(action.parameters.value, `file.jpg`) + // const x = processStringSync(action.parameters.value, context) + // console.warn(x) + + // // Built total context for this action + // const totalContext = { + // ...context, + // state: get(stateStore), + // actions: buttonContext, + // } + + // action = enrichDataBindings(action, totalContext) + + // console.error(action) +} + const handlerMap = { ["Fetch Row"]: fetchRowHandler, ["Save Row"]: saveRowHandler, @@ -418,6 +435,7 @@ const handlerMap = { ["Prompt User"]: promptUserHandler, ["Open Side Panel"]: openSidePanelHandler, ["Close Side Panel"]: closeSidePanelHandler, + ["Download File"]: downloadFileHandler, } const confirmTextMap = { From df05cf23454c3ce1635d44087bb414923d2a1fb5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 25 Mar 2024 10:24:20 +0100 Subject: [PATCH 006/112] Lint --- packages/frontend-core/src/utils/download.js | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/frontend-core/src/utils/download.js b/packages/frontend-core/src/utils/download.js index b887122969..55ba899ee6 100644 --- a/packages/frontend-core/src/utils/download.js +++ b/packages/frontend-core/src/utils/download.js @@ -46,23 +46,23 @@ export async function downloadFile(url, body) { if (!response.ok) { return false + } else { + const contentDisposition = response.headers.get("Content-Disposition") + + const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( + contentDisposition + ) + + const filename = matches[1].replace(/['"]/g, "") + + const url = URL.createObjectURL(await response.blob()) + + const link = document.createElement("a") + link.href = url + link.download = filename + link.click() + + URL.revokeObjectURL(url) + return true } - - const contentDisposition = response.headers.get("Content-Disposition") - - const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( - contentDisposition - ) - - const filename = matches[1].replace(/['"]/g, "") - - const url = URL.createObjectURL(await response.blob()) - - const link = document.createElement("a") - link.href = url - link.download = filename - link.click() - - URL.revokeObjectURL(url) - return true } From 12f9b47954e987aa8acbc549a78fe56a3fc19949 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 25 Mar 2024 12:29:38 +0100 Subject: [PATCH 007/112] Implement download by url --- .../actions/DownloadFile.svelte | 74 +++++++++++++++++-- packages/client/src/utils/buttonActions.js | 26 ++++--- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte index 741b21a768..702f0cc911 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DownloadFile.svelte @@ -1,7 +1,9 @@
- (parameters.value = e.detail)} + table.label} + getOptionValue={table => table.resourceId} + /> + + opt.label} - getOptionValue={opt => opt.value} - on:change={e => (matchAny = e.detail === "or")} - placeholder={null} - /> - {#if datasource?.type === "table"} + {#if fieldOptions?.length} + {#if !rawFilters?.length} + Add your first filter expression. + {:else} +
onFieldChange(filter)} - placeholder="Column" - /> - onValueTypeChange(filter)} - placeholder={null} - /> - {#if filter.field && filter.valueType === "Binding"} - (filter.value = event.detail)} - /> - {:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)} - - {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} - - {:else if filter.type === "options"} - - {:else if filter.type === "boolean"} - - {:else if filter.type === "datetime"} - - {:else if filter.type === FieldType.BB_REFERENCE} - - {:else} - - {/if} - duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> - {/each} + {/if}
+
+
+ +
+
+ {#each rawFilters as filter} + onOperatorChange(filter)} + placeholder={null} + /> + + {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} + + {:else if filter.type === "options"} + + {:else if filter.type === "boolean"} + + {:else if filter.type === "datetime"} + + {:else if filter.type === FieldType.BB_REFERENCE} + + {:else} + + {/if} + duplicateFilter(filter.id)} + /> + removeFilter(filter.id)} + /> + {/each} +
+
+ {/if} +
+
+ {:else} + + None of the table column can be used for filtering. + {/if} -
- -
From a4a095b6a1cf5668ea906020c1c3fdd77b50df09 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 11:37:59 +0200 Subject: [PATCH 031/112] Handle formulas for getValidOperatorsForType --- packages/shared-core/src/filters.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index c2a8cfd416..95d5269c4b 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -2,6 +2,7 @@ import { Datasource, FieldSubtype, FieldType, + FormulaType, SearchFilter, SearchQuery, SearchQueryFields, @@ -19,7 +20,11 @@ const HBS_REGEX = /{{([^{].*?)}}/g * Returns the valid operator options for a certain data type */ export const getValidOperatorsForType = ( - fieldType: { type: FieldType; subtype?: FieldSubtype }, + fieldType: { + type: FieldType + subtype?: FieldSubtype + formulaType?: FormulaType + }, field: string, datasource: Datasource & { tableId: any } ) => { @@ -46,7 +51,7 @@ export const getValidOperatorsForType = ( value: string label: string }[] = [] - const { type, subtype } = fieldType + const { type, subtype, formulaType } = fieldType if (type === FieldType.STRING) { ops = stringOps } else if (type === FieldType.NUMBER || type === FieldType.BIGINT) { @@ -61,7 +66,7 @@ export const getValidOperatorsForType = ( ops = stringOps } else if (type === FieldType.DATETIME) { ops = numOps - } else if (type === FieldType.FORMULA) { + } else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) { ops = stringOps.concat([Op.MoreThan, Op.LessThan]) } else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) { ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In] From 709457477e801a35963a1b80191cbdb099a0cc38 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 12:29:45 +0200 Subject: [PATCH 032/112] Move filterbuilder to frontend-core --- .../controls/FilterEditor/FilterDrawer.svelte | 325 +++--------------- packages/frontend-core/package.json | 1 + .../src/components/FilterBuilder.svelte | 281 +++++++++++++++ .../frontend-core/src/components/index.js | 1 + 4 files changed, 325 insertions(+), 283 deletions(-) create mode 100644 packages/frontend-core/src/components/FilterBuilder.svelte diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index b4e45c9487..c235ea4591 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -1,25 +1,12 @@ - -
- - {#if fieldOptions?.length} - {#if !rawFilters?.length} - Add your first filter expression. - {:else} -
- opt.label} - getOptionValue={opt => opt.value} - on:change={e => (onEmptyFilter = e.detail)} - placeholder={null} - /> - {/if} -
-
-
- -
-
- {#each rawFilters as filter} - onOperatorChange(filter)} - placeholder={null} - /> - - {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} - - {:else if filter.type === "options"} - - {:else if filter.type === "boolean"} - - {:else if filter.type === "datetime"} - - {:else if filter.type === FieldType.BB_REFERENCE} - - {:else} - - {/if} - duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> - {/each} -
-
- {/if} -
- -
- {:else} - - None of the table column can be used for filtering. - + + +
+ opt.label} + getOptionValue={opt => opt.value} + on:change={e => (onEmptyFilter = e.detail)} + placeholder={null} + /> {/if} - -
+
+
+ (filter.value = event.detail)} + /> +
+
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/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte new file mode 100644 index 0000000000..67bfca524d --- /dev/null +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -0,0 +1,281 @@ + + +
+ + + {#if !filters?.length} + Add your first filter expression. + {:else} + + {/if} + + {#if filters?.length} +
+ +
+
+ {#each filters as filter} + onOperatorChange(filter)} + placeholder={null} + /> + {#if allowBindings} + + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} + + {:else if filter.type === FieldType.OPTIONS} + + {:else if filter.type === FieldType.BOOLEAN} + + {:else if filter.type === FieldType.DATETIME} + + {:else} + + {/if} +
+ duplicateFilter(filter.id)} + /> +
+
+ removeFilter(filter.id)} + /> +
+ {/each} +
+ {/if} +
+ +
+
+
+ + diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js index f71420b12b..0d4ff8ea35 100644 --- a/packages/frontend-core/src/components/index.js +++ b/packages/frontend-core/src/components/index.js @@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte" export { default as Updating } from "./Updating.svelte" export { Grid } from "./grid" export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte" +export { default as FilterBuilder } from "./FilterBuilder.svelte" From 3f85514a0a999ad8996921dfc995c12c17aa4d91 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 12:44:33 +0200 Subject: [PATCH 033/112] Fix --- .../settings/controls/FilterEditor/FilterDrawer.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index c235ea4591..af2b13d7e2 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -31,7 +31,7 @@ let matchAny = false let onEmptyFilter = "all" - $: parseFilters(filters) + $: parseFilters(rawFilters) $: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter)) // Remove field key prefixes and determine which behaviours to use @@ -79,7 +79,12 @@ - +
onFieldChange(filter)} - placeholder="Column" - /> - onValueTypeChange(filter)} + bind:value={filter.field} + options={fieldOptions} + on:change={() => onFieldChange(filter)} + placeholder="Column" + /> + - {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} - - {:else if filter.type === FieldType.OPTIONS} - - {:else if filter.type === FieldType.BOOLEAN} - - {:else if filter.type === FieldType.DATETIME} - - {:else} - - {/if} -
- duplicateFilter(filter.id)} - /> -
-
- removeFilter(filter.id)} - /> -
- {/each} + {#if allowBindings} + + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} + + {:else if filter.type === FieldType.OPTIONS} + + {:else if filter.type === FieldType.BOOLEAN} + + {:else if filter.type === FieldType.DATETIME} + + {:else} + + {/if} +
+ duplicateFilter(filter.id)} + /> +
+
+ removeFilter(filter.id)} + /> +
+ {/each} +
+ {/if} +
+
+ {:else} + None of the table column can be used for filtering. {/if} -
- -
From 6945ed5674e6bcc333ada1e6a9d4b5a032313b2d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 12:51:40 +0200 Subject: [PATCH 035/112] Format --- .../src/components/FilterBuilder.svelte | 156 +++++++++--------- 1 file changed, 79 insertions(+), 77 deletions(-) diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 5aef3d0fd0..9fee854649 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -157,86 +157,88 @@ {/if} {#if filters?.length} -
- -
-
- {#each filters as filter} - onOperatorChange(filter)} - placeholder={null} - /> - {#if allowBindings} +
+
+ +
+
+ {#each filters as filter} onOperatorChange(filter)} placeholder={null} /> - {/if} - {#if allowBindings && filter.field && filter.valueType === "Binding"} - - {:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)} - - {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} - - {:else if filter.type === FieldType.OPTIONS} - - {:else if filter.type === FieldType.BOOLEAN} - - {:else if filter.type === FieldType.DATETIME} - - {:else} - - {/if} -
- duplicateFilter(filter.id)} - /> -
-
- removeFilter(filter.id)} - /> -
- {/each} + {#if allowBindings} + + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} + + {:else if filter.type === FieldType.OPTIONS} + + {:else if filter.type === FieldType.BOOLEAN} + + {:else if filter.type === FieldType.DATETIME} + + {:else} + + {/if} +
+ duplicateFilter(filter.id)} + /> +
+
+ removeFilter(filter.id)} + /> +
+ {/each} +
{/if}
From ba171bb5a229c9e089484ef8926c6a04245bd410 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 11 Apr 2024 11:58:21 +0100 Subject: [PATCH 036/112] Reduce duplication in search.spec.ts --- .../src/api/routes/tests/search.spec.ts | 292 +++++++++--------- 1 file changed, 144 insertions(+), 148 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 3fabbfbef9..5be65553e4 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -6,11 +6,18 @@ import { Datasource, EmptyFilterOption, FieldType, - Row, - SearchFilters, + RowSearchParams, + SortOrder, Table, } from "@budibase/types" +function leftContainsRight< + A extends Record, + B extends Record +>(left: A, right: B) { + return Object.entries(right).every(([k, v]) => left[k] === v) +} + jest.unmock("mssql") describe.each([ @@ -47,10 +54,79 @@ describe.each([ } }) - describe("strings", () => { - beforeAll(async () => { - table = await config.api.table.save( - tableForDatasource(datasource, { + async function testSearch>( + test: SearchTest, + table: Table + ) { + const expected = test.expectToFind + delete test.expectToFind + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...test, + tableId: table._id!, + }) + if (!expected) { + return + } + expect(foundRows).toHaveLength(expected.length) + expect(foundRows).toEqual( + expect.arrayContaining( + expected.map(expectedRow => + expect.objectContaining( + foundRows.find(foundRow => leftContainsRight(foundRow, expectedRow)) + ) + ) + ) + ) + } + + function searchTests>( + name: string, + opts: { + table: (ds?: Datasource) => Promise + rows: T[] + tests: SearchTest[] + } + ) { + let table: Table + + for (const test of opts.tests) { + test.toString = () => { + const queryStr = JSON.stringify({ + query: test.query, + limit: test.limit, + sort: test.sort, + sortOrder: test.sortOrder, + }) + const expectStr = JSON.stringify(test.expectToFind) + return `should run: ${queryStr} and find ${expectStr}` + } + } + + // eslint-disable-next-line jest/valid-title + describe(name, () => { + beforeAll(async () => { + table = await opts.table(datasource) + }) + + beforeAll(async () => { + await Promise.all( + opts.rows.map(r => config.api.row.save(table._id!, r)) + ) + }) + + it.each(opts.tests)(`%s`, test => testSearch(test, table)) + }) + } + + interface SearchTest> + extends Omit { + expectToFind?: RowType[] + } + + searchTests("strings", { + table: async ds => { + return await config.api.table.save( + tableForDatasource(ds, { schema: { name: { name: "name", @@ -59,66 +135,38 @@ describe.each([ }, }) ) - }) + }, + rows: [{ name: "foo" }, { name: "bar" }], + tests: [ + // These test cases are generic and don't really need to be repeated for + // all data types, so we just do them here. - const rows = [{ name: "foo" }, { name: "bar" }] - let savedRows: Row[] - - beforeAll(async () => { - savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) - }) - - interface StringSearchTest { - query: SearchFilters - expected: (typeof rows)[number][] - } - - const stringSearchTests: StringSearchTest[] = [ - // These three test cases are generic and don't really need - // to be repeated for all data types, so we just do them here. - { query: {}, expected: rows }, + // @ts-expect-error - intentionally not passing a query to make sure the + // API can handle it. + { expectToFind: [{ name: "foo" }, { name: "bar" }] }, + { query: {}, expectToFind: [{ name: "foo" }, { name: "bar" }] }, { query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, - expected: rows, + expectToFind: [{ name: "foo" }, { name: "bar" }], }, { query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, - expected: [], + expectToFind: [], }, // The rest of these tests are specific to strings. - { query: { string: { name: "foo" } }, expected: [rows[0]] }, - { query: { string: { name: "none" } }, expected: [] }, - { query: { fuzzy: { name: "oo" } }, expected: [rows[0]] }, - { query: { equal: { name: "foo" } }, expected: [rows[0]] }, - { query: { notEqual: { name: "foo" } }, expected: [rows[1]] }, - { query: { oneOf: { name: ["foo"] } }, expected: [rows[0]] }, - ] - - it.each(stringSearchTests)( - `should be able to run query: $query`, - async ({ query, expected }) => { - const { rows: foundRows } = await config.api.row.search(table._id!, { - tableId: table._id!, - query, - }) - expect(foundRows).toHaveLength(expected.length) - expect(foundRows).toEqual( - expect.arrayContaining( - expected.map(r => - expect.objectContaining(savedRows.find(sr => sr.name === r.name)!) - ) - ) - ) - } - ) + { query: { string: { name: "foo" } }, expectToFind: [{ name: "foo" }] }, + { query: { string: { name: "none" } }, expectToFind: [] }, + { query: { fuzzy: { name: "oo" } }, expectToFind: [{ name: "foo" }] }, + { query: { equal: { name: "foo" } }, expectToFind: [{ name: "foo" }] }, + { query: { notEqual: { name: "foo" } }, expectToFind: [{ name: "bar" }] }, + { query: { oneOf: { name: ["foo"] } }, expectToFind: [{ name: "foo" }] }, + ], }) - describe("number", () => { - beforeAll(async () => { - table = await config.api.table.save( - tableForDatasource(datasource, { + searchTests("numbers", { + table: async ds => { + return await config.api.table.save( + tableForDatasource(ds, { schema: { age: { name: "age", @@ -127,56 +175,33 @@ describe.each([ }, }) ) - }) - - const rows = [{ age: 1 }, { age: 10 }] - let savedRows: Row[] - - beforeAll(async () => { - savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) - }) - - interface NumberSearchTest { - query: SearchFilters - expected: (typeof rows)[number][] - } - - const numberSearchTests: NumberSearchTest[] = [ - { query: { equal: { age: 1 } }, expected: [rows[0]] }, - { query: { equal: { age: 2 } }, expected: [] }, - { query: { notEqual: { age: 1 } }, expected: [rows[1]] }, - { query: { oneOf: { age: [1] } }, expected: [rows[0]] }, - { query: { range: { age: { low: 1, high: 5 } } }, expected: [rows[0]] }, - { query: { range: { age: { low: 0, high: 1 } } }, expected: [rows[0]] }, - { query: { range: { age: { low: 3, high: 4 } } }, expected: [] }, - { query: { range: { age: { low: 0, high: 11 } } }, expected: rows }, - ] - - it.each(numberSearchTests)( - `should be able to run query: $query`, - async ({ query, expected }) => { - const { rows: foundRows } = await config.api.row.search(table._id!, { - tableId: table._id!, - query, - }) - expect(foundRows).toHaveLength(expected.length) - expect(foundRows).toEqual( - expect.arrayContaining( - expected.map(r => - expect.objectContaining(savedRows.find(sr => sr.age === r.age)!) - ) - ) - ) - } - ) + }, + rows: [{ age: 1 }, { age: 10 }], + tests: [ + { query: { equal: { age: 1 } }, expectToFind: [{ age: 1 }] }, + { query: { equal: { age: 2 } }, expectToFind: [] }, + { query: { notEqual: { age: 1 } }, expectToFind: [{ age: 10 }] }, + { query: { oneOf: { age: [1] } }, expectToFind: [{ age: 1 }] }, + { + query: { range: { age: { low: 1, high: 5 } } }, + expectToFind: [{ age: 1 }], + }, + { + query: { range: { age: { low: 0, high: 1 } } }, + expectToFind: [{ age: 1 }], + }, + { query: { range: { age: { low: 3, high: 4 } } }, expectToFind: [] }, + { + query: { range: { age: { low: 0, high: 11 } } }, + expectToFind: [{ age: 1 }, { age: 10 }], + }, + ], }) - describe("dates", () => { - beforeEach(async () => { - table = await config.api.table.save( - tableForDatasource(datasource, { + searchTests("dates", { + table: async ds => { + return await config.api.table.save( + tableForDatasource(ds, { schema: { dob: { name: "dob", @@ -185,41 +210,27 @@ describe.each([ }, }) ) - }) - - const rows = [ + }, + rows: [ { dob: new Date("2020-01-01").toISOString() }, { dob: new Date("2020-01-10").toISOString() }, - ] - let savedRows: Row[] - - beforeEach(async () => { - savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) - }) - - interface DateSearchTest { - query: SearchFilters - expected: (typeof rows)[number][] - } - - const dateSearchTests: DateSearchTest[] = [ + ], + tests: [ { query: { equal: { dob: new Date("2020-01-01").toISOString() } }, - expected: [rows[0]], + expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], }, { query: { equal: { dob: new Date("2020-01-02").toISOString() } }, - expected: [], + expectToFind: [], }, { query: { notEqual: { dob: new Date("2020-01-01").toISOString() } }, - expected: [rows[1]], + expectToFind: [{ dob: new Date("2020-01-10").toISOString() }], }, { query: { oneOf: { dob: [new Date("2020-01-01").toISOString()] } }, - expected: [rows[0]], + expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], }, { query: { @@ -230,7 +241,7 @@ describe.each([ }, }, }, - expected: [rows[0]], + expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], }, { query: { @@ -241,7 +252,10 @@ describe.each([ }, }, }, - expected: rows, + expectToFind: [ + { dob: new Date("2020-01-01").toISOString() }, + { dob: new Date("2020-01-10").toISOString() }, + ], }, { query: { @@ -252,26 +266,8 @@ describe.each([ }, }, }, - expected: [rows[1]], + expectToFind: [{ dob: new Date("2020-01-10").toISOString() }], }, - ] - - it.each(dateSearchTests)( - `should be able to run query: $query`, - async ({ query, expected }) => { - const { rows: foundRows } = await config.api.row.search(table._id!, { - tableId: table._id!, - query, - }) - expect(foundRows).toHaveLength(expected.length) - expect(foundRows).toEqual( - expect.arrayContaining( - expected.map(r => - expect.objectContaining(savedRows.find(sr => sr.dob === r.dob)!) - ) - ) - ) - } - ) + ], }) }) From 0d564a8b4cd025415b4e0f8ca25c409f81e0d373 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 11 Apr 2024 11:58:49 +0100 Subject: [PATCH 037/112] Remove unused variables. --- packages/server/src/api/routes/tests/search.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 5be65553e4..79399a9272 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -7,7 +7,6 @@ import { EmptyFilterOption, FieldType, RowSearchParams, - SortOrder, Table, } from "@budibase/types" @@ -32,7 +31,6 @@ describe.each([ const config = setup.getConfig() let envCleanup: (() => void) | undefined - let table: Table let datasource: Datasource | undefined beforeAll(async () => { From eb56140ce25e7ad7cb36bbb301c0c8dae03d0e2a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 11 Apr 2024 12:03:34 +0100 Subject: [PATCH 038/112] Convert dates to strings, looks nicer and makes no difference. --- .../src/api/routes/tests/search.spec.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 79399a9272..a99df1a744 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -210,61 +210,61 @@ describe.each([ ) }, rows: [ - { dob: new Date("2020-01-01").toISOString() }, - { dob: new Date("2020-01-10").toISOString() }, + { dob: "2020-01-01T00:00:00.000Z" }, + { dob: "2020-01-10T00:00:00.000Z" }, ], tests: [ { - query: { equal: { dob: new Date("2020-01-01").toISOString() } }, - expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], + query: { equal: { dob: "2020-01-01T00:00:00.000Z" } }, + expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], }, { - query: { equal: { dob: new Date("2020-01-02").toISOString() } }, + query: { equal: { dob: "2020-01-02T00:00:00.000Z" } }, expectToFind: [], }, { - query: { notEqual: { dob: new Date("2020-01-01").toISOString() } }, - expectToFind: [{ dob: new Date("2020-01-10").toISOString() }], + query: { notEqual: { dob: "2020-01-01T00:00:00.000Z" } }, + expectToFind: [{ dob: "2020-01-10T00:00:00.000Z" }], }, { - query: { oneOf: { dob: [new Date("2020-01-01").toISOString()] } }, - expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], + query: { oneOf: { dob: ["2020-01-01T00:00:00.000Z"] } }, + expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], }, { query: { range: { dob: { - low: new Date("2020-01-01").toISOString(), - high: new Date("2020-01-05").toISOString(), + low: "2020-01-01T00:00:00.000Z", + high: "2020-01-05T00:00:00.000Z", }, }, }, - expectToFind: [{ dob: new Date("2020-01-01").toISOString() }], + expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], }, { query: { range: { dob: { - low: new Date("2020-01-01").toISOString(), - high: new Date("2020-01-10").toISOString(), + low: "2020-01-01T00:00:00.000Z", + high: "2020-01-10T00:00:00.000Z", }, }, }, expectToFind: [ - { dob: new Date("2020-01-01").toISOString() }, - { dob: new Date("2020-01-10").toISOString() }, + { dob: "2020-01-01T00:00:00.000Z" }, + { dob: "2020-01-10T00:00:00.000Z" }, ], }, { query: { range: { dob: { - low: new Date("2020-01-05").toISOString(), - high: new Date("2020-01-10").toISOString(), + low: "2020-01-05T00:00:00.000Z", + high: "2020-01-10T00:00:00.000Z", }, }, }, - expectToFind: [{ dob: new Date("2020-01-10").toISOString() }], + expectToFind: [{ dob: "2020-01-10T00:00:00.000Z" }], }, ], }) From 3201eb5953a8e8b6c5cf4d2b3b6e357ae6062f38 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Thu, 11 Apr 2024 12:53:32 +0100 Subject: [PATCH 039/112] adds sidepanel open and close actions, and gives the user the option to disable click-outside closure of sidepanel --- packages/client/manifest.json | 22 +++++++++++++- .../client/src/components/app/Layout.svelte | 6 +++- .../src/components/app/SidePanel.svelte | 29 +++++++++++++++++++ packages/client/src/stores/sidePanel.js | 9 ++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 08d614391b..2f52085e38 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6487,7 +6487,27 @@ "illegalChildren": ["section", "sidepanel"], "showEmptyState": false, "draggable": false, - "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action." + "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.", + "sendEvents": true, + "settings": [ + { + "type": "boolean", + "key": "clickOutsideToClose", + "label": "Click away to close", + "defaultValue": true + }, + { + "type": "event", + "key": "sidePanelOpen", + "label": "Side Panel Open" + }, + { + "type": "event", + "key": "sidePanelClose", + "label": "Side Panel Close", + "info": "Side panel actions configured here will run after the 'Open side panel' action runs, and are not capable of preventing or stopping it. Any form validation should therefore be done before that action if invoked, and not here." + } + ] }, "rowexplorer": { "block": true, diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index 992a166143..5b68171539 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -292,7 +292,11 @@ id="side-panel-container" class:open={$sidePanelStore.open} use:clickOutside={{ - callback: autoCloseSidePanel ? sidePanelStore.actions.close : null, + callback: + $sidePanelStore.clickOutsideToClose && autoCloseSidePanel + ? sidePanelStore.actions.close + : null, + allowedType: "mousedown", }} class:builder={$builderStore.inBuilder} diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index 825b401bb8..48c84828b0 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -5,6 +5,13 @@ const { styleable, sidePanelStore, builderStore, dndIsDragging } = getContext("sdk") + let handlingSidePanelOpen + let handlingSidePanelClose + + export let sidePanelOpen + export let sidePanelClose + export let clickOutsideToClose + // Automatically show and hide the side panel when inside the builder. // For some unknown reason, svelte reactivity breaks if we reference the // reactive variable "open" inside the following expression, or if we define @@ -26,6 +33,10 @@ } } + $: { + sidePanelStore.actions.setSidepanelState(clickOutsideToClose) + } + // Derive visibility $: open = $sidePanelStore.contentId === $component.id @@ -40,6 +51,22 @@ } } + const handleSidePanelOpen = async () => { + handlingSidePanelOpen = true + if (sidePanelOpen) { + await sidePanelOpen() + } + handlingSidePanelOpen = false + } + + const handleSidePanelClose = async () => { + handlingSidePanelClose = true + if (sidePanelClose) { + await sidePanelClose() + } + handlingSidePanelOpen = false + } + const showInSidePanel = (el, visible) => { const update = visible => { const target = document.getElementById("side-panel-container") @@ -47,10 +74,12 @@ if (visible) { if (!target.contains(node)) { target.appendChild(node) + handleSidePanelOpen() } } else { if (target.contains(node)) { target.removeChild(node) + handleSidePanelClose() } } } diff --git a/packages/client/src/stores/sidePanel.js b/packages/client/src/stores/sidePanel.js index 3b3b9f5f4d..df66eca01c 100644 --- a/packages/client/src/stores/sidePanel.js +++ b/packages/client/src/stores/sidePanel.js @@ -3,6 +3,7 @@ import { writable, derived } from "svelte/store" export const createSidePanelStore = () => { const initialState = { contentId: null, + clickOutsideToClose: true, } const store = writable(initialState) const derivedStore = derived(store, $store => { @@ -32,11 +33,19 @@ export const createSidePanelStore = () => { }, 50) } + const setSidepanelState = bool => { + clearTimeout(timeout) + store.update(state => { + state.clickOutsideToClose = bool + return state + }) + } return { subscribe: derivedStore.subscribe, actions: { open, close, + setSidepanelState, }, } } From 2fcdf2602ec5bdc0421e77634f995c574343aa0f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 16:00:56 +0200 Subject: [PATCH 040/112] Handle extra filters in base --- .../controls/FilterEditor/FilterDrawer.svelte | 70 +++++-------------- .../src/components/FilterBuilder.svelte | 66 ++++++++++++++++- 2 files changed, 82 insertions(+), 54 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index af2b13d7e2..49c2f17c59 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -1,5 +1,5 @@ -
- opt.label} - getOptionValue={opt => opt.value} - on:change={e => (onEmptyFilter = e.detail)} - placeholder={null} - /> - {/if} -
+
filter.operator === "allOr") != null + $: onEmptyFilter = + filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" + + $: console.warn(filters) + + const behaviourOptions = [ + { value: "and", label: "Match all filters" }, + { value: "or", label: "Match any filter" }, + ] + const onEmptyOptions = [ + { value: "all", label: "Return all table rows" }, + { value: "none", label: "Return no rows" }, + ] + const context = getContext("context") $: fieldOptions = (schemaFields ?? []) @@ -144,6 +160,22 @@ filter.value = filter.type === FieldType.ARRAY ? [] : null } } + + function handleAllOr(option) { + filters = filters.filter(f => f.operator !== "allOr") + if (option === "or") { + filters.push({ operator: "allOr" }) + } + } + + function handleOnEmptyFilter(value) { + const existingFilter = filters?.find(filter => filter.onEmptyFilter) + if (existingFilter) { + existingFilter.onEmptyFilter = value + } else { + filters.push({ onEmptyFilter: value }) + } + }
@@ -154,6 +186,30 @@ 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 filters?.length} @@ -162,7 +218,7 @@
- {#each filters as filter} + {#each filters.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter) as filter} {/if} diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterUsers.svelte b/packages/frontend-core/src/components/FilterUsers.svelte similarity index 100% rename from packages/builder/src/components/design/settings/controls/FilterEditor/FilterUsers.svelte rename to packages/frontend-core/src/components/FilterUsers.svelte From c07882b4526899912ec31787e496c1c06fc03872 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 11 Apr 2024 15:16:26 +0100 Subject: [PATCH 042/112] Restructure search.spec.ts to be much more readable. --- .../src/api/routes/tests/search.spec.ts | 406 +++++++++--------- 1 file changed, 200 insertions(+), 206 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index a99df1a744..fdf1ed7603 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -7,15 +7,11 @@ import { EmptyFilterOption, FieldType, RowSearchParams, + SearchFilters, Table, + TableSchema, } from "@budibase/types" - -function leftContainsRight< - A extends Record, - B extends Record ->(left: A, right: B) { - return Object.entries(right).every(([k, v]) => left[k] === v) -} +import _ from "lodash" jest.unmock("mssql") @@ -32,6 +28,7 @@ describe.each([ let envCleanup: (() => void) | undefined let datasource: Datasource | undefined + let table: Table beforeAll(async () => { if (isSqs) { @@ -52,220 +49,217 @@ describe.each([ } }) - async function testSearch>( - test: SearchTest, - table: Table - ) { - const expected = test.expectToFind - delete test.expectToFind - const { rows: foundRows } = await config.api.row.search(table._id!, { - ...test, - tableId: table._id!, - }) - if (!expected) { - return - } - expect(foundRows).toHaveLength(expected.length) - expect(foundRows).toEqual( - expect.arrayContaining( - expected.map(expectedRow => - expect.objectContaining( - foundRows.find(foundRow => leftContainsRight(foundRow, expectedRow)) - ) - ) - ) + async function createTable(schema: TableSchema) { + table = await config.api.table.save( + tableForDatasource(datasource, { schema }) ) } - function searchTests>( - name: string, - opts: { - table: (ds?: Datasource) => Promise
- rows: T[] - tests: SearchTest[] - } - ) { - let table: Table + async function createRows(rows: Record[]) { + await Promise.all(rows.map(r => config.api.row.save(table._id!, r))) + } - for (const test of opts.tests) { - test.toString = () => { - const queryStr = JSON.stringify({ - query: test.query, - limit: test.limit, - sort: test.sort, - sortOrder: test.sortOrder, - }) - const expectStr = JSON.stringify(test.expectToFind) - return `should run: ${queryStr} and find ${expectStr}` - } - } + class SearchAssertion { + constructor(private readonly query: RowSearchParams) {} - // eslint-disable-next-line jest/valid-title - describe(name, () => { - beforeAll(async () => { - table = await opts.table(datasource) + async toFind(expectedRows: any[]) { + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...this.query, + tableId: table._id!, }) - beforeAll(async () => { - await Promise.all( - opts.rows.map(r => config.api.row.save(table._id!, r)) + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) + ) + ) ) + ) + } + + async toFindNothing() { + await this.toFind([]) + } + } + + function expectSearch(query: Omit) { + return new SearchAssertion({ ...query, tableId: table._id! }) + } + + function expectQuery(query: SearchFilters) { + return expectSearch({ query }) + } + + describe("strings", () => { + beforeAll(async () => { + await createTable({ + name: { name: "name", type: FieldType.STRING }, + }) + await createRows([{ name: "foo" }, { name: "bar" }]) + }) + + describe("misc", () => { + it("should return all if no query is passed", () => + expectSearch({} as RowSearchParams).toFind([ + { name: "foo" }, + { name: "bar" }, + ])) + + it("should return all if empty query is passed", () => + expectQuery({}).toFind([{ name: "foo" }, { name: "bar" }])) + + it("should return all if onEmptyFilter is RETURN_ALL", () => + expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + }).toFind([{ name: "foo" }, { name: "bar" }])) + + it("should return nothing if onEmptyFilter is RETURN_NONE", () => + expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + }).toFindNothing()) + }) + + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { name: "foo" } }).toFind([{ name: "foo" }])) + + it("fails to find nonexistent row", () => + expectQuery({ equal: { name: "none" } }).toFindNothing()) + }) + + describe("notEqual", () => { + it("successfully finds a row", () => + expectQuery({ notEqual: { name: "foo" } }).toFind([{ name: "bar" }])) + + it("fails to find nonexistent row", () => + expectQuery({ notEqual: { name: "bar" } }).toFind([{ name: "foo" }])) + }) + + describe("oneOf", () => { + it("successfully finds a row", () => + expectQuery({ oneOf: { name: ["foo"] } }).toFind([{ name: "foo" }])) + + it("fails to find nonexistent row", () => + expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()) + }) + + describe("fuzzy", () => { + it("successfully finds a row", () => + expectQuery({ fuzzy: { name: "oo" } }).toFind([{ name: "foo" }])) + + it("fails to find nonexistent row", () => + expectQuery({ fuzzy: { name: "none" } }).toFindNothing()) + }) + }) + + describe("numbers", () => { + beforeAll(async () => { + await createTable({ + age: { name: "age", type: FieldType.NUMBER }, + }) + await createRows([{ age: 1 }, { age: 10 }]) + }) + + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { age: 1 } }).toFind([{ age: 1 }])) + + it("fails to find nonexistent row", () => + expectQuery({ equal: { age: 2 } }).toFindNothing()) + }) + + describe("notEqual", () => { + it("successfully finds a row", () => + expectQuery({ notEqual: { age: 1 } }).toFind([{ age: 10 }])) + + it("fails to find nonexistent row", () => + expectQuery({ notEqual: { age: 10 } }).toFind([{ age: 1 }])) + }) + + describe("oneOf", () => { + it("successfully finds a row", () => + expectQuery({ oneOf: { age: [1] } }).toFind([{ age: 1 }])) + + it("fails to find nonexistent row", () => + expectQuery({ oneOf: { age: [2] } }).toFindNothing()) + }) + + describe("range", () => { + it("successfully finds a row", () => + expectQuery({ + range: { age: { low: 1, high: 5 } }, + }).toFind([{ age: 1 }])) + + it("successfully finds multiple rows", () => + expectQuery({ + range: { age: { low: 1, high: 10 } }, + }).toFind([{ age: 1 }, { age: 10 }])) + + it("successfully finds a row with a high bound", () => + expectQuery({ + range: { age: { low: 5, high: 10 } }, + }).toFind([{ age: 10 }])) + }) + }) + + describe("dates", () => { + const JAN_1ST = "2020-01-01T00:00:00.000Z" + const JAN_2ND = "2020-01-02T00:00:00.000Z" + const JAN_5TH = "2020-01-05T00:00:00.000Z" + const JAN_10TH = "2020-01-10T00:00:00.000Z" + + beforeAll(async () => { + await createTable({ + dob: { name: "dob", type: FieldType.DATETIME }, }) - it.each(opts.tests)(`%s`, test => testSearch(test, table)) + await createRows([{ dob: JAN_1ST }, { dob: JAN_10TH }]) }) - } - interface SearchTest> - extends Omit { - expectToFind?: RowType[] - } + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { dob: JAN_1ST } }).toFind([{ dob: JAN_1ST }])) - searchTests("strings", { - table: async ds => { - return await config.api.table.save( - tableForDatasource(ds, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - }, - }) - ) - }, - rows: [{ name: "foo" }, { name: "bar" }], - tests: [ - // These test cases are generic and don't really need to be repeated for - // all data types, so we just do them here. + it("fails to find nonexistent row", () => + expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()) + }) - // @ts-expect-error - intentionally not passing a query to make sure the - // API can handle it. - { expectToFind: [{ name: "foo" }, { name: "bar" }] }, - { query: {}, expectToFind: [{ name: "foo" }, { name: "bar" }] }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, - expectToFind: [{ name: "foo" }, { name: "bar" }], - }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, - expectToFind: [], - }, - // The rest of these tests are specific to strings. - { query: { string: { name: "foo" } }, expectToFind: [{ name: "foo" }] }, - { query: { string: { name: "none" } }, expectToFind: [] }, - { query: { fuzzy: { name: "oo" } }, expectToFind: [{ name: "foo" }] }, - { query: { equal: { name: "foo" } }, expectToFind: [{ name: "foo" }] }, - { query: { notEqual: { name: "foo" } }, expectToFind: [{ name: "bar" }] }, - { query: { oneOf: { name: ["foo"] } }, expectToFind: [{ name: "foo" }] }, - ], - }) + describe("notEqual", () => { + it("successfully finds a row", () => + expectQuery({ notEqual: { dob: JAN_1ST } }).toFind([{ dob: JAN_10TH }])) - searchTests("numbers", { - table: async ds => { - return await config.api.table.save( - tableForDatasource(ds, { - schema: { - age: { - name: "age", - type: FieldType.NUMBER, - }, - }, - }) - ) - }, - rows: [{ age: 1 }, { age: 10 }], - tests: [ - { query: { equal: { age: 1 } }, expectToFind: [{ age: 1 }] }, - { query: { equal: { age: 2 } }, expectToFind: [] }, - { query: { notEqual: { age: 1 } }, expectToFind: [{ age: 10 }] }, - { query: { oneOf: { age: [1] } }, expectToFind: [{ age: 1 }] }, - { - query: { range: { age: { low: 1, high: 5 } } }, - expectToFind: [{ age: 1 }], - }, - { - query: { range: { age: { low: 0, high: 1 } } }, - expectToFind: [{ age: 1 }], - }, - { query: { range: { age: { low: 3, high: 4 } } }, expectToFind: [] }, - { - query: { range: { age: { low: 0, high: 11 } } }, - expectToFind: [{ age: 1 }, { age: 10 }], - }, - ], - }) + it("fails to find nonexistent row", () => + expectQuery({ notEqual: { dob: JAN_10TH } }).toFind([{ dob: JAN_1ST }])) + }) - searchTests("dates", { - table: async ds => { - return await config.api.table.save( - tableForDatasource(ds, { - schema: { - dob: { - name: "dob", - type: FieldType.DATETIME, - }, - }, - }) - ) - }, - rows: [ - { dob: "2020-01-01T00:00:00.000Z" }, - { dob: "2020-01-10T00:00:00.000Z" }, - ], - tests: [ - { - query: { equal: { dob: "2020-01-01T00:00:00.000Z" } }, - expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], - }, - { - query: { equal: { dob: "2020-01-02T00:00:00.000Z" } }, - expectToFind: [], - }, - { - query: { notEqual: { dob: "2020-01-01T00:00:00.000Z" } }, - expectToFind: [{ dob: "2020-01-10T00:00:00.000Z" }], - }, - { - query: { oneOf: { dob: ["2020-01-01T00:00:00.000Z"] } }, - expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], - }, - { - query: { - range: { - dob: { - low: "2020-01-01T00:00:00.000Z", - high: "2020-01-05T00:00:00.000Z", - }, - }, - }, - expectToFind: [{ dob: "2020-01-01T00:00:00.000Z" }], - }, - { - query: { - range: { - dob: { - low: "2020-01-01T00:00:00.000Z", - high: "2020-01-10T00:00:00.000Z", - }, - }, - }, - expectToFind: [ - { dob: "2020-01-01T00:00:00.000Z" }, - { dob: "2020-01-10T00:00:00.000Z" }, - ], - }, - { - query: { - range: { - dob: { - low: "2020-01-05T00:00:00.000Z", - high: "2020-01-10T00:00:00.000Z", - }, - }, - }, - expectToFind: [{ dob: "2020-01-10T00:00:00.000Z" }], - }, - ], + describe("oneOf", () => { + it("successfully finds a row", () => + expectQuery({ oneOf: { dob: [JAN_1ST] } }).toFind([{ dob: JAN_1ST }])) + + it("fails to find nonexistent row", () => + expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing()) + }) + + describe("range", () => { + it("successfully finds a row", () => + expectQuery({ + range: { dob: { low: JAN_1ST, high: JAN_5TH } }, + }).toFind([{ dob: JAN_1ST }])) + + it("successfully finds multiple rows", () => + expectQuery({ + range: { dob: { low: JAN_1ST, high: JAN_10TH } }, + }).toFind([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + + it("successfully finds a row with a high bound", () => + expectQuery({ + range: { dob: { low: JAN_5TH, high: JAN_10TH } }, + }).toFind([{ dob: JAN_10TH }])) + }) }) }) From 1f77b09eed7a644136da163e39d5ec2ab7acaedb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 16:22:16 +0200 Subject: [PATCH 043/112] Handle drawers --- .../SetupPanel/AutomationBlockSetup.svelte | 4 +- .../buttons/TableFilterButton.svelte | 4 +- ...lterDrawer.svelte => FilterBuilder.svelte} | 53 +++++++------------ .../controls/FilterEditor/FilterEditor.svelte | 27 ++++++---- .../app/dynamic-filter/FilterModal.svelte | 7 ++- 5 files changed, 47 insertions(+), 48 deletions(-) rename packages/builder/src/components/design/settings/controls/FilterEditor/{FilterDrawer.svelte => FilterBuilder.svelte} (67%) diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 0632993cf0..717905eb90 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -37,7 +37,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,7 +442,7 @@ - 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)} >
- - import { DrawerContent } from "@budibase/bbui" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" @@ -61,35 +60,23 @@ } - - -
-
- (filter.value = event.detail)} - /> -
- - - - + +
+
+ (filter.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 @@ @@ -24,9 +24,9 @@
{ - sidePanelVisble = false + sidePanelVisible = false }} > diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 2f52085e38..f7d437a4fd 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6505,7 +6505,7 @@ "type": "event", "key": "sidePanelClose", "label": "Side Panel Close", - "info": "Side panel actions configured here will run after the 'Open side panel' action runs, and are not capable of preventing or stopping it. Any form validation should therefore be done before that action if invoked, and not here." + "info": "Side panel actions configured here will run after the 'Open side panel' action runs, and are not capable of preventing or stopping it. Any form validation should therefore be done before that action is invoked, and not here." } ] }, diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index 48c84828b0..98398c4671 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -5,9 +5,6 @@ const { styleable, sidePanelStore, builderStore, dndIsDragging } = getContext("sdk") - let handlingSidePanelOpen - let handlingSidePanelClose - export let sidePanelOpen export let sidePanelClose export let clickOutsideToClose @@ -52,19 +49,15 @@ } const handleSidePanelOpen = async () => { - handlingSidePanelOpen = true if (sidePanelOpen) { await sidePanelOpen() } - handlingSidePanelOpen = false } const handleSidePanelClose = async () => { - handlingSidePanelClose = true if (sidePanelClose) { await sidePanelClose() } - handlingSidePanelOpen = false } const showInSidePanel = (el, visible) => { From fc96d51519a08b932bcbe5b614f255b800a3691d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Apr 2024 16:45:28 +0200 Subject: [PATCH 047/112] Fix grid --- .../frontend-core/src/components/FilterBuilder.svelte | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index c7c0d45994..0972bd278c 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -218,7 +218,7 @@
-
+
{#each filters.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter) as filter} onFieldChange(filter, e.detail)} - placeholder="Column" - /> - - {: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} -
- -
- -
- - + diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 14bbc47e86..9125be401b 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -29,8 +29,6 @@ $: onEmptyFilter = filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" - $: console.warn(filters) - const behaviourOptions = [ { value: "and", label: "Match all filters" }, { value: "or", label: "Match any filter" }, From 4b382200481d3e040c1149d417ae4e01e02101dc Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 12 Apr 2024 09:50:11 +0200 Subject: [PATCH 057/112] Fix drawer --- .../frontend-core/src/components/FilterBuilder.svelte | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 9125be401b..dfdf48a6ec 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -29,6 +29,10 @@ $: onEmptyFilter = filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" + $: fieldFilters = filters.filter( + filter => filter.operator !== "allOr" && !filter.onEmptyFilter + ) + const behaviourOptions = [ { value: "and", label: "Match all filters" }, { value: "or", label: "Match any filter" }, @@ -177,7 +181,7 @@ {#if fieldOptions?.length} - {#if !filters?.length} + {#if !fieldFilters?.length} Add your first filter expression. {:else} @@ -207,13 +211,13 @@ {/if} {/if} - {#if filters?.length} + {#if fieldFilters?.length}
- {#each filters.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter) as filter} + {#each fieldFilters as filter} {#if fieldFilters?.length}
-
- -
+ {#if filtersLabel} +
+ +
+ {/if}
{#each fieldFilters as filter} Date: Fri, 12 Apr 2024 10:27:34 +0200 Subject: [PATCH 062/112] Fix api --- packages/frontend-core/src/components/FilterUsers.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/FilterUsers.svelte b/packages/frontend-core/src/components/FilterUsers.svelte index 88383ba170..1712d7ebdf 100644 --- a/packages/frontend-core/src/components/FilterUsers.svelte +++ b/packages/frontend-core/src/components/FilterUsers.svelte @@ -1,9 +1,9 @@ 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/src/api/attachments.js b/packages/frontend-core/src/api/attachments.js index f79b461574..e3b1b74e5b 100644 --- a/packages/frontend-core/src/api/attachments.js +++ b/packages/frontend-core/src/api/attachments.js @@ -61,32 +61,5 @@ 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, - }, - }) - }, } } diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index 3a1f165b6e..e7dc51e5d5 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -61,14 +61,6 @@ } } - const deleteAttachments = async fileList => { - try { - return await API.deleteBuilderAttachments(fileList) - } catch (error) { - return [] - } - } - onMount(() => { api = { focus: () => open(), @@ -101,7 +93,6 @@ on:change={e => onChange(e.detail)} maximum={maximum || schema.constraints?.length?.maximum} {processFiles} - {deleteAttachments} {handleFileTooLarge} />
From 205858c6d7aa5119d62336bbf86b5c72236dc72b Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Fri, 12 Apr 2024 13:27:41 +0200 Subject: [PATCH 072/112] Account portal submodule to master --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index edd67c7653..bd0e01d639 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit edd67c7653a4ca4daa39f907d4045dd22892f819 +Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344 From 0514641f049285e6139b59a54744d1e79d103105 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Fri, 12 Apr 2024 13:24:21 +0100 Subject: [PATCH 073/112] removes unecessary on-sidepanel-open actions feature. --- packages/client/manifest.json | 2 +- packages/client/src/components/app/Layout.svelte | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 622381847c..f370b67670 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6729,7 +6729,7 @@ { "type": "boolean", "key": "clickOutsideToClose", - "label": "Click away to close", + "label": "Click outside to close", "defaultValue": true }, { diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index acef8001a8..bae2bd0faf 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -73,7 +73,10 @@ $context.device.width, $context.device.height ) - $: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open + $: autoCloseSidePanel = + !$builderStore.inBuilder && + $sidePanelStore.open && + $sidePanelStore.clickOutsideToClose $: screenId = $builderStore.inBuilder ? `${$builderStore.screen?._id}-screen` : "screen" @@ -317,11 +320,7 @@ id="side-panel-container" class:open={$sidePanelStore.open} use:clickOutside={{ - callback: - $sidePanelStore.clickOutsideToClose && autoCloseSidePanel - ? sidePanelStore.actions.close - : null, - + callback: autoCloseSidePanel ? sidePanelStore.actions.close : null, allowedType: "mousedown", }} class:builder={$builderStore.inBuilder} From 5a1de4b45cbed80f743ce1ab2c15c151ff47bfe7 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 12 Apr 2024 13:24:45 +0000 Subject: [PATCH 074/112] Bump version to 2.23.5 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 78a3aa13e9..9839b8b166 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.23.4", + "version": "2.23.5", "npmClient": "yarn", "packages": [ "packages/*", From 1632c9d7a84f74e94fa07a0615517e49fd3cc992 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Fri, 12 Apr 2024 14:33:46 +0100 Subject: [PATCH 075/112] removes unused function --- packages/client/src/components/app/SidePanel.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index 8e0dcd99e4..624617ad69 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -47,12 +47,6 @@ } } - const handleSidePanelOpen = async () => { - if (sidePanelOpen) { - await sidePanelOpen() - } - } - const handleSidePanelClose = async () => { if (sidePanelClose) { await sidePanelClose() From 565ee5f7dac0edee614ba841eeef49adf152dfe3 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Fri, 12 Apr 2024 15:23:24 +0100 Subject: [PATCH 076/112] brings key and label into line with standard practices. Removes unecessary info. --- packages/client/manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index f370b67670..c9e28e202b 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6734,9 +6734,8 @@ }, { "type": "event", - "key": "sidePanelClose", - "label": "Side Panel Close", - "info": "Side panel actions configured here will run after the 'Open side panel' action runs, and are not capable of preventing or stopping it. Any form validation should therefore be done before that action is invoked, and not here." + "key": "onSidePanelClose", + "label": "On side panel close" } ] }, From ebb79c16fe7d626d9d201fe5d1090da6e4ab078f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Apr 2024 16:15:36 +0100 Subject: [PATCH 077/112] Aliasing support for SQS. --- .../api/controllers/row/ExternalRequest.ts | 3 +- packages/server/src/db/utils.ts | 1 + .../src/integrations/tests/sqlAlias.spec.ts | 4 +- packages/server/src/sdk/app/rows/index.ts | 2 + .../server/src/sdk/app/rows/search/sqs.ts | 99 +++++++++++-------- .../row/alias.ts => sdk/app/rows/sqlAlias.ts} | 28 ++++-- 6 files changed, 86 insertions(+), 51 deletions(-) rename packages/server/src/{api/controllers/row/alias.ts => sdk/app/rows/sqlAlias.ts} (87%) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 7fc0333de1..4adbb72c7a 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -36,7 +36,6 @@ import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils" import { processObjectSync } from "@budibase/string-templates" import { cloneDeep } from "lodash/fp" import { db as dbCore } from "@budibase/backend-core" -import AliasTables from "./alias" import sdk from "../../../sdk" import env from "../../../environment" @@ -618,7 +617,7 @@ export class ExternalRequest { if (env.SQL_ALIASING_DISABLE) { response = await getDatasourceAndQuery(json) } else { - const aliasing = new AliasTables(Object.keys(this.tables)) + const aliasing = new sdk.rows.AliasTables(Object.keys(this.tables)) response = await aliasing.queryWithAliasing(json) } diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index b1c02b1764..ce8d0accbb 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -40,6 +40,7 @@ export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.Inte export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}` export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}` export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}` +export const SQS_DATASOURCE_INTERNAL = "internal" export const ViewName = dbCore.ViewName export const InternalTables = dbCore.InternalTable export const UNICODE_MAX = dbCore.UNICODE_MAX diff --git a/packages/server/src/integrations/tests/sqlAlias.spec.ts b/packages/server/src/integrations/tests/sqlAlias.spec.ts index bfca24ff7d..58c3a05245 100644 --- a/packages/server/src/integrations/tests/sqlAlias.spec.ts +++ b/packages/server/src/integrations/tests/sqlAlias.spec.ts @@ -8,8 +8,10 @@ import { import { join } from "path" import Sql from "../base/sql" import { SqlClient } from "../utils" -import AliasTables from "../../api/controllers/row/alias" import { generator } from "@budibase/backend-core/tests" +import sdk from "../../sdk" + +const AliasTables = sdk.rows.AliasTables function multiline(sql: string) { return sql.replace(/\n/g, "").replace(/ +/g, " ") diff --git a/packages/server/src/sdk/app/rows/index.ts b/packages/server/src/sdk/app/rows/index.ts index ea501e93d9..c117941419 100644 --- a/packages/server/src/sdk/app/rows/index.ts +++ b/packages/server/src/sdk/app/rows/index.ts @@ -3,6 +3,7 @@ import * as rows from "./rows" import * as search from "./search" import * as utils from "./utils" import * as external from "./external" +import AliasTables from "./sqlAlias" export default { ...attachments, @@ -10,4 +11,5 @@ export default { ...search, utils, external, + AliasTables, } diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 5b0b6e3bc7..20edb988d3 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -20,7 +20,12 @@ import { } from "../../../../api/controllers/row/utils" import sdk from "../../../index" import { context } from "@budibase/backend-core" -import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils" +import { + CONSTANT_INTERNAL_ROW_COLS, + SQS_DATASOURCE_INTERNAL, +} from "../../../../db/utils" +import AliasTables from "../sqlAlias" +import { outputProcessing } from "../../../../utilities/rowProcessor" function buildInternalFieldList( table: Table, @@ -31,19 +36,19 @@ function buildInternalFieldList( fieldList = fieldList.concat( CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`) ) - if (opts.relationships) { - for (let col of Object.values(table.schema)) { - if (col.type === FieldType.LINK) { - const linkCol = col as RelationshipFieldMetadata - const relatedTable = tables.find( - table => table._id === linkCol.tableId - )! - fieldList = fieldList.concat( - buildInternalFieldList(relatedTable, tables, { relationships: false }) - ) - } else { - fieldList.push(`${table._id}.${col.name}`) - } + for (let col of Object.values(table.schema)) { + const isRelationship = col.type === FieldType.LINK + if (!opts.relationships && isRelationship) { + continue + } + if (isRelationship) { + const linkCol = col as RelationshipFieldMetadata + const relatedTable = tables.find(table => table._id === linkCol.tableId)! + fieldList = fieldList.concat( + buildInternalFieldList(relatedTable, tables, { relationships: false }) + ) + } else { + fieldList.push(`${table._id}.${col.name}`) } } return fieldList @@ -94,14 +99,14 @@ function buildTableMap(tables: Table[]) { } export async function search( - options: RowSearchParams + options: RowSearchParams, + table: Table ): Promise> { - const { tableId, paginate, query, ...params } = options + const { paginate, query, ...params } = options const builder = new SqlQueryBuilder(SqlClient.SQL_LITE) const allTables = await sdk.tables.getAllInternalTables() const allTablesMap = buildTableMap(allTables) - const table = allTables.find(table => table._id === tableId) if (!table) { throw new Error("Unable to find table") } @@ -111,7 +116,7 @@ export async function search( const request: QueryJson = { endpoint: { // not important, we query ourselves - datasourceId: "internal", + datasourceId: SQS_DATASOURCE_INTERNAL, entityId: table._id!, operation: Operation.READ, }, @@ -154,34 +159,44 @@ export async function search( } } try { - const query = builder._query(request, { - disableReturning: true, + const alias = new AliasTables(allTables.map(table => table.name)) + const rows = await alias.queryWithAliasing(request, async json => { + const query = builder._query(json, { + disableReturning: true, + }) + + if (Array.isArray(query)) { + throw new Error("SQS cannot currently handle multiple queries") + } + + let sql = query.sql, + bindings = query.bindings + + // quick hack for docIds + sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`") + sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`") + + const db = context.getAppDB() + return await db.sql(sql, bindings) }) - if (Array.isArray(query)) { - throw new Error("SQS cannot currently handle multiple queries") - } - - let sql = query.sql, - bindings = query.bindings - - // quick hack for docIds - sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`") - sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`") - - const db = context.getAppDB() - const rows = await db.sql(sql, bindings) + // process from the format of tableId.column to expected format + const processed = await sqlOutputProcessing( + rows, + table!, + allTablesMap, + relationships, + { + sqs: true, + } + ) return { - rows: await sqlOutputProcessing( - rows, - table!, - allTablesMap, - relationships, - { - sqs: true, - } - ), + // final row processing for response + rows: await outputProcessing(table, processed, { + preserveLinks: true, + squash: true, + }), } } catch (err: any) { const msg = typeof err === "string" ? err : err.message diff --git a/packages/server/src/api/controllers/row/alias.ts b/packages/server/src/sdk/app/rows/sqlAlias.ts similarity index 87% rename from packages/server/src/api/controllers/row/alias.ts rename to packages/server/src/sdk/app/rows/sqlAlias.ts index 0ec9d1a09c..0fc338ecbe 100644 --- a/packages/server/src/api/controllers/row/alias.ts +++ b/packages/server/src/sdk/app/rows/sqlAlias.ts @@ -6,11 +6,12 @@ import { Row, SearchFilters, } from "@budibase/types" -import { getSQLClient } from "../../../sdk/app/rows/utils" +import { getSQLClient } from "./utils" import { cloneDeep } from "lodash" -import sdk from "../../../sdk" +import datasources from "../datasources" import { makeExternalQuery } from "../../../integrations/base/query" import { SqlClient } from "../../../integrations/utils" +import { SQS_DATASOURCE_INTERNAL } from "../../../db/utils" const WRITE_OPERATIONS: Operation[] = [ Operation.CREATE, @@ -156,12 +157,19 @@ export default class AliasTables { } async queryWithAliasing( - json: QueryJson + json: QueryJson, + queryFn?: (json: QueryJson) => Promise ): Promise { const datasourceId = json.endpoint.datasourceId - const datasource = await sdk.datasources.get(datasourceId) + const isSqs = datasourceId === SQS_DATASOURCE_INTERNAL + let aliasingEnabled: boolean, datasource: Datasource | undefined + if (isSqs) { + aliasingEnabled = true + } else { + datasource = await datasources.get(datasourceId) + aliasingEnabled = this.isAliasingEnabled(json, datasource) + } - const aliasingEnabled = this.isAliasingEnabled(json, datasource) if (aliasingEnabled) { json = cloneDeep(json) // run through the query json to update anywhere a table may be used @@ -207,7 +215,15 @@ export default class AliasTables { } json.tableAliases = invertedTableAliases } - const response = await makeExternalQuery(datasource, json) + + let response: DatasourcePlusQueryResponse + if (datasource && !isSqs) { + response = await makeExternalQuery(datasource, json) + } else if (queryFn) { + response = await queryFn(json) + } else { + throw new Error("No supplied method to perform aliased query") + } if (Array.isArray(response) && aliasingEnabled) { return this.reverse(response) } else { From c40e9656345f5e95ead009976ab00755973b4845 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Apr 2024 16:16:31 +0100 Subject: [PATCH 078/112] Getting relationships working properly as well as renaming internal -> sqs in function opts. --- packages/server/src/api/controllers/row/utils/basic.ts | 9 +++++---- .../server/src/api/controllers/row/utils/sqlUtils.ts | 4 ++-- packages/server/src/api/controllers/row/utils/utils.ts | 6 ++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/controllers/row/utils/basic.ts b/packages/server/src/api/controllers/row/utils/basic.ts index 1fc84de9c7..6255e13c1c 100644 --- a/packages/server/src/api/controllers/row/utils/basic.ts +++ b/packages/server/src/api/controllers/row/utils/basic.ts @@ -62,12 +62,12 @@ export function basicProcessing({ row, table, isLinked, - internal, + sqs, }: { row: Row table: Table isLinked: boolean - internal?: boolean + sqs?: boolean }): Row { const thisRow: Row = {} // filter the row down to what is actually the row (not joined) @@ -84,12 +84,13 @@ export function basicProcessing({ thisRow[fieldName] = value } } - if (!internal) { + if (!sqs) { thisRow._id = generateIdForRow(row, table, isLinked) thisRow.tableId = table._id thisRow._rev = "rev" } else { - for (let internalColumn of CONSTANT_INTERNAL_ROW_COLS) { + const columns = Object.keys(table.schema) + for (let internalColumn of [...CONSTANT_INTERNAL_ROW_COLS, ...columns]) { thisRow[internalColumn] = extractFieldValue({ row, tableName: table._id!, diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 6f9837e0ab..372b8394ff 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -51,11 +51,11 @@ export async function updateRelationshipColumns( continue } - let linked = await basicProcessing({ + let linked = basicProcessing({ row, table: linkedTable, isLinked: true, - internal: opts?.sqs, + sqs: opts?.sqs, }) if (!linked._id) { continue diff --git a/packages/server/src/api/controllers/row/utils/utils.ts b/packages/server/src/api/controllers/row/utils/utils.ts index f387a468cf..bf9ede6fe3 100644 --- a/packages/server/src/api/controllers/row/utils/utils.ts +++ b/packages/server/src/api/controllers/row/utils/utils.ts @@ -132,6 +132,7 @@ export async function sqlOutputProcessing( let rowId = row._id if (opts?.sqs) { rowId = getInternalRowId(row, table) + row._id = rowId } else if (!rowId) { rowId = generateIdForRow(row, table) row._id = rowId @@ -153,7 +154,7 @@ export async function sqlOutputProcessing( row, table, isLinked: false, - internal: opts?.sqs, + sqs: opts?.sqs, }), table ) @@ -167,7 +168,8 @@ export async function sqlOutputProcessing( tables, row, finalRows, - relationships + relationships, + opts ) } From bfb7750213400e833bfb4ffeae0be9462b66e0bc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Apr 2024 16:17:06 +0100 Subject: [PATCH 079/112] Getting search input mapping up a level in the search SDK - avoids having to call it for every search type. --- packages/server/src/sdk/app/rows/search.ts | 11 +++++++--- .../src/sdk/app/rows/search/external.ts | 13 +++++++----- .../src/sdk/app/rows/search/internal.ts | 20 ++++++++++--------- .../app/rows/search/tests/external.spec.ts | 6 +++--- .../app/rows/search/tests/internal.spec.ts | 4 ++-- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index f681bfeb90..5d8f7ef80b 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -13,6 +13,8 @@ import * as sqs from "./search/sqs" import env from "../../../environment" import { ExportRowsParams, ExportRowsResult } from "./search/types" import { dataFilters } from "@budibase/shared-core" +import sdk from "../../index" +import { searchInputMapping } from "./search/utils" export { isValidFilter } from "../../../integrations/utils" @@ -72,12 +74,15 @@ export async function search( } } + const table = await sdk.tables.getTable(options.tableId) + options = searchInputMapping(table, options) + if (isExternalTable) { - return external.search(options) + return external.search(options, table) } else if (env.SQS_SEARCH_ENABLE) { - return sqs.search(options) + return sqs.search(options, table) } else { - return internal.search(options) + return internal.search(options, table) } } diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts index e0a3bad94e..077f971903 100644 --- a/packages/server/src/sdk/app/rows/search/external.ts +++ b/packages/server/src/sdk/app/rows/search/external.ts @@ -8,6 +8,7 @@ import { SearchFilters, RowSearchParams, SearchResponse, + Table, } from "@budibase/types" import * as exporters from "../../../../api/controllers/view/exporters" import { handleRequest } from "../../../../api/controllers/row/external" @@ -18,13 +19,13 @@ import { import { utils } from "@budibase/shared-core" import { ExportRowsParams, ExportRowsResult } from "./types" import { HTTPError, db } from "@budibase/backend-core" -import { searchInputMapping } from "./utils" import pick from "lodash/pick" import { outputProcessing } from "../../../../utilities/rowProcessor" import sdk from "../../../" export async function search( - options: RowSearchParams + options: RowSearchParams, + table: Table ): Promise> { const { tableId } = options const { paginate, query, ...params } = options @@ -68,8 +69,6 @@ export async function search( } try { - const table = await sdk.tables.getTable(tableId) - options = searchInputMapping(table, options) let rows = await handleRequest(Operation.READ, tableId, { filters: query, sort, @@ -150,11 +149,15 @@ export async function exportRows( } const datasource = await sdk.datasources.get(datasourceId!) + const table = await sdk.tables.getTable(tableId) if (!datasource || !datasource.entities) { throw new HTTPError("Datasource has not been configured for plus API.", 400) } - let result = await search({ tableId, query: requestQuery, sort, sortOrder }) + let result = await search( + { tableId, query: requestQuery, sort, sortOrder }, + table + ) let rows: Row[] = [] let headers diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts index 610807a10e..ffd13ed731 100644 --- a/packages/server/src/sdk/app/rows/search/internal.ts +++ b/packages/server/src/sdk/app/rows/search/internal.ts @@ -33,7 +33,8 @@ import pick from "lodash/pick" import { breakRowIdField } from "../../../../integrations/utils" export async function search( - options: RowSearchParams + options: RowSearchParams, + table: Table ): Promise> { const { tableId } = options @@ -51,8 +52,6 @@ export async function search( query: {}, } - let table = await sdk.tables.getTable(tableId) - options = searchInputMapping(table, options) if (params.sort && !params.sortType) { const schema = table.schema const sortField = schema[params.sort] @@ -122,12 +121,15 @@ export async function exportRows( result = await outputProcessing(table, response) } else if (query) { - let searchResponse = await search({ - tableId, - query, - sort, - sortOrder, - }) + let searchResponse = await search( + { + tableId, + query, + sort, + sortOrder, + }, + table + ) result = searchResponse.rows } diff --git a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts index f2bdec4692..53bc049a9b 100644 --- a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts @@ -112,7 +112,7 @@ describe("external search", () => { tableId, query: {}, } - const result = await search(searchParams) + const result = await search(searchParams, config.table!) expect(result.rows).toHaveLength(10) expect(result.rows).toEqual( @@ -130,7 +130,7 @@ describe("external search", () => { query: {}, fields: ["name", "age"], } - const result = await search(searchParams) + const result = await search(searchParams, config.table!) expect(result.rows).toHaveLength(10) expect(result.rows).toEqual( @@ -157,7 +157,7 @@ describe("external search", () => { }, }, } - const result = await search(searchParams) + const result = await search(searchParams, config.table!) expect(result.rows).toHaveLength(3) expect(result.rows.map(row => row.id)).toEqual([1, 4, 8]) diff --git a/packages/server/src/sdk/app/rows/search/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/search/tests/internal.spec.ts index 5be0f4a258..1c5f396737 100644 --- a/packages/server/src/sdk/app/rows/search/tests/internal.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/internal.spec.ts @@ -81,7 +81,7 @@ describe("internal", () => { tableId, query: {}, } - const result = await search(searchParams) + const result = await search(searchParams, config.table!) expect(result.rows).toHaveLength(10) expect(result.rows).toEqual( @@ -99,7 +99,7 @@ describe("internal", () => { query: {}, fields: ["name", "age"], } - const result = await search(searchParams) + const result = await search(searchParams, config.table!) expect(result.rows).toHaveLength(10) expect(result.rows).toEqual( From 7d7de33cabbcfcefb7ea668a92813b96c1e28b3b Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Apr 2024 16:29:48 +0100 Subject: [PATCH 080/112] Removing CouchDB SQS image for now. --- hosting/docker-compose.dev.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index 77f6bd053b..9dba5d427c 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -42,13 +42,12 @@ services: couchdb-service: container_name: budi-couchdb3-dev restart: on-failure - image: budibase/couchdb:v3.2.1-sqs + image: budibase/couchdb environment: - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} - COUCHDB_USER=${COUCH_DB_USER} ports: - "${COUCH_DB_PORT}:5984" - - "${COUCH_DB_SQS_PORT}:4984" volumes: - couchdb_data:/data From aeda5931c07c84ed826e5e37bd54b3382d955653 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Apr 2024 16:34:33 +0100 Subject: [PATCH 081/112] Fixing lint. --- packages/server/src/sdk/app/rows/search/internal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts index ffd13ed731..906ca016d1 100644 --- a/packages/server/src/sdk/app/rows/search/internal.ts +++ b/packages/server/src/sdk/app/rows/search/internal.ts @@ -1,6 +1,6 @@ import { context, db, HTTPError } from "@budibase/backend-core" import env from "../../../../environment" -import { fullSearch, paginatedSearch, searchInputMapping } from "./utils" +import { fullSearch, paginatedSearch } from "./utils" import { getRowParams, InternalTables } from "../../../../db/utils" import { Database, From fbff5c0a316c0acf732fe97b61ee8f6c8edbdf6e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 12 Apr 2024 16:44:34 +0100 Subject: [PATCH 082/112] Rename toContains to toContainsExactly to better reflect what it does. --- .../src/api/routes/tests/search.spec.ts | 114 ++++++++++++------ 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 4db47be216..a473cb77b4 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -64,7 +64,11 @@ describe.each([ class SearchAssertion { constructor(private readonly query: RowSearchParams) {} - async toMatch(expectedRows: any[]) { + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows matters. Rows returned in an order + // different to the one passed in will cause the assertion to fail. Extra + // rows returned by the query will also cause the assertion to fail. + async toMatchExactly(expectedRows: any[]) { const { rows: foundRows } = await config.api.row.search(table._id!, { ...this.query, tableId: table._id!, @@ -82,7 +86,10 @@ describe.each([ ) } - async toContain(expectedRows: any[]) { + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows is not important, but extra rows will + // cause the assertion to fail. + async toContainExactly(expectedRows: any[]) { const { rows: foundRows } = await config.api.row.search(table._id!, { ...this.query, tableId: table._id!, @@ -102,8 +109,29 @@ describe.each([ ) } + // Asserts that the query returns rows matching the set of rows passed in. + // The order of the rows is not important. Extra rows will not cause the + // assertion to fail. + async toContain(expectedRows: any[]) { + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...this.query, + tableId: table._id!, + }) + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) + ) + ) + ) + ) + } + async toFindNothing() { - await this.toContain([]) + await this.toContainExactly([]) } async toHaveLength(length: number) { @@ -135,18 +163,18 @@ describe.each([ describe("misc", () => { it("should return all if no query is passed", () => - expectSearch({} as RowSearchParams).toContain([ + expectSearch({} as RowSearchParams).toContainExactly([ { name: "foo" }, { name: "bar" }, ])) it("should return all if empty query is passed", () => - expectQuery({}).toContain([{ name: "foo" }, { name: "bar" }])) + expectQuery({}).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return all if onEmptyFilter is RETURN_ALL", () => expectQuery({ onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toContain([{ name: "foo" }, { name: "bar" }])) + }).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return nothing if onEmptyFilter is RETURN_NONE", () => expectQuery({ @@ -159,7 +187,9 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { name: "foo" } }).toContain([{ name: "foo" }])) + expectQuery({ equal: { name: "foo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { name: "none" } }).toFindNothing()) @@ -167,15 +197,21 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { name: "foo" } }).toContain([{ name: "bar" }])) + expectQuery({ notEqual: { name: "foo" } }).toContainExactly([ + { name: "bar" }, + ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { name: "bar" } }).toContain([{ name: "foo" }])) + expectQuery({ notEqual: { name: "bar" } }).toContainExactly([ + { name: "foo" }, + ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { name: ["foo"] } }).toContain([{ name: "foo" }])) + expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()) @@ -183,7 +219,9 @@ describe.each([ describe("fuzzy", () => { it("successfully finds a row", () => - expectQuery({ fuzzy: { name: "oo" } }).toContain([{ name: "foo" }])) + expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ fuzzy: { name: "none" } }).toFindNothing()) @@ -195,14 +233,14 @@ describe.each([ query: {}, sort: "name", sortOrder: SortOrder.ASCENDING, - }).toMatch([{ name: "bar" }, { name: "foo" }])) + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) it("sorts descending", () => expectSearch({ query: {}, sort: "name", sortOrder: SortOrder.DESCENDING, - }).toMatch([{ name: "foo" }, { name: "bar" }])) + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) describe("sortType STRING", () => { it("sorts ascending", () => @@ -211,7 +249,7 @@ describe.each([ sort: "name", sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING, - }).toMatch([{ name: "bar" }, { name: "foo" }])) + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) it("sorts descending", () => expectSearch({ @@ -219,7 +257,7 @@ describe.each([ sort: "name", sortType: SortType.STRING, sortOrder: SortOrder.DESCENDING, - }).toMatch([{ name: "foo" }, { name: "bar" }])) + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) }) }) }) @@ -234,7 +272,7 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { age: 1 } }).toContain([{ age: 1 }])) + expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ equal: { age: 2 } }).toFindNothing()) @@ -242,15 +280,15 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { age: 1 } }).toContain([{ age: 10 }])) + expectQuery({ notEqual: { age: 1 } }).toContainExactly([{ age: 10 }])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { age: 10 } }).toContain([{ age: 1 }])) + expectQuery({ notEqual: { age: 10 } }).toContainExactly([{ age: 1 }])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { age: [1] } }).toContain([{ age: 1 }])) + expectQuery({ oneOf: { age: [1] } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { age: [2] } }).toFindNothing()) @@ -260,17 +298,17 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { age: { low: 1, high: 5 } }, - }).toContain([{ age: 1 }])) + }).toContainExactly([{ age: 1 }])) it("successfully finds multiple rows", () => expectQuery({ range: { age: { low: 1, high: 10 } }, - }).toContain([{ age: 1 }, { age: 10 }])) + }).toContainExactly([{ age: 1 }, { age: 10 }])) it("successfully finds a row with a high bound", () => expectQuery({ range: { age: { low: 5, high: 10 } }, - }).toContain([{ age: 10 }])) + }).toContainExactly([{ age: 10 }])) }) describe("sort", () => { @@ -279,14 +317,14 @@ describe.each([ query: {}, sort: "age", sortOrder: SortOrder.ASCENDING, - }).toMatch([{ age: 1 }, { age: 10 }])) + }).toMatchExactly([{ age: 1 }, { age: 10 }])) it("sorts descending", () => expectSearch({ query: {}, sort: "age", sortOrder: SortOrder.DESCENDING, - }).toMatch([{ age: 10 }, { age: 1 }])) + }).toMatchExactly([{ age: 10 }, { age: 1 }])) }) describe("sortType NUMBER", () => { @@ -296,7 +334,7 @@ describe.each([ sort: "age", sortType: SortType.NUMBER, sortOrder: SortOrder.ASCENDING, - }).toMatch([{ age: 1 }, { age: 10 }])) + }).toMatchExactly([{ age: 1 }, { age: 10 }])) it("sorts descending", () => expectSearch({ @@ -304,7 +342,7 @@ describe.each([ sort: "age", sortType: SortType.NUMBER, sortOrder: SortOrder.DESCENDING, - }).toMatch([{ age: 10 }, { age: 1 }])) + }).toMatchExactly([{ age: 10 }, { age: 1 }])) }) }) @@ -324,7 +362,9 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { dob: JAN_1ST } }).toContain([{ dob: JAN_1ST }])) + expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_1ST }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()) @@ -332,19 +372,19 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { dob: JAN_1ST } }).toContain([ + expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ { dob: JAN_10TH }, ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { dob: JAN_10TH } }).toContain([ + expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([ { dob: JAN_1ST }, ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContain([ + expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([ { dob: JAN_1ST }, ])) @@ -356,17 +396,17 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_5TH } }, - }).toContain([{ dob: JAN_1ST }])) + }).toContainExactly([{ dob: JAN_1ST }])) it("successfully finds multiple rows", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_10TH } }, - }).toContain([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) it("successfully finds a row with a high bound", () => expectQuery({ range: { dob: { low: JAN_5TH, high: JAN_10TH } }, - }).toContain([{ dob: JAN_10TH }])) + }).toContainExactly([{ dob: JAN_10TH }])) }) describe("sort", () => { @@ -375,14 +415,14 @@ describe.each([ query: {}, sort: "dob", sortOrder: SortOrder.ASCENDING, - }).toMatch([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) it("sorts descending", () => expectSearch({ query: {}, sort: "dob", sortOrder: SortOrder.DESCENDING, - }).toMatch([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) describe("sortType STRING", () => { it("sorts ascending", () => @@ -391,7 +431,7 @@ describe.each([ sort: "dob", sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING, - }).toMatch([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) it("sorts descending", () => expectSearch({ @@ -399,7 +439,7 @@ describe.each([ sort: "dob", sortType: SortType.STRING, sortOrder: SortOrder.DESCENDING, - }).toMatch([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) }) }) }) From 6d8dc7c2f6129c082bac9f97afe7b93f65e3cd1e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 12 Apr 2024 17:30:56 +0100 Subject: [PATCH 083/112] Add some more range tests. --- .../src/api/routes/tests/search.spec.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index a473cb77b4..f6945cbe46 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -227,6 +227,28 @@ describe.each([ expectQuery({ fuzzy: { name: "none" } }).toFindNothing()) }) + describe("range", () => { + it("successfully finds multiple rows", () => + expectQuery({ + range: { name: { low: "a", high: "z" } }, + }).toContainExactly([{ name: "bar" }, { name: "foo" }])) + + it("successfully finds a row with a high bound", () => + expectQuery({ + range: { name: { low: "a", high: "c" } }, + }).toContainExactly([{ name: "bar" }])) + + it("successfully finds a row with a low bound", () => + expectQuery({ + range: { name: { low: "f", high: "z" } }, + }).toContainExactly([{ name: "foo" }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { name: { low: "g", high: "h" } }, + }).toFindNothing()) + }) + describe("sort", () => { it("sorts ascending", () => expectSearch({ @@ -309,6 +331,11 @@ describe.each([ expectQuery({ range: { age: { low: 5, high: 10 } }, }).toContainExactly([{ age: 10 }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { age: { low: 5, high: 9 } }, + }).toFindNothing()) }) describe("sort", () => { @@ -350,6 +377,7 @@ describe.each([ const JAN_1ST = "2020-01-01T00:00:00.000Z" const JAN_2ND = "2020-01-02T00:00:00.000Z" const JAN_5TH = "2020-01-05T00:00:00.000Z" + const JAN_9TH = "2020-01-09T00:00:00.000Z" const JAN_10TH = "2020-01-10T00:00:00.000Z" beforeAll(async () => { @@ -407,6 +435,11 @@ describe.each([ expectQuery({ range: { dob: { low: JAN_5TH, high: JAN_10TH } }, }).toContainExactly([{ dob: JAN_10TH }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { dob: { low: JAN_5TH, high: JAN_9TH } }, + }).toFindNothing()) }) describe("sort", () => { From 4e4dfefedef34af178d27cb78bc64cbfa9192a89 Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Mon, 15 Apr 2024 13:41:32 +0100 Subject: [PATCH 084/112] Revert "adds sidepanel open and close actions, and gives the user the option to disable click-outside closure of sidepanel" --- packages/bbui/src/Layout/Page.svelte | 10 +++++----- packages/client/manifest.json | 16 +--------------- packages/client/src/components/app/Layout.svelte | 5 +---- .../client/src/components/app/SidePanel.svelte | 14 -------------- packages/client/src/stores/sidePanel.js | 9 --------- 5 files changed, 7 insertions(+), 47 deletions(-) diff --git a/packages/bbui/src/Layout/Page.svelte b/packages/bbui/src/Layout/Page.svelte index 62dd9cc909..2169a12459 100644 --- a/packages/bbui/src/Layout/Page.svelte +++ b/packages/bbui/src/Layout/Page.svelte @@ -7,11 +7,11 @@ export let narrower = false export let noPadding = false - let sidePanelVisible = false + let sidePanelVisble = false setContext("side-panel", { - open: () => (sidePanelVisible = true), - close: () => (sidePanelVisible = false), + open: () => (sidePanelVisble = true), + close: () => (sidePanelVisble = false), }) @@ -24,9 +24,9 @@
{ - sidePanelVisible = false + sidePanelVisble = false }} > diff --git a/packages/client/manifest.json b/packages/client/manifest.json index c9e28e202b..40abc7a9a0 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6723,21 +6723,7 @@ "illegalChildren": ["section", "sidepanel"], "showEmptyState": false, "draggable": false, - "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.", - "sendEvents": true, - "settings": [ - { - "type": "boolean", - "key": "clickOutsideToClose", - "label": "Click outside to close", - "defaultValue": true - }, - { - "type": "event", - "key": "onSidePanelClose", - "label": "On side panel close" - } - ] + "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action." }, "rowexplorer": { "block": true, diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index bae2bd0faf..8508e943ff 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -73,10 +73,7 @@ $context.device.width, $context.device.height ) - $: autoCloseSidePanel = - !$builderStore.inBuilder && - $sidePanelStore.open && - $sidePanelStore.clickOutsideToClose + $: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open $: screenId = $builderStore.inBuilder ? `${$builderStore.screen?._id}-screen` : "screen" diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index 624617ad69..825b401bb8 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -5,9 +5,6 @@ const { styleable, sidePanelStore, builderStore, dndIsDragging } = getContext("sdk") - export let sidePanelClose - export let clickOutsideToClose - // Automatically show and hide the side panel when inside the builder. // For some unknown reason, svelte reactivity breaks if we reference the // reactive variable "open" inside the following expression, or if we define @@ -29,10 +26,6 @@ } } - $: { - sidePanelStore.actions.setSidepanelState(clickOutsideToClose) - } - // Derive visibility $: open = $sidePanelStore.contentId === $component.id @@ -47,12 +40,6 @@ } } - const handleSidePanelClose = async () => { - if (sidePanelClose) { - await sidePanelClose() - } - } - const showInSidePanel = (el, visible) => { const update = visible => { const target = document.getElementById("side-panel-container") @@ -64,7 +51,6 @@ } else { if (target.contains(node)) { target.removeChild(node) - handleSidePanelClose() } } } diff --git a/packages/client/src/stores/sidePanel.js b/packages/client/src/stores/sidePanel.js index df66eca01c..3b3b9f5f4d 100644 --- a/packages/client/src/stores/sidePanel.js +++ b/packages/client/src/stores/sidePanel.js @@ -3,7 +3,6 @@ import { writable, derived } from "svelte/store" export const createSidePanelStore = () => { const initialState = { contentId: null, - clickOutsideToClose: true, } const store = writable(initialState) const derivedStore = derived(store, $store => { @@ -33,19 +32,11 @@ export const createSidePanelStore = () => { }, 50) } - const setSidepanelState = bool => { - clearTimeout(timeout) - store.update(state => { - state.clickOutsideToClose = bool - return state - }) - } return { subscribe: derivedStore.subscribe, actions: { open, close, - setSidepanelState, }, } } From 68c5e657ddd0fb5b45948e418531d72cd16b178e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 15 Apr 2024 13:46:31 +0100 Subject: [PATCH 085/112] Updating @types/archiver to be more specific. --- packages/server/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index ad03033e67..76402785d7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -125,7 +125,7 @@ "@babel/preset-env": "7.16.11", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", - "@types/archiver": "^6.0.2", + "@types/archiver": "6.0.2", "@types/global-agent": "2.1.1", "@types/google-spreadsheet": "3.1.5", "@types/jest": "29.5.5", diff --git a/yarn.lock b/yarn.lock index a36b54d3be..ce39c89075 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5174,7 +5174,7 @@ dependencies: "@types/node" "*" -"@types/archiver@^6.0.2": +"@types/archiver@6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.2.tgz#0daf8c83359cbde69de1e4b33dcade6a48a929e2" integrity sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw== From 8b9d07fed6896ffabdc16d6dc98798d99dd5e358 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:37:12 +0100 Subject: [PATCH 086/112] Simplify camunda account-portal local dev setup (#13482) --- package.json | 1 + scripts/deploy-camunda.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100755 scripts/deploy-camunda.sh diff --git a/package.json b/package.json index 2816247939..e520b7c2cf 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up --ignore @budibase/account-portal-server && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server", "dev:server": "yarn run kill-server && lerna run --stream dev --scope @budibase/worker --scope @budibase/server", "dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server", + "dev:camunda": "./scripts/deploy-camunda.sh", "dev:all": "yarn run kill-all && lerna run --stream dev", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", diff --git a/scripts/deploy-camunda.sh b/scripts/deploy-camunda.sh new file mode 100755 index 0000000000..d01ed64b5a --- /dev/null +++ b/scripts/deploy-camunda.sh @@ -0,0 +1,31 @@ +#!/bin/bash +yarn global add zbctl +export ZEEBE_ADDRESS='localhost:26500' + +cd ../budibase-bpm + +is_camunda_ready() { + if (zbctl --insecure status 2>/dev/null) | grep -q 'Healthy'; then + return 1 + else + return 0 + fi +} + +docker-compose up -d +echo "waiting for Camunda to be ready..." + +while is_camunda_ready -eq 0; do sleep 1; done + +cd src/main/resources/models + +echo "deploy processes..." +zbctl deploy resource offboarding.bpmn --insecure +zbctl deploy resource onboarding.bpmn --insecure + +cd ../../../../../budibase/packages/account-portal/packages/server + +yarn worker:run & cd ../../../.. && yarn dev:accountportal + + + From 203e32ecc6c39f836f74711f2a3b89f70a4ef167 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 15 Apr 2024 15:09:17 +0100 Subject: [PATCH 087/112] Commenting the field type enumeration to better explain what all of the types do and how they are represented within Budibase. --- packages/types/src/documents/app/row.ts | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 222c346591..ccdf001965 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -1,22 +1,76 @@ import { Document } from "../document" export enum FieldType { + // a primitive type, stores a string, called Text within Budibase. This is one of the default + // types of Budibase, if an external type is not fully understood, we will treat it as text. STRING = "string", + // similar to string type, called Long Form Text within Budibase. This is mainly a frontend + // orientated type which disables a larger text input area. This can also be used + // in conjunction with the 'useRichText' option to support a markdown editor/viewer. LONGFORM = "longform", + // similar to string type, called Options within Budibase. This works very similarly to + // the string type within the backend, but is validated to a list of options. This will + // be displayed a select input within the builder/client. OPTIONS = "options", + // a primitive type, stores a number, as a floating point, called Number within Budibase. + // this type will always represent numbers as reals/floating point - there is no integer only + // type within Budibase. NUMBER = "number", + // a primitive type, stores a boolean, called Boolean within Budibase. This is often represented + // as a toggle or checkbox within forms/grids. BOOLEAN = "boolean", + // a JSON type, this type is always an array of strings, called Multi-select within Budibase. + // This type can be compared to the options type, as it functions similarly, but allows picking + // multiple options rather than a single option. ARRAY = "array", + // a string type, this is always a string when input/returned from the API, called Date/Time within + // Budibase. We utilise ISO date strings for representing dates, this type has a range of sub-types + // to restrict it to date only, time only and ignore timezone capabilities. DATETIME = "datetime", + // a JSON type, an array of metadata about files held in object storage, called Attachment List within + // Budibase. To utilise this type there is an API for uploading files to Budibase, which returns metadata + // that can be stored against columns of this type. Currently this is not supported on external databases. ATTACHMENTS = "attachment", + // a JSON type, similar to the attachments type, called Attachment within Budibase. This type functions + // much the same as the attachment list, but only holds a single attachment metadata as an object. + // This simpifies the binding experience of using this column type. ATTACHMENT_SINGLE = "attachment_single", + // a complex type, called Relationships within Budibase. This is the most complex type of Budibase, + // nothing should be stored against rows under link columns; this type simply represents the + // relationship between tables as part of the table schema. When rows are input to the Budibase API + // relationships to be made are represented as a list of row IDs to link. When rows are returned + // from the Budibase API it will contain a list of row IDs and display column values of the related rows. LINK = "link", + // a complex type, called Formulas within Budibase. This type has two variants, static and dynamic, with + // static only being supported against internal tables. Dynamic formulas calculate a provided HBS/JS binding + // based on the row context and enrich it when rows are being returned from the API. Static bindings calculate + // this when rows are being stored, so that the formula output can be searched upon within the DB. FORMULA = "formula", + // a complex type, called Auto Column within Budibase. This type has a few variants, with options such as a + // date for created at/updated at, an auto ID column with auto-increments as rows are saved and a user + // relationship type which stores the created by/updated by user details. This sub-types all depend on the + // date, number of link types respectively. AUTO = "auto", + // a JSON type, called JSON within Budibase. This type allows any arbitrary JSON to be input to this column + // type, which will be represented a string in the row. This type depends on a schema being provided to make the + // JSON searchable/bindable, the JSON cannot be fully dynamic. JSON = "json", + // an internal type, this is an old deprecated type which is no longer used - still represented to note it + // could appear in very old tables. INTERNAL = "internal", + // a string type, called Barcode/QR within Budibase. This type is used to denote to forms to that this column + // should be filled in using a camera to read a barcode, there is a form component which will be used when this + // type is found. The column will contain the contents of any barcode scanned. BARCODEQR = "barcodeqr", + // a string type, this allows representing very large integers, but they are held/managed within Budibase as + // strings. When stored in external databases Budibase will attempt to use a real big integer type and depend + // on the database parsing the string to this type as part of saving. BIGINT = "bigint", + // a JSON type, called User within Budibase. This type is used to represent a link to an internal Budibase + // resource, like a user or group, today only users are supported. This type will be represented as an + // array of internal resource IDs (e.g. user IDs) within the row - this ID list will be enriched with + // the full resources when rows are returned from the API. The full resources can be input to the API, or + // an array of resource IDs, the API will squash these down and validate them before saving the row. BB_REFERENCE = "bb_reference", } From d61d5f51cc5c791041a23e0e41469e5603f59575 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Apr 2024 15:31:46 +0100 Subject: [PATCH 088/112] Add tests for array column types, fixing some bugs along the way. --- .../src/api/routes/tests/search.spec.ts | 74 +++++++++++++++++++ packages/server/src/constants/index.ts | 2 + packages/server/src/integrations/base/sql.ts | 5 ++ packages/server/src/sdk/app/rows/search.ts | 6 +- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index f6945cbe46..5b71ec9044 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -476,4 +476,78 @@ describe.each([ }) }) }) + + describe("array of strings", () => { + beforeAll(async () => { + await createTable({ + numbers: { + name: "numbers", + type: FieldType.ARRAY, + constraints: { inclusion: ["one", "two", "three"] }, + }, + }) + await createRows([{ numbers: ["one", "two"] }, { numbers: ["three"] }]) + }) + + describe("contains", () => { + it("successfully finds a row", () => + expectQuery({ contains: { numbers: ["one"] } }).toContainExactly([ + { numbers: ["one", "two"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ contains: { numbers: ["none"] } }).toFindNothing()) + + it("fails to find row containing all", () => + expectQuery({ + contains: { numbers: ["one", "two", "three"] }, + }).toFindNothing()) + + it("finds all with empty list", () => + expectQuery({ contains: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + }) + + describe("notContains", () => { + it("successfully finds a row", () => + expectQuery({ notContains: { numbers: ["one"] } }).toContainExactly([ + { numbers: ["three"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ + notContains: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + + it("finds all with empty list", () => + expectQuery({ notContains: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + }) + + describe("containsAny", () => { + it("successfully finds rows", () => + expectQuery({ + containsAny: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ containsAny: { numbers: ["none"] } }).toFindNothing()) + + it("finds all with empty list", () => + expectQuery({ containsAny: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + }) + }) }) diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 42a1b53224..37c275c8a3 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -20,6 +20,7 @@ export enum FilterTypes { NOT_EMPTY = "notEmpty", CONTAINS = "contains", NOT_CONTAINS = "notContains", + CONTAINS_ANY = "containsAny", ONE_OF = "oneOf", } @@ -30,6 +31,7 @@ export const NoEmptyFilterStrings = [ FilterTypes.NOT_EQUAL, FilterTypes.CONTAINS, FilterTypes.NOT_CONTAINS, + FilterTypes.CONTAINS_ANY, ] export const CanSwitchTypes = [ diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index f5828f9419..259abec106 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -233,6 +233,11 @@ class InternalBuilder { (statement ? andOr : "") + `LOWER(${likeKey(this.client, key)}) LIKE ?` } + + if (statement === "") { + return + } + // @ts-ignore query = query[rawFnc](`${not}(${statement})`, value) }) diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index f681bfeb90..5a016c821f 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -29,6 +29,10 @@ function pickApi(tableId: any) { return internal } +function isEmptyArray(value: any) { + return Array.isArray(value) && value.length === 0 +} + // don't do a pure falsy check, as 0 is included // https://github.com/Budibase/budibase/issues/10118 export function removeEmptyFilters(filters: SearchFilters) { @@ -47,7 +51,7 @@ export function removeEmptyFilters(filters: SearchFilters) { for (let [key, value] of Object.entries( filters[filterType] as object )) { - if (value == null || value === "") { + if (value == null || value === "" || isEmptyArray(value)) { // @ts-ignore delete filters[filterField][key] } From 81425b3d287340054d4796e7e6763ce72e7e7d23 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 15 Apr 2024 15:50:25 +0100 Subject: [PATCH 089/112] Addressing PR comment.s --- packages/types/src/documents/app/row.ts | 142 +++++++++++++++--------- 1 file changed, 88 insertions(+), 54 deletions(-) diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index ccdf001965..4f2f9f99ef 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -1,76 +1,110 @@ import { Document } from "../document" export enum FieldType { - // a primitive type, stores a string, called Text within Budibase. This is one of the default - // types of Budibase, if an external type is not fully understood, we will treat it as text. + /** + * a primitive type, stores a string, called Text within Budibase. This is one of the default + * types of Budibase, if an external type is not fully understood, we will treat it as text. + */ STRING = "string", - // similar to string type, called Long Form Text within Budibase. This is mainly a frontend - // orientated type which disables a larger text input area. This can also be used - // in conjunction with the 'useRichText' option to support a markdown editor/viewer. + /** + * similar to string type, called Long Form Text within Budibase. This is mainly a frontend + * orientated type which enables a larger text input area. This can also be used + * in conjunction with the 'useRichText' option to support a markdown editor/viewer. + */ LONGFORM = "longform", - // similar to string type, called Options within Budibase. This works very similarly to - // the string type within the backend, but is validated to a list of options. This will - // be displayed a select input within the builder/client. + /** + * similar to string type, called Options within Budibase. This works very similarly to + * the string type within the backend, but is validated to a list of options. This will + * display a - {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)} + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)} { +const cleanupQuery = (query: SearchFilters) => { if (!query) { return query } for (let filterField of NoEmptyFilterStrings) { - if (!query[filterField]) { + const operator = filterField as SearchFilterOperator + if (!query[operator]) { continue } - for (let [key, value] of Object.entries(query[filterField]!)) { + for (let [key, value] of Object.entries(query[operator]!)) { if (value == null || value === "") { - delete query[filterField]![key] + delete query[operator]![key] } } } @@ -136,7 +137,7 @@ export const removeKeyNumbering = (key: string): string => { * @param filter the builder filter structure */ export const buildLuceneQuery = (filter: SearchFilter[]) => { - let query: SearchQuery = { + let query: SearchFilters = { string: {}, fuzzy: {}, range: {}, @@ -157,6 +158,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { filter.forEach(expression => { let { operator, field, type, value, externalType, onEmptyFilter } = expression + const queryOperator = operator as SearchFilterOperator const isHbs = typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0 // Parse all values into correct types @@ -171,8 +173,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { if ( type === "datetime" && !isHbs && - operator !== "empty" && - operator !== "notEmpty" + queryOperator !== "empty" && + queryOperator !== "notEmpty" ) { // Ensure date value is a valid date and parse into correct format if (!value) { @@ -185,7 +187,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { } } if (type === "number" && typeof value === "string" && !isHbs) { - if (operator === "oneOf") { + if (queryOperator === "oneOf") { value = value.split(",").map(item => parseFloat(item)) } else { value = parseFloat(value) @@ -225,24 +227,24 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { ) { query.range[field].high = value } - } else if (query[operator] && operator !== "onEmptyFilter") { + } else if (query[queryOperator] && operator !== "onEmptyFilter") { if (type === "boolean") { // Transform boolean filters to cope with null. // "equals false" needs to be "not equals true" // "not equals false" needs to be "equals true" - if (operator === "equal" && value === false) { + if (queryOperator === "equal" && value === false) { query.notEqual = query.notEqual || {} query.notEqual[field] = true - } else if (operator === "notEqual" && value === false) { + } else if (queryOperator === "notEqual" && value === false) { query.equal = query.equal || {} query.equal[field] = true } else { - query[operator] = query[operator] || {} - query[operator]![field] = value + query[queryOperator] = query[queryOperator] || {} + query[queryOperator]![field] = value } } else { - query[operator] = query[operator] || {} - query[operator]![field] = value + query[queryOperator] = query[queryOperator] || {} + query[queryOperator]![field] = value } } }) @@ -255,7 +257,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { * @param docs the data * @param query the JSON lucene query */ -export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { +export const runLuceneQuery = (docs: any[], query?: SearchFilters) => { if (!docs || !Array.isArray(docs)) { return [] } @@ -269,7 +271,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Iterates over a set of filters and evaluates a fail function against a doc const match = ( - type: keyof SearchQueryFields, + type: SearchFilterOperator, failFn: (docValue: any, testValue: any) => boolean ) => (doc: any) => { @@ -286,7 +288,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a string match (fails if the value does not start with the string) const stringMatch = match( - SearchQueryOperators.STRING, + SearchFilterOperator.STRING, (docValue: string, testValue: string) => { return ( !docValue || @@ -297,7 +299,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a fuzzy match (treat the same as starts with when running locally) const fuzzyMatch = match( - SearchQueryOperators.FUZZY, + SearchFilterOperator.FUZZY, (docValue: string, testValue: string) => { return ( !docValue || @@ -308,7 +310,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a range match const rangeMatch = match( - SearchQueryOperators.RANGE, + SearchFilterOperator.RANGE, ( docValue: string | number | null, testValue: { low: number; high: number } @@ -331,7 +333,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an equal match (fails if the value is different) const equalMatch = match( - SearchQueryOperators.EQUAL, + SearchFilterOperator.EQUAL, (docValue: any, testValue: string | null) => { return testValue != null && testValue !== "" && docValue !== testValue } @@ -339,7 +341,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a not-equal match (fails if the value is the same) const notEqualMatch = match( - SearchQueryOperators.NOT_EQUAL, + SearchFilterOperator.NOT_EQUAL, (docValue: any, testValue: string | null) => { return testValue != null && testValue !== "" && docValue === testValue } @@ -347,7 +349,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an empty match (fails if the value is not empty) const emptyMatch = match( - SearchQueryOperators.EMPTY, + SearchFilterOperator.EMPTY, (docValue: string | null) => { return docValue != null && docValue !== "" } @@ -355,7 +357,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a not-empty match (fails is the value is empty) const notEmptyMatch = match( - SearchQueryOperators.NOT_EMPTY, + SearchFilterOperator.NOT_EMPTY, (docValue: string | null) => { return docValue == null || docValue === "" } @@ -363,7 +365,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process an includes match (fails if the value is not included) const oneOf = match( - SearchQueryOperators.ONE_OF, + SearchFilterOperator.ONE_OF, (docValue: any, testValue: any) => { if (typeof testValue === "string") { testValue = testValue.split(",") @@ -376,28 +378,28 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { ) const containsAny = match( - SearchQueryOperators.CONTAINS_ANY, + SearchFilterOperator.CONTAINS_ANY, (docValue: any, testValue: any) => { return !docValue?.includes(...testValue) } ) const contains = match( - SearchQueryOperators.CONTAINS, + SearchFilterOperator.CONTAINS, (docValue: string | any[], testValue: any[]) => { return !testValue?.every((item: any) => docValue?.includes(item)) } ) const notContains = match( - SearchQueryOperators.NOT_CONTAINS, + SearchFilterOperator.NOT_CONTAINS, (docValue: string | any[], testValue: any[]) => { return testValue?.every((item: any) => docValue?.includes(item)) } ) const docMatch = (doc: any) => { - const filterFunctions: Record boolean> = + const filterFunctions: Record boolean> = { string: stringMatch, fuzzy: fuzzyMatch, @@ -412,7 +414,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { notContains: notContains, } - const activeFilterKeys: SearchQueryOperators[] = Object.entries(query || {}) + const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {}) .filter( ([key, value]: [string, any]) => !["allOr", "onEmptyFilter"].includes(key) && @@ -480,7 +482,7 @@ export const luceneLimit = (docs: any[], limit: string) => { return docs.slice(0, numLimit) } -export const hasFilters = (query?: SearchQuery) => { +export const hasFilters = (query?: SearchFilters) => { if (!query) { return false } diff --git a/packages/shared-core/src/tests/filters.test.ts b/packages/shared-core/src/tests/filters.test.ts index e74e37d681..f188c5f951 100644 --- a/packages/shared-core/src/tests/filters.test.ts +++ b/packages/shared-core/src/tests/filters.test.ts @@ -1,6 +1,6 @@ import { - SearchQuery, - SearchQueryOperators, + SearchFilters, + SearchFilterOperator, FieldType, SearchFilter, } from "@budibase/types" @@ -46,8 +46,8 @@ describe("runLuceneQuery", () => { }, ] - function buildQuery(filters: { [filterKey: string]: any }): SearchQuery { - const query: SearchQuery = { + function buildQuery(filters: { [filterKey: string]: any }): SearchFilters { + const query: SearchFilters = { string: {}, fuzzy: {}, range: {}, @@ -63,7 +63,7 @@ describe("runLuceneQuery", () => { } for (const filterKey in filters) { - query[filterKey as SearchQueryOperators] = filters[filterKey] + query[filterKey as SearchFilterOperator] = filters[filterKey] } return query @@ -265,13 +265,13 @@ describe("buildLuceneQuery", () => { it("should parseFloat if the type is a number, but the value is a numeric string", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "customer_id", type: FieldType.NUMBER, value: "1212", }, { - operator: SearchQueryOperators.ONE_OF, + operator: SearchFilterOperator.ONE_OF, field: "customer_id", type: FieldType.NUMBER, value: "1000,1212,3400", @@ -299,13 +299,13 @@ describe("buildLuceneQuery", () => { it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "customer_id", type: FieldType.NUMBER, value: "{{ customer_id }}", }, { - operator: SearchQueryOperators.ONE_OF, + operator: SearchFilterOperator.ONE_OF, field: "customer_id", type: FieldType.NUMBER, value: "{{ list_of_customer_ids }}", @@ -333,19 +333,19 @@ describe("buildLuceneQuery", () => { it("should cast string to boolean if the type is boolean", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "a", type: FieldType.BOOLEAN, value: "not_true", }, { - operator: SearchQueryOperators.NOT_EQUAL, + operator: SearchFilterOperator.NOT_EQUAL, field: "b", type: FieldType.BOOLEAN, value: "not_true", }, { - operator: SearchQueryOperators.EQUAL, + operator: SearchFilterOperator.EQUAL, field: "c", type: FieldType.BOOLEAN, value: "true", @@ -374,19 +374,19 @@ describe("buildLuceneQuery", () => { it("should split the string for contains operators", () => { const filter: SearchFilter[] = [ { - operator: SearchQueryOperators.CONTAINS, + operator: SearchFilterOperator.CONTAINS, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", }, { - operator: SearchQueryOperators.NOT_CONTAINS, + operator: SearchFilterOperator.NOT_CONTAINS, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", }, { - operator: SearchQueryOperators.CONTAINS_ANY, + operator: SearchFilterOperator.CONTAINS_ANY, field: "description", type: FieldType.ARRAY, value: "Large box,Heavy box,Small box", diff --git a/packages/types/src/api/web/searchFilter.ts b/packages/types/src/api/web/searchFilter.ts index ac3c446e36..5223204a7f 100644 --- a/packages/types/src/api/web/searchFilter.ts +++ b/packages/types/src/api/web/searchFilter.ts @@ -1,68 +1,11 @@ import { FieldType } from "../../documents" -import { EmptyFilterOption } from "../../sdk" +import { EmptyFilterOption, SearchFilters } from "../../sdk" export type SearchFilter = { - operator: keyof SearchQuery + operator: keyof SearchFilters | "rangeLow" | "rangeHigh" onEmptyFilter?: EmptyFilterOption field: string type?: FieldType value: any externalType?: string } - -export enum SearchQueryOperators { - STRING = "string", - FUZZY = "fuzzy", - RANGE = "range", - EQUAL = "equal", - NOT_EQUAL = "notEqual", - EMPTY = "empty", - NOT_EMPTY = "notEmpty", - ONE_OF = "oneOf", - CONTAINS = "contains", - NOT_CONTAINS = "notContains", - CONTAINS_ANY = "containsAny", -} - -export type SearchQuery = { - allOr?: boolean - onEmptyFilter?: EmptyFilterOption - [SearchQueryOperators.STRING]?: { - [key: string]: string - } - [SearchQueryOperators.FUZZY]?: { - [key: string]: string - } - [SearchQueryOperators.RANGE]?: { - [key: string]: { - high: number | string - low: number | string - } - } - [SearchQueryOperators.EQUAL]?: { - [key: string]: any - } - [SearchQueryOperators.NOT_EQUAL]?: { - [key: string]: any - } - [SearchQueryOperators.EMPTY]?: { - [key: string]: any - } - [SearchQueryOperators.NOT_EMPTY]?: { - [key: string]: any - } - [SearchQueryOperators.ONE_OF]?: { - [key: string]: any[] - } - [SearchQueryOperators.CONTAINS]?: { - [key: string]: any[] - } - [SearchQueryOperators.NOT_CONTAINS]?: { - [key: string]: any[] - } - [SearchQueryOperators.CONTAINS_ANY]?: { - [key: string]: any[] - } -} - -export type SearchQueryFields = Omit diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 0ef7493016..10630c272c 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -1,5 +1,5 @@ import { User } from "../../documents" -import { SearchQuery } from "./searchFilter" +import { SearchFilters } from "../../sdk" export interface SaveUserResponse { _id: string @@ -55,7 +55,7 @@ export interface InviteUsersResponse { export interface SearchUsersRequest { bookmark?: string - query?: SearchQuery + query?: SearchFilters appId?: string limit?: number paginate?: boolean diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts index 0b93fb9215..51d866c9de 100644 --- a/packages/types/src/sdk/search.ts +++ b/packages/types/src/sdk/search.ts @@ -3,47 +3,63 @@ import { Row, Table } from "../documents" import { SortType } from "../api" import { Knex } from "knex" +export enum SearchFilterOperator { + STRING = "string", + FUZZY = "fuzzy", + RANGE = "range", + EQUAL = "equal", + NOT_EQUAL = "notEqual", + EMPTY = "empty", + NOT_EMPTY = "notEmpty", + ONE_OF = "oneOf", + CONTAINS = "contains", + NOT_CONTAINS = "notContains", + CONTAINS_ANY = "containsAny", +} + export interface SearchFilters { allOr?: boolean onEmptyFilter?: EmptyFilterOption - string?: { + [SearchFilterOperator.STRING]?: { [key: string]: string } - fuzzy?: { + [SearchFilterOperator.FUZZY]?: { [key: string]: string } - range?: { + [SearchFilterOperator.RANGE]?: { [key: string]: { high: number | string low: number | string } } - equal?: { + [SearchFilterOperator.EQUAL]?: { [key: string]: any } - notEqual?: { + [SearchFilterOperator.NOT_EQUAL]?: { [key: string]: any } - empty?: { + [SearchFilterOperator.EMPTY]?: { [key: string]: any } - notEmpty?: { + [SearchFilterOperator.NOT_EMPTY]?: { [key: string]: any } - oneOf?: { + [SearchFilterOperator.ONE_OF]?: { [key: string]: any[] } - contains?: { - [key: string]: any[] | any - } - notContains?: { + [SearchFilterOperator.CONTAINS]?: { [key: string]: any[] } - containsAny?: { + [SearchFilterOperator.NOT_CONTAINS]?: { + [key: string]: any[] + } + [SearchFilterOperator.CONTAINS_ANY]?: { [key: string]: any[] } } +export type SearchQueryFields = Omit + export interface SortJson { [key: string]: { direction: SortDirection diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index d08a4ef8c7..541004391d 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -4,7 +4,7 @@ import { InviteUsersRequest, User, CreateAdminUserRequest, - SearchQuery, + SearchFilters, InviteUsersResponse, } from "@budibase/types" import structures from "../structures" @@ -150,7 +150,7 @@ export class UserAPI extends TestAPI { } searchUsers = ( - { query }: { query?: SearchQuery }, + { query }: { query?: SearchFilters }, opts?: { status?: number; noHeaders?: boolean } ) => { const req = this.request From c51df0eceb32f5448533a5212984e2e98af9f102 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 17 Apr 2024 14:05:47 +0100 Subject: [PATCH 109/112] Fixing test case. --- .../sdk/app/rows/search/tests/{lucene.ts => lucene.spec.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/server/src/sdk/app/rows/search/tests/{lucene.ts => lucene.spec.ts} (98%) diff --git a/packages/server/src/sdk/app/rows/search/tests/lucene.ts b/packages/server/src/sdk/app/rows/search/tests/lucene.spec.ts similarity index 98% rename from packages/server/src/sdk/app/rows/search/tests/lucene.ts rename to packages/server/src/sdk/app/rows/search/tests/lucene.spec.ts index 708f362198..d9c1c79177 100644 --- a/packages/server/src/sdk/app/rows/search/tests/lucene.ts +++ b/packages/server/src/sdk/app/rows/search/tests/lucene.spec.ts @@ -160,7 +160,7 @@ describe("internal search", () => { const response = await search.paginatedSearch( { contains: { - column: "a", + column: ["a"], colArr: [1, 2, 3], }, }, @@ -168,7 +168,7 @@ describe("internal search", () => { ) checkLucene( response, - `(*:* AND column:a AND colArr:(1 AND 2 AND 3))`, + `(*:* AND column:(a) AND colArr:(1 AND 2 AND 3))`, PARAMS ) }) From 160c3913a2cd35d6ff200c2634f0f18d6c4c43e5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Apr 2024 15:48:44 +0200 Subject: [PATCH 110/112] Update submodule refs --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index e6c7cde321..c78bab4ca1 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e6c7cde321b6943e3f0f464957b1c1c5b22fbb50 +Subproject commit c78bab4ca13176263510cbdbc7d115d9d9d22ad9 From fcb04c6be128dffb6450a62a891fee78a1f55b2d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Apr 2024 15:50:29 +0200 Subject: [PATCH 111/112] Update submodule refs --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index e2357a67de..266807f80b 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit e2357a67de047c430908901889bbcb83767970ef +Subproject commit 266807f80b63cddfdcdcff2c83ff0732a68bf3bb From c763d3758e083aeaf93e4b21b4946b3959a7b818 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Apr 2024 15:59:21 +0200 Subject: [PATCH 112/112] Update submodule refs --- packages/account-portal | 2 +- packages/pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/account-portal b/packages/account-portal index 266807f80b..eb7d5da233 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 266807f80b63cddfdcdcff2c83ff0732a68bf3bb +Subproject commit eb7d5da233885c5cffd9c255d3e954d0cd39185e diff --git a/packages/pro b/packages/pro index c78bab4ca1..06b1064f7e 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit c78bab4ca13176263510cbdbc7d115d9d9d22ad9 +Subproject commit 06b1064f7e2f7cac5d4bef2ee999796a2a1f0f2c