From 80a772f39fb8d7cc23bbd03cee40947cd2e7c3c6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 5 Apr 2024 13:15:06 +0100 Subject: [PATCH 1/8] Add snippets to app imports --- packages/server/src/api/controllers/application.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index ceef421fab..6acdfcd465 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -320,6 +320,7 @@ async function performAppCreate(ctx: UserCtx) { "theme", "customTheme", "icon", + "snippets", ] keys.forEach(key => { if (existing[key]) { From 6d8dc7c2f6129c082bac9f97afe7b93f65e3cd1e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 12 Apr 2024 17:30:56 +0100 Subject: [PATCH 2/8] 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 3/8] 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 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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