diff --git a/lerna.json b/lerna.json index d7175264a2..22cf4a1e83 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 20a1b55607..ad02630ccb 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/types": "^1.1.18-alpha.4", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 7beb5f2663..9a22fa3cea 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/string-templates": "^1.1.18-alpha.4", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/builder/package.json b/packages/builder/package.json index 570e8b1be8..d1a8e29ff3 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,10 +69,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", - "@budibase/client": "^1.1.18-alpha.2", - "@budibase/frontend-core": "^1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.4", + "@budibase/client": "^1.1.18-alpha.4", + "@budibase/frontend-core": "^1.1.18-alpha.4", + "@budibase/string-templates": "^1.1.18-alpha.4", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 8b34cf8cd2..f77374985d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -246,6 +246,7 @@ {bindings} allowJS={false} updateOnChange={false} + drawerLeft="260px" /> {/if} {:else if value.customType === "query"} @@ -335,6 +336,7 @@ {bindings} updateOnChange={false} placeholder={value.customType === "queryLimit" ? queryLimit : ""} + drawerLeft="260px" /> {/if} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte index b162408b26..b8d418c62b 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte @@ -18,6 +18,7 @@ export let fillWidth export let allowJS = true export let updateOnChange = true + export let drawerLeft const dispatch = createEventDispatcher() let bindingDrawer @@ -53,7 +54,7 @@ {/if} - + Add the objects on the left to enrich your text. diff --git a/packages/builder/src/components/integration/KeyValueBuilder.svelte b/packages/builder/src/components/integration/KeyValueBuilder.svelte index 9b46bc0364..4ffb380aa4 100644 --- a/packages/builder/src/components/integration/KeyValueBuilder.svelte +++ b/packages/builder/src/components/integration/KeyValueBuilder.svelte @@ -32,6 +32,7 @@ export let menuItems export let showMenu = false export let bindings = [] + export let bindingDrawerLeft let fields = Object.entries(object || {}).map(([name, value]) => ({ name, @@ -119,6 +120,7 @@ value={field.value} allowJS={false} fillWidth={true} + drawerLeft={bindingDrawerLeft} /> {:else} @@ -448,6 +449,7 @@ name="param" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> @@ -458,6 +460,7 @@ name="header" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> diff --git a/packages/cli/package.json b/packages/cli/package.json index 887eaf4c76..bf28b34f0f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,7 +26,7 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.4", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/manifest.json b/packages/client/manifest.json index fdb5700a5c..8be92e19f6 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3509,17 +3509,18 @@ }, { "type": "boolean", - "label": "Show button", + "label": "Use button for click action", "key": "showButton" }, { "type": "text", "key": "buttonText", - "label": "Button text" + "label": "Button text", + "dependsOn": "showButton" }, { "type": "event", - "label": "Button action", + "label": "Click action", "key": "buttonOnClick" } ] @@ -3841,18 +3842,19 @@ }, { "type": "boolean", - "label": "Show button", + "label": "Use button for click action", "key": "showCardButton" }, { "type": "text", "key": "cardButtonText", "label": "Button text", - "nested": true + "nested": true, + "dependsOn": "showCardButton" }, { "type": "event", - "label": "Button action", + "label": "Click action", "key": "cardButtonOnClick", "nested": true } diff --git a/packages/client/package.json b/packages/client/package.json index 19a581c6d0..e1efa3995b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", - "@budibase/frontend-core": "^1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.4", + "@budibase/frontend-core": "^1.1.18-alpha.4", + "@budibase/string-templates": "^1.1.18-alpha.4", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/components/app/SpectrumCard.svelte b/packages/client/src/components/app/SpectrumCard.svelte index 4b4f9d62f2..3b2fe44f4e 100644 --- a/packages/client/src/components/app/SpectrumCard.svelte +++ b/packages/client/src/components/app/SpectrumCard.svelte @@ -19,9 +19,10 @@ const handleLink = e => { if (!linkURL) { - return + return false } e.preventDefault() + e.stopPropagation() routeStore.actions.navigate(linkURL, linkPeek) } @@ -32,6 +33,8 @@ tabindex="0" role="figure" class:horizontal + class:clickable={buttonOnClick && !showButton} + on:click={showButton ? null : buttonOnClick} > {#if imageURL} {/if} @@ -81,6 +86,11 @@ flex-direction: column; justify-content: flex-start; align-items: stretch; + transition: border-color 130ms ease-out; + } + .spectrum-Card.clickable:hover { + cursor: pointer; + border-color: var(--spectrum-global-color-gray-500) !important; } .spectrum-Card.horizontal { flex-direction: row; @@ -90,7 +100,7 @@ padding: var(--spectrum-global-dimension-size-50) 0; } .spectrum-Card-title.link { - transition: color 130ms ease-in-out; + transition: color 130ms ease-out; } .spectrum-Card-title.link:hover { cursor: pointer; diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index ceacf027c1..6b77b38894 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "^1.1.18-alpha.2", + "@budibase/bbui": "^1.1.18-alpha.4", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 00af4127e4..4ac2a02f6c 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -35,7 +35,7 @@ export const OperatorOptions = { label: "Less than", }, Contains: { - value: "equal", + value: "contains", label: "Contains", }, NotContains: { diff --git a/packages/server/package.json b/packages/server/package.json index 22d1ef43a0..db6ea93c6f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -77,11 +77,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "^1.1.18-alpha.2", - "@budibase/client": "^1.1.18-alpha.2", - "@budibase/pro": "1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.4", + "@budibase/client": "^1.1.18-alpha.4", + "@budibase/pro": "1.1.18-alpha.4", + "@budibase/string-templates": "^1.1.18-alpha.4", + "@budibase/types": "^1.1.18-alpha.4", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index e6090ad8f0..8a04fc2bd0 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -19,6 +19,7 @@ class QueryBuilder { empty: {}, notEmpty: {}, oneOf: {}, + contains: {}, ...base, } this.limit = 50 @@ -119,6 +120,11 @@ class QueryBuilder { return this } + addContains(key, value) { + this.query.contains[key] = value + return this + } + /** * Preprocesses a value before going into a lucene search. * Transforms strings to lowercase and wraps strings and bools in quotes. @@ -164,6 +170,31 @@ class QueryBuilder { return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` } + const contains = (key, value) => { + if (!value && value !== 0) { + return null + } + return `${key}:${builder.preprocess(value, { escape: true })}` + } + + const oneOf = (key, value) => { + if (!Array.isArray(value)) { + if (typeof value === "string") { + value = value.split(",") + } else { + return "" + } + } + let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}` + for (let i = 1; i < value.length; i++) { + orStatement += ` OR ${builder.preprocess( + value[i], + allPreProcessingOpts + )}` + } + return `${key}:(${orStatement})` + } + function build(structure, queryFn) { for (let [key, value] of Object.entries(structure)) { key = builder.preprocess(key.replace(/ /g, "_"), { @@ -239,26 +270,10 @@ class QueryBuilder { build(this.query.notEmpty, key => `${key}:["" TO *]`) } if (this.query.oneOf) { - build(this.query.oneOf, (key, value) => { - if (!Array.isArray(value)) { - if (typeof value === "string") { - value = value.split(",") - } else { - return "" - } - } - let orStatement = `${builder.preprocess( - value[0], - allPreProcessingOpts - )}` - for (let i = 1; i < value.length; i++) { - orStatement += ` OR ${builder.preprocess( - value[i], - allPreProcessingOpts - )}` - } - return `${key}:(${orStatement})` - }) + build(this.query.oneOf, oneOf) + } + if (this.query.contains) { + build(this.query.contains, contains) } // make sure table ID is always added as an AND if (tableId) { diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index 90c81abe9f..9752fc947a 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -131,6 +131,9 @@ export interface SearchFilters { oneOf?: { [key: string]: any[] } + contains?: { + [key: string]: any + } } export interface SortJson { diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index fbbc42151a..750564c6ff 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -142,6 +142,21 @@ class InternalBuilder { } } } + + const like = (key: string, value: any) => { + const fnc = allOr ? "orWhere" : "where" + // postgres supports ilike, nothing else does + if (this.client === SqlClients.POSTGRES) { + query = query[fnc](key, "ilike", `%${value}%`) + } else { + const rawFnc = `${fnc}Raw` + // @ts-ignore + query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ + `%${value}%`, + ]) + } + } + if (!filters) { return query } @@ -168,19 +183,7 @@ class InternalBuilder { }) } if (filters.fuzzy) { - iterate(filters.fuzzy, (key, value) => { - const fnc = allOr ? "orWhere" : "where" - // postgres supports ilike, nothing else does - if (this.client === SqlClients.POSTGRES) { - query = query[fnc](key, "ilike", `%${value}%`) - } else { - const rawFnc = `${fnc}Raw` - // @ts-ignore - query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ - `%${value}%`, - ]) - } - }) + iterate(filters.fuzzy, like) } if (filters.range) { iterate(filters.range, (key, value) => { @@ -223,6 +226,34 @@ class InternalBuilder { query = query[fnc](key) }) } + if (filters.contains) { + const fnc = allOr ? "orWhere" : "where" + const rawFnc = `${fnc}Raw` + if (this.client === SqlClients.POSTGRES) { + iterate(filters.contains, (key: string, value: any) => { + const fieldNames = key.split(/\./g) + const tableName = fieldNames[0] + const columnName = fieldNames[1] + if (typeof value === "string") { + value = `"${value}"` + } + // @ts-ignore + query = query[rawFnc]( + `"${tableName}"."${columnName}"::jsonb @> '[${value}]'` + ) + }) + } else if (this.client === SqlClients.MY_SQL) { + iterate(filters.contains, (key: string, value: any) => { + if (typeof value === "string") { + value = `"${value}"` + } + // @ts-ignore + query = query[rawFnc](`JSON_CONTAINS(${key}, '${value}')`) + }) + } else { + iterate(filters.contains, like) + } + } return query } diff --git a/packages/server/src/integrations/tests/sql.spec.js b/packages/server/src/integrations/tests/sql.spec.js index c2e65c56b7..55c762573a 100644 --- a/packages/server/src/integrations/tests/sql.spec.js +++ b/packages/server/src/integrations/tests/sql.spec.js @@ -1,4 +1,5 @@ const Sql = require("../base/sql") +const { SqlClients } = require("../utils") const TABLE_NAME = "test" @@ -46,7 +47,7 @@ function generateDeleteJson(table = TABLE_NAME, filters = {}) { describe("SQL query builder", () => { const limit = 500 - const client = "pg" + const client = SqlClients.POSTGRES let sql beforeEach(() => { @@ -173,15 +174,15 @@ describe("SQL query builder", () => { }) it("should work with MS-SQL", () => { - const query = new Sql("mssql", 10)._query(generateReadJson()) + const query = new Sql(SqlClients.MS_SQL, 10)._query(generateReadJson()) expect(query).toEqual({ bindings: [10], sql: `select * from (select top (@p0) * from [${TABLE_NAME}]) as [${TABLE_NAME}]` }) }) - it("should work with mySQL", () => { - const query = new Sql("mysql", 10)._query(generateReadJson()) + it("should work with MySQL", () => { + const query = new Sql(SqlClients.MY_SQL, 10)._query(generateReadJson()) expect(query).toEqual({ bindings: [10], sql: `select * from (select * from \`${TABLE_NAME}\` limit ?) as \`${TABLE_NAME}\`` @@ -238,4 +239,49 @@ describe("SQL query builder", () => { sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"` }) }) + + it("should use like expression for MS-SQL when filter is contains", () => { + const query = new Sql(SqlClients.MS_SQL, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10, "%20%", "%John%"], + sql: `select * from (select top (@p0) * from [${TABLE_NAME}] where LOWER(${TABLE_NAME}.age) LIKE @p1 and LOWER(${TABLE_NAME}.name) LIKE @p2) as [${TABLE_NAME}]` + }) + }) + + it("should use JSON_CONTAINS expression for MySQL when filter is contains", () => { + const query = new Sql(SqlClients.MY_SQL, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10], + sql: `select * from (select * from \`${TABLE_NAME}\` where JSON_CONTAINS(${TABLE_NAME}.age, '20') and JSON_CONTAINS(${TABLE_NAME}.name, '"John"') limit ?) as \`${TABLE_NAME}\`` + }) + }) + + it("should use jsonb operator expression for PostgreSQL when filter is contains", () => { + const query = new Sql(SqlClients.POSTGRES, 10)._query(generateReadJson({ + filters: { + contains: { + age: 20, + name: "John" + } + } + })) + expect(query).toEqual({ + bindings: [10], + sql: `select * from (select * from \"${TABLE_NAME}\" where \"${TABLE_NAME}\".\"age\"::jsonb @> '[20]' and \"${TABLE_NAME}\".\"name\"::jsonb @> '["John"]' limit $1) as \"${TABLE_NAME}\"` + }) + }) }) diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index c7fca714ca..1fa97718ef 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 3d68984e5b..3233361991 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 5f5648e8b6..b16c661f40 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.1.18-alpha.2", + "version": "1.1.18-alpha.4", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -35,10 +35,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^1.1.18-alpha.2", - "@budibase/pro": "1.1.18-alpha.2", - "@budibase/string-templates": "^1.1.18-alpha.2", - "@budibase/types": "^1.1.18-alpha.2", + "@budibase/backend-core": "^1.1.18-alpha.4", + "@budibase/pro": "1.1.18-alpha.4", + "@budibase/string-templates": "^1.1.18-alpha.4", + "@budibase/types": "^1.1.18-alpha.4", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2",