diff --git a/.eslintrc.json b/.eslintrc.json index 9dab2f1a88..f614f1ad91 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -92,7 +92,8 @@ // differs to external, but the API is broadly the same "jest/no-conditional-expect": "off", // have to turn this off to allow function overloading in typescript - "no-dupe-class-members": "off" + "no-dupe-class-members": "off", + "no-redeclare": "off" } }, { diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index eb11627758..b7dbcae771 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -73,9 +73,9 @@ jobs: - name: Check types run: | if ${{ env.USE_NX_AFFECTED }}; then - yarn check:types --since=${{ env.NX_BASE_BRANCH }} + yarn check:types --since=${{ env.NX_BASE_BRANCH }} --ignore @budibase/account-portal-server else - yarn check:types + yarn check:types --ignore @budibase/account-portal-server fi helm-lint: diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index 1f34ca2ee2..93e26a954d 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -333,11 +333,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" bulma@^0.9.3: version "0.9.3" @@ -781,10 +781,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1709,10 +1709,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" - integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== unbox-primitive@^1.0.1: version "1.0.1" diff --git a/lerna.json b/lerna.json index 70d6a683e0..76d39a69b3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.28.6", + "version": "2.29.2", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/package.json b/package.json index 5377dfc5a1..e05eb795bc 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui", "build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal-server --scope @budibase/account-portal-ui", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", - "check:types": "lerna run --concurrency 2 check:types", + "check:types": "lerna run --concurrency 2 check:types --ignore @budibase/account-portal-server", "build:sdk": "lerna run --stream build:sdk", "deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular", "release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset", diff --git a/packages/account-portal b/packages/account-portal index 247f56d455..ff16525b73 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 247f56d455abbd64da17d865275ed978f577549f +Subproject commit ff16525b73c5751d344f5c161a682609c0a993f2 diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 2fd713119b..3085b91ef1 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -72,4 +72,4 @@ export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs" export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory" export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses" export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee" -export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default" +export { DEFAULT_BB_DATASOURCE_ID } from "@budibase/shared-core" diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index bfa7595d62..69c98fe569 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -1,14 +1,5 @@ -export const CONSTANT_INTERNAL_ROW_COLS = [ - "_id", - "_rev", - "type", - "createdAt", - "updatedAt", - "tableId", -] as const - -export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const - -export function isInternalColumnName(name: string): boolean { - return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name) -} +export { + CONSTANT_INTERNAL_ROW_COLS, + CONSTANT_EXTERNAL_ROW_COLS, + isInternalColumnName, +} from "@budibase/shared-core" diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 61d5849058..cdc5f3d3c8 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -1,10 +1,10 @@ import { Knex, knex } from "knex" import * as dbCore from "../db" import { - isIsoDateString, - isValidFilter, getNativeSql, isExternalTable, + isIsoDateString, + isValidFilter, } from "./utils" import { SqlStatements } from "./sqlStatements" import SqlTableQueryBuilder from "./sqlTable" @@ -12,21 +12,21 @@ import { BBReferenceFieldMetadata, FieldSchema, FieldType, + INTERNAL_TABLE_SOURCE_ID, JsonFieldMetadata, + JsonTypes, Operation, + prefixed, QueryJson, - SqlQuery, + QueryOptions, RelationshipsJson, SearchFilters, - SortDirection, + SortOrder, + SqlClient, + SqlQuery, SqlQueryBinding, Table, TableSourceType, - INTERNAL_TABLE_SOURCE_ID, - SqlClient, - QueryOptions, - JsonTypes, - prefixed, } from "@budibase/types" import environment from "../environment" import { helpers } from "@budibase/shared-core" @@ -114,7 +114,7 @@ function generateSelectStatement( ): (string | Knex.Raw)[] | "*" { const { resource, meta } = json - if (!resource) { + if (!resource || !resource.fields || resource.fields.length === 0) { return "*" } @@ -410,28 +410,50 @@ class InternalBuilder { return query } - addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder { - let { sort, paginate } = json + addDistinctCount( + query: Knex.QueryBuilder, + json: QueryJson + ): Knex.QueryBuilder { const table = json.meta.table + const primary = table.primary + const aliases = json.tableAliases + const aliased = + table.name && aliases?.[table.name] ? aliases[table.name] : table.name + if (!primary) { + throw new Error("SQL counting requires primary key to be supplied") + } + return query.countDistinct(`${aliased}.${primary[0]} as total`) + } + + addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder { + let { sort } = json + const table = json.meta.table + const primaryKey = table.primary const tableName = getTableName(table) const aliases = json.tableAliases const aliased = tableName && aliases?.[tableName] ? aliases[tableName] : table?.name + if (!Array.isArray(primaryKey)) { + throw new Error("Sorting requires primary key to be specified for table") + } if (sort && Object.keys(sort || {}).length > 0) { for (let [key, value] of Object.entries(sort)) { const direction = - value.direction === SortDirection.ASCENDING ? "asc" : "desc" + value.direction === SortOrder.ASCENDING ? "asc" : "desc" let nulls if (this.client === SqlClient.POSTGRES) { // All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues - nulls = value.direction === SortDirection.ASCENDING ? "first" : "last" + nulls = value.direction === SortOrder.ASCENDING ? "first" : "last" } query = query.orderBy(`${aliased}.${key}`, direction, nulls) } - } else if (this.client === SqlClient.MS_SQL && paginate?.limit) { - // @ts-ignore - query = query.orderBy(`${aliased}.${table?.primary[0]}`) + } + + // add sorting by the primary key if the result isn't already sorted by it, + // to make sure result is deterministic + if (!sort || sort[primaryKey[0]] === undefined) { + query = query.orderBy(`${aliased}.${primaryKey[0]}`) } return query } @@ -522,7 +544,7 @@ class InternalBuilder { }) } } - return query.limit(BASE_LIMIT) + return query } knexWithAlias( @@ -533,13 +555,12 @@ class InternalBuilder { const tableName = endpoint.entityId const tableAlias = aliases?.[tableName] - const query = knex( + return knex( this.tableNameWithSchema(tableName, { alias: tableAlias, schema: endpoint.schema, }) ) - return query } create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder { @@ -571,52 +592,94 @@ class InternalBuilder { return query.insert(parsedBody) } - read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder { - let { endpoint, resource, filters, paginate, relationships, tableAliases } = - json + bulkUpsert(knex: Knex, json: QueryJson): Knex.QueryBuilder { + const { endpoint, body } = json + let query = this.knexWithAlias(knex, endpoint) + if (!Array.isArray(body)) { + return query + } + const parsedBody = body.map(row => parseBody(row)) + if ( + this.client === SqlClient.POSTGRES || + this.client === SqlClient.SQL_LITE || + this.client === SqlClient.MY_SQL + ) { + const primary = json.meta.table.primary + if (!primary) { + throw new Error("Primary key is required for upsert") + } + const ret = query.insert(parsedBody).onConflict(primary).merge() + return ret + } else if (this.client === SqlClient.MS_SQL) { + // No upsert or onConflict support in MSSQL yet, see: + // https://github.com/knex/knex/pull/6050 + return query.insert(parsedBody) + } + return query.upsert(parsedBody) + } + + read( + knex: Knex, + json: QueryJson, + opts: { + limits?: { base: number; query: number } + } = {} + ): Knex.QueryBuilder { + let { endpoint, filters, paginate, relationships, tableAliases } = json + const { limits } = opts + const counting = endpoint.operation === Operation.COUNT const tableName = endpoint.entityId - // select all if not specified - if (!resource) { - resource = { fields: [] } - } - let selectStatement: string | (string | Knex.Raw)[] = "*" - // handle select - if (resource.fields && resource.fields.length > 0) { - // select the resources as the format "table.columnName" - this is what is provided - // by the resource builder further up - selectStatement = generateSelectStatement(json, knex) - } - let foundLimit = limit || BASE_LIMIT + // start building the query + let query = this.knexWithAlias(knex, endpoint, tableAliases) // handle pagination let foundOffset: number | null = null + let foundLimit = limits?.query || limits?.base if (paginate && paginate.page && paginate.limit) { // @ts-ignore const page = paginate.page <= 1 ? 0 : paginate.page - 1 const offset = page * paginate.limit foundLimit = paginate.limit foundOffset = offset + } else if (paginate && paginate.offset && paginate.limit) { + foundLimit = paginate.limit + foundOffset = paginate.offset } else if (paginate && paginate.limit) { foundLimit = paginate.limit } - // start building the query - let query = this.knexWithAlias(knex, endpoint, tableAliases) - query = query.limit(foundLimit) - if (foundOffset) { - query = query.offset(foundOffset) + // counting should not sort, limit or offset + if (!counting) { + // add the found limit if supplied + if (foundLimit != null) { + query = query.limit(foundLimit) + } + // add overall pagination + if (foundOffset != null) { + query = query.offset(foundOffset) + } + // add sorting to pre-query + // no point in sorting when counting + query = this.addSorting(query, json) } + // add filters to the query (where) query = this.addFilters(query, filters, json.meta.table, { aliases: tableAliases, }) - // add sorting to pre-query - query = this.addSorting(query, json) const alias = tableAliases?.[tableName] || tableName - let preQuery = knex({ - [alias]: query, - } as any).select(selectStatement) as any + let preQuery: Knex.QueryBuilder = knex({ + // the typescript definition for the knex constructor doesn't support this + // syntax, but it is the only way to alias a pre-query result as part of + // a query - there is an alias dictionary type, but it assumes it can only + // be a table name, not a pre-query + [alias]: query as any, + }) + // if counting, use distinct count, else select + preQuery = !counting + ? preQuery.select(generateSelectStatement(json, knex)) + : this.addDistinctCount(preQuery, json) // have to add after as well (this breaks MS-SQL) - if (this.client !== SqlClient.MS_SQL) { + if (this.client !== SqlClient.MS_SQL && !counting) { preQuery = this.addSorting(preQuery, json) } // handle joins @@ -627,6 +690,13 @@ class InternalBuilder { endpoint.schema, tableAliases ) + + // add a base limit over the whole query + // if counting we can't set this limit + if (limits?.base) { + query = query.limit(limits.base) + } + return this.addFilters(query, filters, json.meta.table, { relationship: true, aliases: tableAliases, @@ -671,6 +741,19 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { this.limit = limit } + private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) { + const sqlClient = this.getSqlClient() + if (opts?.disableBindings) { + return { sql: query.toString() } + } else { + let native = getNativeSql(query) + if (sqlClient === SqlClient.SQL_LITE) { + native = convertBooleans(native) + } + return native + } + } + /** * @param json The JSON query DSL which is to be converted to SQL. * @param opts extra options which are to be passed into the query builder, e.g. disableReturning @@ -694,7 +777,16 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { query = builder.create(client, json, opts) break case Operation.READ: - query = builder.read(client, json, this.limit) + query = builder.read(client, json, { + limits: { + query: this.limit, + base: BASE_LIMIT, + }, + }) + break + case Operation.COUNT: + // read without any limits to count + query = builder.read(client, json) break case Operation.UPDATE: query = builder.update(client, json, opts) @@ -705,6 +797,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { case Operation.BULK_CREATE: query = builder.bulkCreate(client, json) break + case Operation.BULK_UPSERT: + query = builder.bulkUpsert(client, json) + break case Operation.CREATE_TABLE: case Operation.UPDATE_TABLE: case Operation.DELETE_TABLE: @@ -713,15 +808,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { throw `Operation type is not supported by SQL query builder` } - if (opts?.disableBindings) { - return { sql: query.toString() } - } else { - let native = getNativeSql(query) - if (sqlClient === SqlClient.SQL_LITE) { - native = convertBooleans(native) - } - return native - } + return this.convertToNative(query, opts) } async getReturningRow(queryFn: QueryFunction, json: QueryJson) { @@ -797,6 +884,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { await this.getReturningRow(queryFn, this.checkLookupKeys(id, json)) ) } + if (operation === Operation.COUNT) { + return results + } if (operation !== Operation.READ) { return row } diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts index 09f9908baa..bdc8a3dd69 100644 --- a/packages/backend-core/src/sql/sqlTable.ts +++ b/packages/backend-core/src/sql/sqlTable.ts @@ -109,8 +109,10 @@ function generateSchema( const { tableName } = breakExternalTableId(column.tableId) // @ts-ignore const relatedTable = tables[tableName] - if (!relatedTable) { - throw new Error("Referenced table doesn't exist") + if (!relatedTable || !relatedTable.primary) { + throw new Error( + "Referenced table doesn't exist or has no primary keys" + ) } const relatedPrimary = relatedTable.primary[0] const externalType = relatedTable.schema[relatedPrimary].externalType diff --git a/packages/backend-core/src/sql/utils.ts b/packages/backend-core/src/sql/utils.ts index 2d9b289417..45ab510948 100644 --- a/packages/backend-core/src/sql/utils.ts +++ b/packages/backend-core/src/sql/utils.ts @@ -55,10 +55,7 @@ export function buildExternalTableId(datasourceId: string, tableName: string) { return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}` } -export function breakExternalTableId(tableId: string | undefined) { - if (!tableId) { - return {} - } +export function breakExternalTableId(tableId: string) { const parts = tableId.split(DOUBLE_SEPARATOR) let datasourceId = parts.shift() // if they need joined @@ -67,6 +64,9 @@ export function breakExternalTableId(tableId: string | undefined) { if (tableName.includes(ENCODED_SPACE)) { tableName = decodeURIComponent(tableName) } + if (!datasourceId || !tableName) { + throw new Error("Unable to get datasource/table name from table ID") + } return { datasourceId, tableName } } diff --git a/packages/backend-core/src/tenancy/db.ts b/packages/backend-core/src/tenancy/db.ts index f2e4705fa8..d430296a5c 100644 --- a/packages/backend-core/src/tenancy/db.ts +++ b/packages/backend-core/src/tenancy/db.ts @@ -9,8 +9,13 @@ export function getTenantDB(tenantId: string) { export async function saveTenantInfo(tenantInfo: TenantInfo) { const db = getTenantDB(tenantInfo.tenantId) // save the tenant info to db - return await db.put({ + return db.put({ _id: "tenant_info", ...tenantInfo, }) } + +export async function getTenantInfo(tenantId: string): Promise { + const db = getTenantDB(tenantId) + return db.get("tenant_info") +} diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 7dcc2de116..29453ad60a 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -24,7 +24,6 @@ export const account = (partial: Partial = {}): Account => { createdAt: Date.now(), verified: true, verificationSent: true, - tier: "FREE", // DEPRECATED authType: AuthType.PASSWORD, name: generator.name(), size: "10+", diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 4656be69d1..dec1455d0c 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -162,6 +162,7 @@ max-height: 100%; } .modal-inner-wrapper { + padding: 40px; flex: 1 1 auto; display: flex; flex-direction: row; @@ -176,7 +177,6 @@ border: 2px solid var(--spectrum-global-color-gray-200); overflow: visible; max-height: none; - margin: 40px 0; transform: none; --spectrum-dialog-confirm-border-radius: var( --spectrum-global-dimension-size-100 diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 26484e8144..393af940af 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -120,6 +120,8 @@ ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] : [] + let testDataRowVisibility = {} + const getInputData = (testData, blockInputs) => { // Test data is not cloned for reactivity let newInputData = testData || cloneDeep(blockInputs) @@ -417,7 +419,8 @@ (automation.trigger?.event === AutomationEventType.ROW_UPDATE || automation.trigger?.event === AutomationEventType.ROW_SAVE) ) { - if (name !== "id" && name !== "revision") return `trigger.row.${name}` + let noRowKeywordBindings = ["id", "revision", "oldRow"] + if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` } /* End special cases for generating custom schemas based on triggers */ @@ -601,7 +604,7 @@ function getFieldLabel(key, value) { const requiredSuffix = requiredProperties.includes(key) ? "*" : "" const label = `${ - value.title || (key === "row" ? "Table" : key) + value.title || (key === "row" ? "Row" : key) } ${requiredSuffix}` return Helpers.capitalise(label) } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 17ecd8f844..d79eedd194 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -17,6 +17,8 @@ SWITCHABLE_TYPES, ValidColumnNameRegex, helpers, + CONSTANT_INTERNAL_ROW_COLS, + CONSTANT_EXTERNAL_ROW_COLS, } from "@budibase/shared-core" import { createEventDispatcher, getContext, onMount } from "svelte" import { cloneDeep } from "lodash/fp" @@ -52,7 +54,6 @@ const DATE_TYPE = FieldType.DATETIME const dispatch = createEventDispatcher() - const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] const { dispatch: gridDispatch, rows } = getContext("grid") export let field @@ -487,20 +488,27 @@ }) } const newError = {} + const prohibited = externalTable + ? CONSTANT_EXTERNAL_ROW_COLS + : CONSTANT_INTERNAL_ROW_COLS if (!externalTable && fieldInfo.name?.startsWith("_")) { newError.name = `Column name cannot start with an underscore.` } else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) { newError.name = `Illegal character; must be alpha-numeric.` - } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) { - newError.name = `${PROHIBITED_COLUMN_NAMES.join( + } else if ( + prohibited.some( + name => fieldInfo?.name?.toLowerCase() === name.toLowerCase() + ) + ) { + newError.name = `${prohibited.join( ", " - )} are not allowed as column names` + )} are not allowed as column names - case insensitive.` } else if (inUse($tables.selected, fieldInfo.name, originalName)) { newError.name = `Column name already in use.` } if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) { - newError.subtype = `Auto Column requires a type` + newError.subtype = `Auto Column requires a type.` } if (fieldInfo.fieldName && fieldInfo.tableId) { diff --git a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte index de56fa8ce5..80655d1099 100644 --- a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte +++ b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte @@ -1,9 +1,14 @@ + +
+ + + + + {/if}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js index 587993377d..606ee41d02 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js @@ -21,5 +21,7 @@ export { default as ShowNotification } from "./ShowNotification.svelte" export { default as PromptUser } from "./PromptUser.svelte" export { default as OpenSidePanel } from "./OpenSidePanel.svelte" export { default as CloseSidePanel } from "./CloseSidePanel.svelte" +export { default as OpenModal } from "./OpenModal.svelte" +export { default as CloseModal } from "./CloseModal.svelte" export { default as ClearRowSelection } from "./ClearRowSelection.svelte" export { default as DownloadFile } from "./DownloadFile.svelte" diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 2840a0d662..4022926e7f 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -157,6 +157,18 @@ "component": "CloseSidePanel", "dependsOnFeature": "sidePanel" }, + { + "name": "Open Modal", + "type": "application", + "component": "OpenModal", + "dependsOnFeature": "modal" + }, + { + "name": "Close Modal", + "type": "application", + "component": "CloseModal", + "dependsOnFeature": "modal" + }, { "name": "Clear Row Selection", "type": "data", diff --git a/packages/builder/src/components/design/settings/controls/Explanation/Explanation.svelte b/packages/builder/src/components/design/settings/controls/Explanation/Explanation.svelte index bc45e410c9..42ae9ed783 100644 --- a/packages/builder/src/components/design/settings/controls/Explanation/Explanation.svelte +++ b/packages/builder/src/components/design/settings/controls/Explanation/Explanation.svelte @@ -18,14 +18,11 @@ import subjects from "./subjects" import { appStore } from "stores/builder" - export let explanation - export let columnIcon - export let columnType - export let columnName - export let tableHref = () => {} - export let schema + export let name + export let explanation + export let componentName $: explanationWithPresets = getExplanationWithPresets( explanation, @@ -54,14 +51,8 @@
- - + + {#if messages.includes(messageConstants.stringAsNumber)} {/if} @@ -84,7 +75,7 @@ {#if detailsModalSubject !== subjects.none} - import { - Line, - InfoWord, - DocumentationLink, - Text, - Period, - } from "../typography" + import { Line, InfoWord, DocumentationLink, Text } from "../typography" + import { FieldType } from "@budibase/types" + import { FIELDS } from "constants/backend" import subjects from "../subjects" - export let columnName - export let columnIcon - export let columnType + export let schema + export let name export let tableHref export let setExplanationSubject + const getTypeName = schema => { + const fieldDefinition = Object.values(FIELDS).find( + f => f.type === schema?.type + ) + + if (schema?.type === "jsonarray") { + return "JSON Array" + } + if (schema?.type === "options") { + return "Options" + } + + return fieldDefinition?.name || schema?.type || "Unknown" + } + + const getTypeIcon = schema => { + const fieldDefinition = Object.values(FIELDS).find( + f => f.type === schema?.type + ) + + if (schema?.type === "jsonarray") { + return "BracketsSquare" + } + + return fieldDefinition?.icon || "Circle" + } + const getDocLink = columnType => { - if (columnType === "Number") { + if (columnType === FieldType.NUMBER) { return "https://docs.budibase.com/docs/number" } - if (columnType === "Text") { + if (columnType === FieldType.STRING) { return "https://docs.budibase.com/docs/text" } - if (columnType === "Attachment") { + if (columnType === FieldType.LONGFORM) { + return "https://docs.budibase.com/docs/text" + } + if (columnType === FieldType.ATTACHMENT_SINGLE) { return "https://docs.budibase.com/docs/attachments" } - if (columnType === "Multi-select") { + if (columnType === FieldType.ATTACHMENTS) { + // No distinct multi attachment docs, link to attachment instead + return "https://docs.budibase.com/docs/attachments" + } + if (columnType === FieldType.ARRAY) { return "https://docs.budibase.com/docs/multi-select" } - if (columnType === "JSON") { + if (columnType === FieldType.JSON) { return "https://docs.budibase.com/docs/json" } - if (columnType === "Date/Time") { + if (columnType === "jsonarray") { + return "https://docs.budibase.com/docs/json" + } + if (columnType === FieldType.DATETIME) { return "https://docs.budibase.com/docs/datetime" } - if (columnType === "User") { - return "https://docs.budibase.com/docs/user" + if (columnType === FieldType.BB_REFERENCE_SINGLE) { + return "https://docs.budibase.com/docs/users-1" } - if (columnType === "QR") { + if (columnType === FieldType.BB_REFERENCE) { + return "https://docs.budibase.com/docs/users-1" + } + if (columnType === FieldType.BARCODEQR) { return "https://docs.budibase.com/docs/barcodeqr" } - if (columnType === "Relationship") { + if (columnType === FieldType.LINK) { return "https://docs.budibase.com/docs/relationships" } - if (columnType === "Formula") { + if (columnType === FieldType.FORMULA) { return "https://docs.budibase.com/docs/formula" } - if (columnType === "Options") { + if (columnType === FieldType.OPTIONS) { return "https://docs.budibase.com/docs/options" } - if (columnType === "BigInt") { - // No BigInt docs - return null - } - if (columnType === "Boolean") { + if (columnType === FieldType.BOOLEAN) { return "https://docs.budibase.com/docs/boolean-truefalse" } - if (columnType === "Signature") { + if (columnType === FieldType.SIGNATURE_SINGLE) { // No Signature docs return null } + if (columnType === FieldType.BIGINT) { + // No BigInt docs + return null + } return null } - $: docLink = getDocLink(columnType) + // NOTE The correct indefinite article is based on the pronounciation of the word it precedes, not the spelling. So simply checking if the word begins with a vowel is not sufficient. + + // e.g., `an honor`, `a user` + const getIndefiniteArticle = schema => { + const anTypes = [ + FieldType.OPTIONS, + null, // `null` gets parsed as "unknown" + undefined, // `undefined` gets parsed as "unknown" + ] + + if (anTypes.includes(schema?.type)) { + return "an" + } + + return "a" + } + + $: columnTypeName = getTypeName(schema) + $: columnIcon = getTypeIcon(schema) + $: docLink = getDocLink(schema?.type) + $: indefiniteArticle = getIndefiniteArticle(schema) @@ -71,14 +126,14 @@ on:mouseenter={() => setExplanationSubject(subjects.column)} on:mouseleave={() => setExplanationSubject(subjects.none)} href={tableHref} - text={columnName} + text={name} /> - + - + diff --git a/packages/builder/src/components/design/settings/controls/Explanation/lines/Support.svelte b/packages/builder/src/components/design/settings/controls/Explanation/lines/Support.svelte index 848ab208fb..ccb33798d7 100644 --- a/packages/builder/src/components/design/settings/controls/Explanation/lines/Support.svelte +++ b/packages/builder/src/components/design/settings/controls/Explanation/lines/Support.svelte @@ -2,9 +2,16 @@ import { Line, InfoWord, DocumentationLink, Text } from "../typography" import subjects from "../subjects" import * as explanation from "../explanation" + import { componentStore } from "stores/builder" export let setExplanationSubject export let support + export let componentName + + const getComponentDefinition = componentName => { + const components = $componentStore.components || {} + return components[componentName] || null + } const getIcon = support => { if (support === explanation.support.unsupported) { @@ -39,21 +46,24 @@ $: icon = getIcon(support) $: color = getColor(support) $: text = getText(support) + $: componentDefinition = getComponentDefinition(componentName) - - setExplanationSubject(subjects.support)} - on:mouseleave={() => setExplanationSubject(subjects.none)} - {icon} - {color} - {text} - /> - - - - +{#if componentDefinition} + + setExplanationSubject(subjects.support)} + on:mouseleave={() => setExplanationSubject(subjects.none)} + {icon} + {color} + {text} + /> + + + + +{/if} diff --git a/packages/builder/src/components/design/settings/controls/FieldSelect.svelte b/packages/builder/src/components/design/settings/controls/FieldSelect.svelte index 15b67ded18..aab7eb60a5 100644 --- a/packages/builder/src/components/design/settings/controls/FieldSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldSelect.svelte @@ -6,8 +6,6 @@ import { Explanation } from "./Explanation" import { debounce } from "lodash" import { params } from "@roxi/routify" - import { Constants } from "@budibase/frontend-core" - import { FIELDS } from "constants/backend" export let componentInstance = {} export let value = "" @@ -60,35 +58,6 @@ const onOptionMouseleave = e => { updateTooltip(e, null) } - const getOptionIcon = optionKey => { - const option = schema[optionKey] - if (!option) return "" - - if (option.autocolumn) { - return "MagicWand" - } - const { type, subtype } = option - - const result = - typeof Constants.TypeIconMap[type] === "object" && subtype - ? Constants.TypeIconMap[type][subtype] - : Constants.TypeIconMap[type] - - return result || "Text" - } - - const getOptionIconTooltip = optionKey => { - const option = schema[optionKey] - - const type = option?.type - const field = Object.values(FIELDS).find(f => f.type === type) - - if (field) { - return field.name - } - - return "" - } { if (e.key.toLowerCase() === "enter") { @@ -158,7 +158,32 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + position: relative; + padding: 5px; + right: 6px; + border: 1px solid transparent; + border-radius: 3px; + transition: 150ms background-color, 150ms border-color, 150ms color; } + + .input:hover, + .input:focus { + cursor: text; + background-color: var( + --spectrum-textfield-m-background-color, + var(--spectrum-global-color-gray-50) + ); + border: 1px solid white; + border-color: var( + --spectrum-textfield-m-border-color, + var(--spectrum-alias-border-color) + ); + color: var( + --spectrum-textfield-m-text-color, + var(--spectrum-alias-text-color) + ); + } + .panel-title-content { display: contents; } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index c7c58a6e16..361e07a026 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -59,7 +59,14 @@ // Build up list of illegal children from ancestors let illegalChildren = definition.illegalChildren || [] path.forEach(ancestor => { - if (ancestor._component === `@budibase/standard-components/sidepanel`) { + // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level. + // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here. + if ( + [ + "@budibase/standard-components/sidepanel", + "@budibase/standard-components/modal", + ].includes(ancestor._component) + ) { illegalChildren = [] } const def = componentStore.getDefinition(ancestor._component) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index ba6f403d81..ff58a66221 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -14,7 +14,7 @@ { "name": "Layout", "icon": "ClassicGridView", - "children": ["container", "section", "sidepanel"] + "children": ["container", "section", "sidepanel", "modal"] }, { "name": "Data", diff --git a/packages/builder/src/pages/builder/maintenance/index.svelte b/packages/builder/src/pages/builder/maintenance/index.svelte index e4c379885a..f7eb16ab81 100644 --- a/packages/builder/src/pages/builder/maintenance/index.svelte +++ b/packages/builder/src/pages/builder/maintenance/index.svelte @@ -33,7 +33,8 @@ {/if} diff --git a/packages/builder/src/stores/builder/screens.js b/packages/builder/src/stores/builder/screens.js index 7339593960..b1bef10c36 100644 --- a/packages/builder/src/stores/builder/screens.js +++ b/packages/builder/src/stores/builder/screens.js @@ -125,7 +125,14 @@ export class ScreenStore extends BudiStore { return } - if (type === "@budibase/standard-components/sidepanel") { + // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level. + // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here. + if ( + [ + "@budibase/standard-components/sidepanel", + "@budibase/standard-components/modal", + ].includes(type) + ) { illegalChildren = [] } diff --git a/packages/cli/package.json b/packages/cli/package.json index c1ba49c5e7..88d5926ae3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ "pouchdb": "7.3.0", "pouchdb-replication-stream": "1.2.9", "randomstring": "1.1.5", - "tar": "6.1.15", + "tar": "6.2.1", "yaml": "^2.1.1" }, "devDependencies": { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index d3dbb74280..00b503626f 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -11,6 +11,7 @@ "continueIfAction": true, "showNotificationAction": true, "sidePanel": true, + "modal": true, "skeletonLoader": true }, "typeSupportPresets": { @@ -5223,6 +5224,7 @@ ] }, "chartblock": { + "documentationLink": "https://docs.budibase.com/docs/chart", "block": true, "name": "Chart Block", "icon": "GraphPie", @@ -6974,7 +6976,7 @@ "name": "Side Panel", "icon": "RailRight", "hasChildren": true, - "illegalChildren": ["section", "sidepanel"], + "illegalChildren": ["section", "sidepanel", "modal"], "showEmptyState": false, "draggable": false, "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.", @@ -6992,6 +6994,52 @@ } ] }, + "modal": { + "name": "Modal", + "icon": "MBox", + "hasChildren": true, + "illegalChildren": ["section", "modal", "sidepanel"], + "showEmptyState": false, + "draggable": false, + "info": "Modals are hidden by default. They will only be revealed when triggered by the 'Open Modal' action.", + "settings": [ + { + "type": "boolean", + "key": "ignoreClicksOutside", + "label": "Ignore clicks outside", + "defaultValue": false + }, + { + "type": "event", + "key": "onClose", + "label": "On close" + }, + { + "type": "select", + "label": "Size", + "key": "size", + "defaultValue": "small", + "options": [ + { + "label": "Small", + "value": "small" + }, + { + "label": "Medium", + "value": "medium" + }, + { + "label": "Large", + "value": "large" + }, + { + "label": "Fullscreen", + "value": "fullscreen" + } + ] + } + ] + }, "rowexplorer": { "block": true, "name": "Row Explorer Block", diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index b790ecd0d4..9bfb1192ea 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -19,6 +19,8 @@ devToolsStore, devToolsEnabled, environmentStore, + sidePanelStore, + modalStore, } from "stores" import NotificationDisplay from "components/overlay/NotificationDisplay.svelte" import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte" @@ -102,6 +104,21 @@ embedded: !!$appStore.embedded, }) } + const handleHashChange = () => { + const { open: sidePanelOpen } = $sidePanelStore + if (sidePanelOpen) { + sidePanelStore.actions.close() + } + + const { open: modalOpen } = $modalStore + if (modalOpen) { + modalStore.actions.close() + } + } + window.addEventListener("hashchange", handleHashChange) + return () => { + window.removeEventListener("hashchange", handleHashChange) + } }) $: { diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index 72da3e9012..af74e14aa0 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -12,6 +12,7 @@ linkable, builderStore, sidePanelStore, + modalStore, appStore, } = sdk const context = getContext("context") @@ -77,6 +78,7 @@ !$builderStore.inBuilder && $sidePanelStore.open && !$sidePanelStore.ignoreClicksOutside + $: screenId = $builderStore.inBuilder ? `${$builderStore.screen?._id}-screen` : "screen" @@ -198,6 +200,7 @@ const handleClickLink = () => { mobileOpen = false sidePanelStore.actions.close() + modalStore.actions.close() } diff --git a/packages/client/src/components/app/Link.svelte b/packages/client/src/components/app/Link.svelte index 7eddcc6fe5..1ddc63066d 100644 --- a/packages/client/src/components/app/Link.svelte +++ b/packages/client/src/components/app/Link.svelte @@ -1,7 +1,7 @@ + + +{#if !$builderStore.inBuilder || open} + +
+ + +
+
+{/if} + + diff --git a/packages/client/src/components/app/SidePanel.svelte b/packages/client/src/components/app/SidePanel.svelte index bff5a78837..d49ab33c40 100644 --- a/packages/client/src/components/app/SidePanel.svelte +++ b/packages/client/src/components/app/SidePanel.svelte @@ -29,10 +29,6 @@ } } - // $: { - - // } - // Derive visibility $: open = $sidePanelStore.contentId === $component.id diff --git a/packages/client/src/components/app/blocks/ChartBlock.svelte b/packages/client/src/components/app/blocks/ChartBlock.svelte index ddfc7b522b..4bd2fec0da 100644 --- a/packages/client/src/components/app/blocks/ChartBlock.svelte +++ b/packages/client/src/components/app/blocks/ChartBlock.svelte @@ -35,6 +35,7 @@ export let valueUnits export let yAxisLabel export let xAxisLabel + export let yAxisUnits export let curve // Area @@ -85,6 +86,7 @@ valueUnits, yAxisLabel, xAxisLabel, + yAxisUnits, stacked, horizontal, curve, diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte index d249569731..e3aa20ffa6 100644 --- a/packages/client/src/components/app/blocks/form/FormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte @@ -31,41 +31,23 @@ let schema - $: formattedFields = convertOldFieldFormat(fields) - $: fieldsOrDefault = getDefaultFields(formattedFields, schema) $: fetchSchema(dataSource) $: id = $component.id - // We could simply spread $$props into the inner form and append our - // additions, but that would create svelte warnings about unused props and - // make maintenance in future more confusing as we typically always have a - // proper mapping of schema settings to component exports, without having to - // search multiple files - $: innerProps = { - dataSource, - actionUrl, - actionType, - size, - disabled, - fields: fieldsOrDefault, - title, - description, - schema, - notificationOverride, - buttons: - buttons || - Utils.buildFormBlockButtonConfig({ - _id: id, - showDeleteButton, - showSaveButton, - saveButtonLabel, - deleteButtonLabel, - notificationOverride, - actionType, - actionUrl, - dataSource, - }), - buttonPosition: buttons ? buttonPosition : "top", - } + $: formattedFields = convertOldFieldFormat(fields) + $: fieldsOrDefault = getDefaultFields(formattedFields, schema) + $: buttonsOrDefault = + buttons || + Utils.buildFormBlockButtonConfig({ + _id: id, + showDeleteButton, + showSaveButton, + saveButtonLabel, + deleteButtonLabel, + notificationOverride, + actionType, + actionUrl, + dataSource, + }) // Provide additional data context for live binding eval export const getAdditionalDataContext = () => { @@ -123,5 +105,18 @@ - + diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte index b0733f3f4b..0227107dd2 100644 --- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte @@ -91,15 +91,13 @@ {#if description} {/if} - {#key fields} - -
- {#each fields as field, idx} - - {/each} -
-
- {/key} + +
+ {#each fields as field, idx} + + {/each} +
+
{#if buttonPosition === "bottom"} {$confirmationStore.text} diff --git a/packages/client/src/components/preview/IndicatorSet.svelte b/packages/client/src/components/preview/IndicatorSet.svelte index 3cbd7e2464..2b941b2662 100644 --- a/packages/client/src/components/preview/IndicatorSet.svelte +++ b/packages/client/src/components/preview/IndicatorSet.svelte @@ -57,7 +57,9 @@ return } nextState.indicators[idx].visible = - nextState.indicators[idx].insideSidePanel || entries[0].isIntersecting + nextState.indicators[idx].insideModal || + nextState.indicators[idx].insideSidePanel || + entries[0].isIntersecting if (++callbackCount === observers.length) { state = nextState updating = false @@ -139,6 +141,7 @@ height: elBounds.height + 4, visible: false, insideSidePanel: !!child.closest(".side-panel"), + insideModal: !!child.closest(".modal-content"), }) }) } diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 90e0f9c7dc..50d3f857d5 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -11,6 +11,7 @@ import { currentRole, environmentStore, sidePanelStore, + modalStore, dndIsDragging, confirmationStore, roleStore, @@ -53,6 +54,7 @@ export default { componentStore, environmentStore, sidePanelStore, + modalStore, dndIsDragging, currentRole, confirmationStore, diff --git a/packages/client/src/stores/confirmation.js b/packages/client/src/stores/confirmation.js index bb9a54386f..3fbf3d5deb 100644 --- a/packages/client/src/stores/confirmation.js +++ b/packages/client/src/stores/confirmation.js @@ -4,6 +4,8 @@ const initialState = { showConfirmation: false, title: null, text: null, + confirmButtonText: null, + cancelButtonText: null, onConfirm: null, onCancel: null, } @@ -11,11 +13,20 @@ const initialState = { const createConfirmationStore = () => { const store = writable(initialState) - const showConfirmation = (title, text, onConfirm, onCancel) => { + const showConfirmation = ( + title, + text, + onConfirm, + onCancel, + confirmButtonText, + cancelButtonText + ) => { store.set({ showConfirmation: true, title, text, + confirmButtonText, + cancelButtonText, onConfirm, onCancel, }) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index e9b1ce4434..f2b80ed732 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -27,6 +27,7 @@ export { dndIsDragging, } from "./dnd" export { sidePanelStore } from "./sidePanel" +export { modalStore } from "./modal" export { hoverStore } from "./hover" // Context stores are layered and duplicated, so it is not a singleton diff --git a/packages/client/src/stores/modal.js b/packages/client/src/stores/modal.js new file mode 100644 index 0000000000..4d1331283d --- /dev/null +++ b/packages/client/src/stores/modal.js @@ -0,0 +1,32 @@ +import { writable } from "svelte/store" + +export const createModalStore = () => { + const initialState = { + contentId: null, + } + const store = writable(initialState) + + const open = id => { + store.update(state => { + state.contentId = id + return state + }) + } + + const close = () => { + store.update(state => { + state.contentId = null + return state + }) + } + + return { + subscribe: store.subscribe, + actions: { + open, + close, + }, + } +} + +export const modalStore = createModalStore() diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 482b36cdb8..8f0cb575a7 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -12,6 +12,7 @@ import { uploadStore, rowSelectionStore, sidePanelStore, + modalStore, } from "stores" import { API } from "api" import { ActionTypes } from "constants" @@ -436,6 +437,17 @@ const closeSidePanelHandler = () => { sidePanelStore.actions.close() } +const openModalHandler = action => { + const { id } = action.parameters + if (id) { + modalStore.actions.open(id) + } +} + +const closeModalHandler = () => { + modalStore.actions.close() +} + const downloadFileHandler = async action => { const { url, fileName } = action.parameters try { @@ -499,6 +511,8 @@ const handlerMap = { ["Prompt User"]: promptUserHandler, ["Open Side Panel"]: openSidePanelHandler, ["Close Side Panel"]: closeSidePanelHandler, + ["Open Modal"]: openModalHandler, + ["Close Modal"]: closeModalHandler, ["Download File"]: downloadFileHandler, } @@ -508,6 +522,7 @@ const confirmTextMap = { ["Execute Query"]: "Are you sure you want to execute this query?", ["Trigger Automation"]: "Are you sure you want to trigger this automation?", ["Prompt User"]: "Are you sure you want to continue?", + ["Duplicate Row"]: "Are you sure you want to duplicate this row?", } /** @@ -568,6 +583,11 @@ export const enrichButtonActions = (actions, context) => { const defaultTitleText = action["##eventHandlerType"] const customTitleText = action.parameters?.customTitleText || defaultTitleText + const cancelButtonText = + action.parameters?.cancelButtonText || "Cancel" + const confirmButtonText = + action.parameters?.confirmButtonText || "Confirm" + confirmationStore.actions.showConfirmation( customTitleText, confirmText, @@ -598,7 +618,9 @@ export const enrichButtonActions = (actions, context) => { }, () => { resolve(false) - } + }, + confirmButtonText, + cancelButtonText ) }) } diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 0d254186f2..6d1e1fa502 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -18,7 +18,7 @@ import FilterUsers from "./FilterUsers.svelte" import { getFields } from "../utils/searchFields" - const { OperatorOptions } = Constants + const { OperatorOptions, DEFAULT_BB_DATASOURCE_ID } = Constants export let schemaFields export let filters = [] @@ -28,6 +28,23 @@ export let allowBindings = false export let filtersLabel = "Filters" + $: { + if ( + tables.find( + table => + table._id === datasource.tableId && + table.sourceId === DEFAULT_BB_DATASOURCE_ID + ) && + !schemaFields.some(field => field.name === "_id") + ) { + schemaFields = [ + ...schemaFields, + { name: "_id", type: "string" }, + { name: "_rev", type: "string" }, + ] + } + } + $: matchAny = filters?.find(filter => filter.operator === "allOr") != null $: onEmptyFilter = filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" @@ -35,7 +52,6 @@ $: fieldFilters = filters.filter( filter => filter.operator !== "allOr" && !filter.onEmptyFilter ) - const behaviourOptions = [ { value: "and", label: "Match all filters" }, { value: "or", label: "Match any filter" }, @@ -44,7 +60,6 @@ { value: "all", label: "Return all table rows" }, { value: "none", label: "Return no rows" }, ] - const context = getContext("context") $: fieldOptions = getFields(tables, schemaFields || [], { diff --git a/packages/frontend-core/src/components/Updating.svelte b/packages/frontend-core/src/components/Updating.svelte index 7d4a101fee..7d14e57aba 100644 --- a/packages/frontend-core/src/components/Updating.svelte +++ b/packages/frontend-core/src/components/Updating.svelte @@ -1,18 +1,22 @@