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