diff --git a/.github/workflows/release-singleimage.yml b/.github/workflows/release-singleimage.yml index 92f21bd649..5b75c20d29 100644 --- a/.github/workflows/release-singleimage.yml +++ b/.github/workflows/release-singleimage.yml @@ -1,4 +1,4 @@ -name: Deploy Budibase Single Container Image to DockerHub +name: release-singleimage on: workflow_dispatch: @@ -8,8 +8,8 @@ env: PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} REGISTRY_URL: registry.hub.docker.com jobs: - build: - name: "build" + build-amd64: + name: "build-amd64" runs-on: ubuntu-latest strategy: matrix: @@ -27,14 +27,12 @@ jobs: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} fetch-depth: 0 - - name: Fail if tag is not in master run: | if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" exit 1 fi - - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: @@ -70,9 +68,139 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64 - tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }} + platforms: linux/amd64 + tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} file: ./hosting/single/Dockerfile + + - name: Tag and release Budibase Azure App Service docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64 + build-args: TARGETBUILD=aas + tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }} + file: ./hosting/single/Dockerfile + + build-arm64: + name: "build-arm64" + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - name: Fail if not a tag + run: | + if [[ $GITHUB_REF != refs/tags/* ]]; then + echo "Workflow Dispatch can only be run on tags" + exit 1 + fi + - name: "Checkout" + uses: actions/checkout@v2 + with: + submodules: true + token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + fetch-depth: 0 + - name: Fail if tag is not in master + run: | + if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then + echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" + exit 1 + fi + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Setup QEMU + uses: docker/setup-qemu-action@v1 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Run Yarn + run: yarn + - name: Update versions + run: ./scripts/updateVersions.sh + - name: Runt Yarn Lint + run: yarn lint + - name: Update versions + run: ./scripts/updateVersions.sh + - name: Run Yarn Build + run: yarn build:docker:pre + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_API_KEY }} + - name: Get the latest release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo $release_version + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + - name: Tag and release Budibase service docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/arm64 + tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} + file: ./hosting/single/Dockerfile + + build-aas: + name: "build-aas" + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - name: Fail if not a tag + run: | + if [[ $GITHUB_REF != refs/tags/* ]]; then + echo "Workflow Dispatch can only be run on tags" + exit 1 + fi + - name: "Checkout" + uses: actions/checkout@v2 + with: + submodules: true + token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + fetch-depth: 0 + - name: Fail if tag is not in master + run: | + if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then + echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" + exit 1 + fi + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Setup QEMU + uses: docker/setup-qemu-action@v1 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Run Yarn + run: yarn + - name: Update versions + run: ./scripts/updateVersions.sh + - name: Runt Yarn Lint + run: yarn lint + - name: Update versions + run: ./scripts/updateVersions.sh + - name: Run Yarn Build + run: yarn build:docker:pre + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_API_KEY }} + - name: Get the latest release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo $release_version + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - name: Tag and release Budibase Azure App Service docker image uses: docker/build-push-action@v2 with: diff --git a/lerna.json b/lerna.json index 17c08dcf7f..6e4e2b19ba 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.22-alpha.0", + "version": "2.8.27-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/nx.json b/nx.json index 3df61886c2..c2f44ef70d 100644 --- a/nx.json +++ b/nx.json @@ -3,11 +3,8 @@ "default": { "runner": "nx-cloud", "options": { - "cacheableOperations": [ - "build", - "test" - ], - "accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU=" + "cacheableOperations": ["build", "test"], + "accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ=" } } }, @@ -15,9 +12,7 @@ "dev:builder": { "dependsOn": [ { - "projects": [ - "@budibase/string-templates" - ], + "projects": ["@budibase/string-templates"], "target": "build" } ] diff --git a/package.json b/package.json index 3afe279e00..6a678f1bf3 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@nx/js": "16.4.3", "@rollup/plugin-json": "^4.0.2", "@typescript-eslint/parser": "5.45.0", - "esbuild": "^0.17.18", - "esbuild-node-externals": "^1.7.0", + "esbuild": "^0.18.17", + "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", "eslint-plugin-cypress": "^2.11.3", "husky": "^8.0.3", @@ -51,9 +51,9 @@ "kill-builder": "kill-port 3000", "kill-server": "kill-port 4001 4002", "kill-all": "yarn run kill-builder && yarn run kill-server", - "dev": "yarn run kill-all && lerna run --stream dev:builder --stream", - "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", - "dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server", + "dev": "yarn run kill-all && yarn nx run-many --target=dev:builder", + "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && yarn nx run-many --target=dev:builder --exclude=@budibase/backend-core,@budibase/server,@budibase/worker", + "dev:server": "yarn run kill-server && yarn nx run-many --target=dev:builder --projects=@budibase/worker,@budibase/server", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", "test": "lerna run --stream test --stream", diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index 5b66c356d3..0c320ec776 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init" import { doWithDB, DocumentType } from "../db" import { Database, App } from "@budibase/types" -const AppState = { - INVALID: "invalid", +export enum AppState { + INVALID = "invalid", } + +export interface DeletedApp { + state: AppState +} + const EXPIRY_SECONDS = 3600 /** @@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) { * @param {string} appId the id of the app to get metadata from. * @returns {object} the app metadata. */ -export async function getAppMetadata(appId: string) { +export async function getAppMetadata(appId: string): Promise { const client = await getAppClient() // try cache let metadata = await client.get(appId) @@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) { } await client.store(appId, metadata, expiry) } - // we've stored in the cache an object to tell us that it is currently invalid - if (isInvalid(metadata)) { - throw { status: 404, message: "No app metadata found" } - } - return metadata as App + + return metadata } /** diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..aea485e3e3 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,10 @@ +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 diff --git a/packages/backend-core/src/db/couch/index.ts b/packages/backend-core/src/db/couch/index.ts index c731d20d6c..932efed3f7 100644 --- a/packages/backend-core/src/db/couch/index.ts +++ b/packages/backend-core/src/db/couch/index.ts @@ -2,3 +2,4 @@ export * from "./connections" export * from "./DatabaseImpl" export * from "./utils" export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB" +export * from "../constants" diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 6034296996..4ebf8392b5 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -2,7 +2,7 @@ import env from "../environment" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" -import { getAppMetadata } from "../cache/appMetadata" +import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { App, Database } from "@budibase/types" import { getStartEndKeyURL } from "../docIds" @@ -101,7 +101,9 @@ export async function getAllApps({ const response = await Promise.allSettled(appPromises) const apps = response .filter( - (result: any) => result.status === "fulfilled" && result.value != null + (result: any) => + result.status === "fulfilled" && + result.value?.state !== AppState.INVALID ) .map(({ value }: any) => value) if (!all) { @@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) { ) // have to list the apps which exist, some may have been deleted return settled - .filter(promise => promise.status === "fulfilled") + .filter( + promise => + promise.status === "fulfilled" && + (promise.value as DeletedApp).state !== AppState.INVALID + ) .map(promise => (promise as PromiseFulfilledResult).value) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index f29720e7f7..8694e44f16 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -164,6 +164,7 @@ const environment = { : false, ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, + OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 4e1f1abbb5..7d55d25e89 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError { } } +export class NotFoundError extends HTTPError { + constructor(message: string) { + super(message, 404) + } +} + +export class BadRequestError extends HTTPError { + constructor(message: string) { + super(message, 400) + } +} + // LICENSING export class UsageLimitError extends HTTPError { diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..948d3b692b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise => { } } -const getUniqueTenantId = async (tenantId: string): Promise => { +export const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { diff --git a/packages/backend-core/tests/core/utilities/jestUtils.ts b/packages/backend-core/tests/core/utilities/jestUtils.ts index d84eac548c..4a3da8db8c 100644 --- a/packages/backend-core/tests/core/utilities/jestUtils.ts +++ b/packages/backend-core/tests/core/utilities/jestUtils.ts @@ -1,3 +1,5 @@ +import { db } from "../../../src" + export function expectFunctionWasCalledTimesWith( jestFunction: any, times: number, @@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith( jestFunction.mock.calls.filter((call: any) => call[0] === argument).length ).toBe(times) } + +export const expectAnyInternalColsAttributes: { + [K in (typeof db.CONSTANT_INTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + type: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), + createdAt: expect.anything(), + updatedAt: expect.anything(), +} + +export const expectAnyExternalColsAttributes: { + [K in (typeof db.CONSTANT_EXTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), +} diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 807153cd09..8476399aa3 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -13,7 +13,7 @@ import { } from "@budibase/types" import _ from "lodash" -export const account = (): Account => { +export const account = (partial: Partial = {}): Account => { return { accountId: uuid(), tenantId: generator.word(), @@ -29,6 +29,7 @@ export const account = (): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), + ...partial, } } diff --git a/packages/backend-core/tests/core/utilities/structures/db.ts b/packages/backend-core/tests/core/utilities/structures/db.ts index 31a52dce8b..87325573eb 100644 --- a/packages/backend-core/tests/core/utilities/structures/db.ts +++ b/packages/backend-core/tests/core/utilities/structures/db.ts @@ -1,4 +1,4 @@ -import { structures } from ".." +import { generator } from "./generator" import { newid } from "../../../../src/docIds/newid" export function id() { @@ -6,7 +6,7 @@ export function id() { } export function rev() { - return `${structures.generator.character({ + return `${generator.character({ numeric: true, - })}-${structures.uuid().replace(/-/, "")}` + })}-${generator.guid().replace(/-/, "")}` } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts new file mode 100644 index 0000000000..c3bfba3597 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -0,0 +1 @@ +export * from "./platform" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts new file mode 100644 index 0000000000..98a6314999 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -0,0 +1 @@ +export * as installation from "./installation" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts new file mode 100644 index 0000000000..711c6cf14f --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -0,0 +1,12 @@ +import { generator } from "../../generator" +import { Installation } from "@budibase/types" +import * as db from "../../db" + +export function install(): Installation { + return { + _id: "install", + _rev: db.rev(), + installId: generator.guid(), + version: generator.string(), + } +} diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index 2c094f43a7..1a49e912fc 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,6 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" +export * as docs from "./documents" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 22e73f2871..5cce84edfd 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -3,6 +3,8 @@ import { Customer, Feature, License, + OfflineIdentifier, + OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -11,6 +13,7 @@ import { Quotas, Subscription, } from "@budibase/types" +import { generator } from "./generator" export function price(): PurchasedPrice { return { @@ -127,15 +130,15 @@ export function subscription(): Subscription { } } -export const license = ( - opts: { - quotas?: Quotas - plan?: PurchasedPlan - planType?: PlanType - features?: Feature[] - billing?: Billing - } = {} -): License => { +interface GenerateLicenseOpts { + quotas?: Quotas + plan?: PurchasedPlan + planType?: PlanType + features?: Feature[] + billing?: Billing +} + +export const license = (opts: GenerateLicenseOpts = {}): License => { return { features: opts.features || [], quotas: opts.quotas || quotas(), @@ -143,3 +146,22 @@ export const license = ( billing: opts.billing || billing(), } } + +export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense { + const base = license(opts) + return { + ...base, + expireAt: new Date().toISOString(), + identifier: offlineIdentifier(), + } +} + +export function offlineIdentifier( + installId: string = generator.guid(), + tenantId: string = generator.guid() +): OfflineIdentifier { + return { + installId, + tenantId, + } +} diff --git a/packages/bbui/package.json b/packages/bbui/package.json index b03c83d71b..4d39f6330b 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -96,7 +96,8 @@ "dependsOn": [ { "projects": [ - "@budibase/string-templates" + "@budibase/string-templates", + "@budibase/shared-core" ], "target": "build" } diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 01555446d9..8fa02bb8f3 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -64,7 +64,7 @@ export default function positionDropdown(element, opts) { // Apply styles Object.entries(styles).forEach(([style, value]) => { - if (value) { + if (value != null) { element.style[style] = `${value.toFixed(0)}px` } else { element.style[style] = null diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index e9c8643bce..bbe116721a 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => { readableBinding: `${table._instanceName}.Selected rows`, category: "Selected rows", icon: "ViewRow", + display: { name: table._instanceName }, })) ) @@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => { )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, category: "Selected rows", + display: { name: block._instanceName }, })) ) } diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 33db9b60e3..d4c994dae5 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -1,5 +1,5 @@ @@ -231,7 +236,9 @@
    {#each category as actionType}
  • - {actionType.name} + + {actionType.displayName || actionType.name} +
  • {/each}
@@ -262,7 +269,7 @@ >
- {index + 1}. {action[EVENT_TYPE_KEY]} + {index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
- import { Select, Label, Checkbox, Input } from "@budibase/bbui" + import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui" import { tables } from "stores/backend" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" @@ -10,47 +10,59 @@
- - Please specify one or more rows to delete. +
+ + + {/if} +
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte index ba72fd2ed7..a085d86507 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte @@ -73,9 +73,12 @@ {#if query?.parameters?.length > 0}
{ + parameters.queryParams = { ...v.detail } + }} /> { - tmpQueryParams = value.queryParams + tmpQueryParams = { ...value.queryParams } drawer.show() } const getQueryValue = queries => { - value = queries.find(q => q._id === value._id) || value - return value + return queries.find(q => q._id === value._id) || value } const saveQueryParams = () => { @@ -176,7 +175,10 @@ {#if getQueryParams(value).length > 0} { + tmpQueryParams = { ...v.detail } + }} queryBindings={getQueryParams(value)} bind:bindings /> diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte index 88c3842f54..828d189850 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte @@ -13,13 +13,14 @@ export let value = [] export let componentInstance export let bindings = [] + export let schema = null let drawer $: tempValue = value $: datasource = getDatasourceForProvider($currentAsset, componentInstance) - $: schema = getSchemaForDatasource($currentAsset, datasource)?.schema - $: schemaFields = Object.values(schema || {}) + $: dsSchema = getSchemaForDatasource($currentAsset, datasource)?.schema + $: schemaFields = Object.values(schema || dsSchema || {}) $: text = getText(value?.filter(filter => filter.field)) async function saveFilter() { diff --git a/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte b/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte new file mode 100644 index 0000000000..0010a22d15 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte @@ -0,0 +1,35 @@ + + + diff --git a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte index 6c62c9f5af..6db24e8d69 100644 --- a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte @@ -8,16 +8,29 @@ export let componentDefinition export let type + const dispatch = createEventDispatcher() let drawer - const dispatch = createEventDispatcher() + $: text = getText(value) + const save = () => { dispatch("change", value) drawer.hide() } + + const getText = rules => { + if (!rules?.length) { + return "No rules set" + } else { + return `${rules.length} rule${rules.length === 1 ? "" : "s"} set` + } + } -Configure validation +
+ {text} +
+ Configure validation rules for this field. @@ -31,3 +44,9 @@ {componentDefinition} /> + + diff --git a/packages/builder/src/components/integration/QueryBindingBuilder.svelte b/packages/builder/src/components/integration/QueryBindingBuilder.svelte index 3a89c4b968..af890302b0 100644 --- a/packages/builder/src/components/integration/QueryBindingBuilder.svelte +++ b/packages/builder/src/components/integration/QueryBindingBuilder.svelte @@ -5,6 +5,9 @@ runtimeToReadableBinding, } from "builderStore/dataBinding" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" + import { createEventDispatcher } from "svelte" + + const dispatch = createEventDispatcher() export let bindable = true export let queryBindings = [] @@ -20,7 +23,10 @@ // The readable binding in the UI gets converted to a UUID value that the client understands // for parsing, then converted back so we can display it the readable form in the UI function onBindingChange(param, valueToParse) { - customParams[param] = readableToRuntimeBinding(bindings, valueToParse) + dispatch("change", { + ...customParams, + [param]: readableToRuntimeBinding(bindings, valueToParse), + }) } diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 3377ff3a88..4683bc6335 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -14,8 +14,9 @@ Tab, Modal, ModalContent, + notifications, + Divider, } from "@budibase/bbui" - import { notifications, Divider } from "@budibase/bbui" import ExtraQueryConfig from "./ExtraQueryConfig.svelte" import IntegrationQueryEditor from "components/integration/index.svelte" import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" @@ -28,6 +29,7 @@ import KeyValueBuilder from "./KeyValueBuilder.svelte" import { fieldsToSchema, schemaToFields } from "helpers/data/utils" import AccessLevelSelect from "./AccessLevelSelect.svelte" + import { ValidQueryNameRegex } from "@budibase/shared-core" export let query @@ -47,6 +49,7 @@ let saveModal let override = false let navigateTo = null + let nameError = null // seed the transformer if (query && !query.transformer) { @@ -77,7 +80,7 @@ $: queryConfig = integrationInfo?.query $: shouldShowQueryConfig = queryConfig && query.queryVerb $: readQuery = query.queryVerb === "read" || query.readable - $: queryInvalid = !query.name || (readQuery && data.length === 0) + $: queryInvalid = !query.name || nameError || (readQuery && data.length === 0) //Cast field in query preview response to number if specified by schema $: { @@ -139,9 +142,10 @@ queryStr = JSON.stringify(query) } + notifications.success("Query saved successfully") return response } catch (error) { - notifications.error("Error saving query") + notifications.error(error.message || "Error saving query") } } @@ -183,8 +187,14 @@ value={query.name} on:input={e => { let newValue = e.target.value || "" - query.name = newValue.trim() + if (newValue.match(ValidQueryNameRegex)) { + query.name = newValue.trim() + nameError = null + } else { + nameError = "Invalid query name" + } }} + error={nameError} />
{#if queryConfig} @@ -250,9 +260,9 @@ size="L" /> - Add a JavaScript function to transform the query result. + + Add a JavaScript function to transform the query result. +
Results - + - {#if licenseInfo?.licenseKey} - - {/if} - - + {#if licenseKey} + + {/if} + + + {/if} - Plan - + Plan + You are currently on the {license.plan.type} plan +
+ If you purchase or update your plan on the account + portal, click the refresh button to sync those changes +
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { time: @@ -169,4 +293,7 @@ grid-gap: var(--spacing-l); align-items: center; } + .identifier-input { + width: 300px; + } diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index b9467fd037..2106acac27 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = { adminUser: { checked: false }, sso: { checked: false }, }, + offlineMode: false, } export function createAdminStore() { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 285f045d08..1e4c443f06 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3485,6 +3485,16 @@ } ] }, + { + "type": "validation/link", + "label": "Validation", + "key": "validation" + }, + { + "type": "filter/relationship", + "label": "Filtering", + "key": "filter" + }, { "type": "boolean", "label": "Autocomplete", @@ -3496,11 +3506,6 @@ "label": "Disabled", "key": "disabled", "defaultValue": false - }, - { - "type": "validation/link", - "label": "Validation", - "key": "validation" } ] }, diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index 735b44b9ae..0c8b076a67 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -1,5 +1,6 @@