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/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 c7f6d82437..9084a25753 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6772,21 +6772,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, }, } } 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]) { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index a473cb77b4..5b71ec9044 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", () => { @@ -443,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] } diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index b0c6654150..3034099f7b 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -1,23 +1,116 @@ 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 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 + * display a