diff --git a/packages/bbui/src/Layout/Page.svelte b/packages/bbui/src/Layout/Page.svelte index 2169a12459..62dd9cc909 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 sidePanelVisble = false + let sidePanelVisible = false setContext("side-panel", { - open: () => (sidePanelVisble = true), - close: () => (sidePanelVisble = false), + open: () => (sidePanelVisible = true), + close: () => (sidePanelVisible = false), }) @@ -24,9 +24,9 @@
{ - sidePanelVisble = false + sidePanelVisible = false }} > diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 40abc7a9a0..c9e28e202b 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6723,7 +6723,21 @@ "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 outside to close", + "defaultValue": true + }, + { + "type": "event", + "key": "onSidePanelClose", + "label": "On side panel close" + } + ] }, "rowexplorer": { "block": true, diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index 8508e943ff..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" diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index 825b401bb8..624617ad69 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -5,6 +5,9 @@ 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 @@ -26,6 +29,10 @@ } } + $: { + sidePanelStore.actions.setSidepanelState(clickOutsideToClose) + } + // Derive visibility $: open = $sidePanelStore.contentId === $component.id @@ -40,6 +47,12 @@ } } + const handleSidePanelClose = async () => { + if (sidePanelClose) { + await sidePanelClose() + } + } + const showInSidePanel = (el, visible) => { const update = visible => { const target = document.getElementById("side-panel-container") @@ -51,6 +64,7 @@ } 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, }, } } diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index fdf1ed7603..a473cb77b4 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -8,6 +8,8 @@ import { FieldType, RowSearchParams, SearchFilters, + SortOrder, + SortType, Table, TableSchema, } from "@budibase/types" @@ -62,7 +64,32 @@ describe.each([ class SearchAssertion { constructor(private readonly query: RowSearchParams) {} - async toFind(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!, + }) + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toEqual( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) + ) + ) + ) + } + + // 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!, @@ -82,8 +109,39 @@ 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.toFind([]) + await this.toContainExactly([]) + } + + async toHaveLength(length: number) { + 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).toHaveLength(length) } } @@ -105,28 +163,33 @@ describe.each([ describe("misc", () => { it("should return all if no query is passed", () => - expectSearch({} as RowSearchParams).toFind([ + expectSearch({} as RowSearchParams).toContainExactly([ { name: "foo" }, { name: "bar" }, ])) it("should return all if empty query is passed", () => - expectQuery({}).toFind([{ name: "foo" }, { name: "bar" }])) + expectQuery({}).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return all if onEmptyFilter is RETURN_ALL", () => expectQuery({ onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toFind([{ name: "foo" }, { name: "bar" }])) + }).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return nothing if onEmptyFilter is RETURN_NONE", () => expectQuery({ onEmptyFilter: EmptyFilterOption.RETURN_NONE, }).toFindNothing()) + + it("should respect limit", () => + expectSearch({ limit: 1, paginate: true, query: {} }).toHaveLength(1)) }) describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { name: "foo" } }).toFind([{ name: "foo" }])) + expectQuery({ equal: { name: "foo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { name: "none" } }).toFindNothing()) @@ -134,15 +197,21 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { name: "foo" } }).toFind([{ name: "bar" }])) + expectQuery({ notEqual: { name: "foo" } }).toContainExactly([ + { name: "bar" }, + ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { name: "bar" } }).toFind([{ name: "foo" }])) + expectQuery({ notEqual: { name: "bar" } }).toContainExactly([ + { name: "foo" }, + ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { name: ["foo"] } }).toFind([{ name: "foo" }])) + expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()) @@ -150,11 +219,47 @@ describe.each([ describe("fuzzy", () => { it("successfully finds a row", () => - expectQuery({ fuzzy: { name: "oo" } }).toFind([{ name: "foo" }])) + expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ fuzzy: { name: "none" } }).toFindNothing()) }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + + describe("sortType STRING", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + }) + }) }) describe("numbers", () => { @@ -167,7 +272,7 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { age: 1 } }).toFind([{ age: 1 }])) + expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ equal: { age: 2 } }).toFindNothing()) @@ -175,15 +280,15 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { age: 1 } }).toFind([{ age: 10 }])) + expectQuery({ notEqual: { age: 1 } }).toContainExactly([{ age: 10 }])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { age: 10 } }).toFind([{ age: 1 }])) + expectQuery({ notEqual: { age: 10 } }).toContainExactly([{ age: 1 }])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { age: [1] } }).toFind([{ age: 1 }])) + expectQuery({ oneOf: { age: [1] } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { age: [2] } }).toFindNothing()) @@ -193,17 +298,51 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { age: { low: 1, high: 5 } }, - }).toFind([{ age: 1 }])) + }).toContainExactly([{ age: 1 }])) it("successfully finds multiple rows", () => expectQuery({ range: { age: { low: 1, high: 10 } }, - }).toFind([{ 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 } }, - }).toFind([{ age: 10 }])) + }).toContainExactly([{ age: 10 }])) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }])) + }) + + describe("sortType NUMBER", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }])) }) }) @@ -223,7 +362,9 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { dob: JAN_1ST } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_1ST }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()) @@ -231,15 +372,21 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { dob: JAN_1ST } }).toFind([{ dob: JAN_10TH }])) + expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_10TH }, + ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { dob: JAN_10TH } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([ + { dob: JAN_1ST }, + ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { dob: [JAN_1ST] } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([ + { dob: JAN_1ST }, + ])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing()) @@ -249,17 +396,51 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_5TH } }, - }).toFind([{ dob: JAN_1ST }])) + }).toContainExactly([{ dob: JAN_1ST }])) it("successfully finds multiple rows", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_10TH } }, - }).toFind([{ 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 } }, - }).toFind([{ dob: JAN_10TH }])) + }).toContainExactly([{ dob: JAN_10TH }])) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + + describe("sortType STRING", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + }) }) }) }) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 5b0b6e3bc7..7abd7d9e72 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -132,7 +132,7 @@ export async function search( type: "row", } - if (params.sort && !params.sortType) { + if (params.sort) { const sortField = table.schema[params.sort] const sortType = sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING