diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 675707453e..1ac6b20003 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -194,5 +194,5 @@ jobs: PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: repository: budibase/budibase-deploys - event: deploy-develop-to-qa + event: deploy-budibase-develop-to-qa github_pat: ${{ secrets.GH_ACCESS_TOKEN }} \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..e0bcfe01fb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v14.19.3 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..371cfe355d --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.1 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..8a1af3c071 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +nodejs 14.19.3 +python 3.11.1 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..03d0aa4411 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "svelte.svelte-vscode" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 71f0092a59..ece537efac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,28 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": true - }, - "editor.defaultFormatter": "svelte.svelte-vscode", - "[json]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "debug.javascript.terminalOptions": { - "skipFiles": [ - "${workspaceFolder}/packages/backend-core/node_modules/**", - "/**" - ] - }, - "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[dockercompose]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode" + } } diff --git a/charts/budibase/templates/alb-ingress.yaml b/charts/budibase/templates/alb-ingress.yaml index 388bcf1d3e..c128b70843 100644 --- a/charts/budibase/templates/alb-ingress.yaml +++ b/charts/budibase/templates/alb-ingress.yaml @@ -14,6 +14,9 @@ metadata: alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certificateArn }} {{- end }} + {{- if .Values.ingress.securityGroups }} + alb.ingress.kubernetes.io/security-groups: {{ .Values.ingress.securityGroups }} + {{- end }} spec: rules: - http: diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 889d7e9e23..dd75b2daa3 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -76,7 +76,7 @@ affinity: {} globals: appVersion: "latest" budibaseEnv: PRODUCTION - tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS" + tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR" enableAnalytics: "1" sentryDSN: "" posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index fb0848596c..6e667d23a8 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -9,7 +9,6 @@ From opening a bug report to creating a pull request: every contribution is appr - [Glossary of Terms](#glossary-of-terms) - [Contributing to Budibase](#contributing-to-budibase) - ## Not Sure Where to Start? Budibase is a low-code web application builder that creates svelte-based web applications. @@ -22,7 +21,7 @@ Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna - **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. -- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. +- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. ## Contributor License Agreement (CLA) @@ -45,7 +44,7 @@ A client represents a single budibase customer. Each budibase client will have 1 ### App -A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. +A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. ### Database @@ -73,28 +72,55 @@ A component is the basic frontend building block of a budibase app. ### Component Library -Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. +Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. ## Contributing to Budibase -* Please maintain the existing code style. +- Please maintain the existing code style. -* Please try to keep your commits small and focused. +- Please try to keep your commits small and focused. -* Please write tests. +- Please write tests. -* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. +- If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. -* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. +- Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. ### Getting Started For Contributors -#### 1. Prerequisites -NodeJS Version `14.x.x` +#### 1. Prerequisites -*yarn -* `npm install -g yarn` +- NodeJS version `14.x.x` +- Python version `3.x` -*jest* - `npm install -g jest` +### Using asdf (recommended) + +Asdf is a package manager that allows managing multiple dependencies. + +You can install them following any of the steps described below: + +- Install using script (only for mac users): + +`./scripts/install-contributor-dependencies.sh` + +- Or, manually: + + - Installation steps: https://asdf-vm.com/guide/getting-started.html + - asdf plugin add nodejs + - asdf plugin add python + - npm install -g yarn + +### Using NVM and pyenv + +- NVM: + - Install: https://github.com/nvm-sh/nvm#installing-and-updating + - Setup: `nvm use` +- Pyenv: + + - Install: https://github.com/pyenv/pyenv#installation + - Setup: `pyenv install -v 3.7.2` + +- _yarn -_ `npm install -g yarn` #### 2. Clone this repository @@ -102,7 +128,7 @@ NodeJS Version `14.x.x` then `cd ` into your local copy. -#### 3. Install and Build +#### 3. Install and Build | **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash) @@ -134,9 +160,9 @@ This will enable watch mode for both the builder app, server, client library and #### 5. Debugging using VS Code -To debug the budibase server and worker a VS Code launch configuration has been provided. +To debug the budibase server and worker a VS Code launch configuration has been provided. -Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. +Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. Alternatively to start both components simultaneously select `Start Budibase`. In addition to the above, the remaining budibase components may be run in dev mode using: `yarn dev:noserver`. @@ -156,11 +182,11 @@ For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apa When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are: -- `redis_data` +- `redis_data` - Sessions, email tokens -- `couchdb3_data` +- `couchdb3_data` - Global and app databases -- `minio_data` +- `minio_data` - App manifest, budibase client, static assets ### Development Modes @@ -172,34 +198,42 @@ A combination of environment variables controls the mode budibase runs in. Yarn commands can be used to mimic the different modes as described in the sections below: #### Self Hosted -The default mode. A single tenant installation with no usage restrictions. + +The default mode. A single tenant installation with no usage restrictions. To enable this mode, use: + ``` yarn mode:self ``` #### Cloud -The cloud mode, with account portal turned off. + +The cloud mode, with account portal turned off. To enable this mode, use: + ``` yarn mode:cloud ``` -#### Cloud & Account -The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app +#### Cloud & Account + +The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app To enable this mode, use: + ``` yarn mode:account ``` + ### CI - An overview of the CI pipelines can be found [here](../.github/workflows/README.md) + +An overview of the CI pipelines can be found [here](../.github/workflows/README.md) ### Pro -@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. +@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. ``` . @@ -207,13 +241,14 @@ yarn mode:account |_ budibase-pro ``` -Note that only budibase maintainers will be able to access the pro repo. +Note that only budibase maintainers will be able to access the pro repo. -The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. +The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. ### Troubleshooting Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. + ### Running tests #### End-to-end Tests @@ -226,12 +261,11 @@ yarn test:e2e Or if you are in the builder you can run `yarn cy:test`. - ### Other Useful Information -* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). +- The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). -* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). +- This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). -* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. +- We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. Please read this if you are unfamiliar with it. diff --git a/hosting/dependencies/runner.sh b/hosting/dependencies/runner.sh index ddef044b3c..d7aef15432 100644 --- a/hosting/dependencies/runner.sh +++ b/hosting/dependencies/runner.sh @@ -4,5 +4,5 @@ redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & /bbcouch-runner.sh & /minio/minio server ${DATA_DIR}/minio --console-address ":9001" > /dev/stdout 2>&1 & -echo "Test environment started..." +echo "Budibase dependencies started..." sleep infinity \ No newline at end of file diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index bfeabe4cf3..a9eedac375 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -53,20 +53,6 @@ services: volumes: - couchdb_data:/data - couch-init: - container_name: budi-couchdb-init-dev - image: curlimages/curl - environment: - PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984" - depends_on: - - couchdb-service - command: - [ - "sh", - "-c", - "sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;", - ] - redis-service: container_name: budi-redis-dev restart: on-failure diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index c52f699077..0bd377cd7f 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -10,7 +10,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME [[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000 [[ -z "${NODE_ENV}" ]] && export NODE_ENV=production [[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU -[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS" +[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR" [[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app [[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379 [[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1 diff --git a/lerna.json b/lerna.json index 5c8d4c87a6..5d054e467b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.2.12-alpha.59", + "version": "2.2.27-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts index 2f34b55aa8..0483fb073a 100644 --- a/packages/backend-core/jest.config.ts +++ b/packages/backend-core/jest.config.ts @@ -1,7 +1,7 @@ -import { Config } from "jest" +import { Config } from "@jest/types" const preset = require("ts-jest/jest-preset") -const testContainersSettings = { +const baseConfig: Config.InitialProjectOptions = { ...preset, preset: "@trendyol/jest-testcontainers", setupFiles: ["./tests/jestEnv.ts"], @@ -13,23 +13,23 @@ const testContainersSettings = { if (!process.env.CI) { // use sources when not in CI - testContainersSettings.moduleNameMapper = { + baseConfig.moduleNameMapper = { "@budibase/types": "/../types/src", } } else { console.log("Running tests with compiled dependency sources") } -const config: Config = { +const config: Config.InitialOptions = { projects: [ { - ...testContainersSettings, + ...baseConfig, displayName: "sequential test", testMatch: ["/**/*.seq.spec.[jt]s"], runner: "jest-serial-runner", }, { - ...testContainersSettings, + ...baseConfig, testMatch: ["/**/!(*.seq).spec.[jt]s"], }, ], diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index fa9cccfdd1..2216eae32d 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.2.12-alpha.59", + "version": "2.2.27-alpha.0", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -23,7 +23,7 @@ }, "dependencies": { "@budibase/nano": "10.1.1", - "@budibase/types": "2.2.12-alpha.59", + "@budibase/types": "2.2.27-alpha.0", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js index 058d4321d4..fefca30c18 100644 --- a/packages/backend-core/src/cache/tests/writethrough.spec.js +++ b/packages/backend-core/src/cache/tests/writethrough.spec.js @@ -1,17 +1,17 @@ -const{generator}=require("../../../tests") +require("../../../tests") const { Writethrough } = require("../writethrough") const { getDB } = require("../../db") const tk = require("timekeeper") +const { structures } = require("../../../tests") const START_DATE = Date.now() tk.freeze(START_DATE) -const { newid } = require("../../newid") const DELAY = 5000 -const db = getDB(`db_${newid()}`) -const db2 = getDB(`db_${newid()}`) +const db = getDB(structures.db.id()) +const db2 = getDB(structures.db.id()) const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) describe("writethrough", () => { diff --git a/packages/backend-core/src/db/tests/index.spec.js b/packages/backend-core/src/db/tests/index.spec.js index 56041ea732..0d257f7ed7 100644 --- a/packages/backend-core/src/db/tests/index.spec.js +++ b/packages/backend-core/src/db/tests/index.spec.js @@ -1,19 +1,19 @@ require("../../../tests") -const { newid } = require("../../newid") +const { structures } = require("../../../tests") const { getDB } = require("../db") describe("db", () => { describe("getDB", () => { it("returns a db", async () => { - const dbName = `db_${newid()}` + const dbName = structures.db.id() const db = getDB(dbName) expect(db).toBeDefined() expect(db.name).toBe(dbName) }) it("uses the custom put function", async () => { - const db = getDB(`db_${newid()}`) + const db = getDB(structures.db.id()) let doc = { _id: "test" } await db.put(doc) doc = await db.get(doc._id) diff --git a/packages/backend-core/src/featureFlags/index.ts b/packages/backend-core/src/featureFlags/index.ts index 71e226c976..34ee3599a5 100644 --- a/packages/backend-core/src/featureFlags/index.ts +++ b/packages/backend-core/src/featureFlags/index.ts @@ -6,7 +6,7 @@ import * as tenancy from "../tenancy" * The env var is formatted as: * tenant1:feature1:feature2,tenant2:feature1 */ -function getFeatureFlags() { +export function buildFeatureFlags() { if (!env.TENANT_FEATURE_FLAGS) { return } @@ -27,8 +27,6 @@ function getFeatureFlags() { return tenantFeatureFlags } -const TENANT_FEATURE_FLAGS = getFeatureFlags() - export function isEnabled(featureFlag: string) { const tenantId = tenancy.getTenantId() const flags = getTenantFeatureFlags(tenantId) @@ -36,18 +34,36 @@ export function isEnabled(featureFlag: string) { } export function getTenantFeatureFlags(tenantId: string) { - const flags = [] + let flags: string[] = [] + const envFlags = buildFeatureFlags() + if (envFlags) { + const globalFlags = envFlags["*"] + const tenantFlags = envFlags[tenantId] || [] - if (TENANT_FEATURE_FLAGS) { - const globalFlags = TENANT_FEATURE_FLAGS["*"] - const tenantFlags = TENANT_FEATURE_FLAGS[tenantId] + // Explicitly exclude tenants from global features if required. + // Prefix the tenant flag with '!' + const tenantOverrides = tenantFlags.reduce( + (acc: string[], flag: string) => { + if (flag.startsWith("!")) { + let stripped = flag.substring(1) + acc.push(stripped) + } + return acc + }, + [] + ) if (globalFlags) { flags.push(...globalFlags) } - if (tenantFlags) { + if (tenantFlags.length) { flags.push(...tenantFlags) } + + // Purge any tenant specific overrides + flags = flags.filter(flag => { + return tenantOverrides.indexOf(flag) == -1 && !flag.startsWith("!") + }) } return flags @@ -57,4 +73,5 @@ export enum TenantFeatureFlag { LICENSING = "LICENSING", GOOGLE_SHEETS = "GOOGLE_SHEETS", USER_GROUPS = "USER_GROUPS", + ONBOARDING_TOUR = "ONBOARDING_TOUR", } diff --git a/packages/backend-core/src/featureFlags/tests/featureFlags.spec.ts b/packages/backend-core/src/featureFlags/tests/featureFlags.spec.ts new file mode 100644 index 0000000000..1b68959329 --- /dev/null +++ b/packages/backend-core/src/featureFlags/tests/featureFlags.spec.ts @@ -0,0 +1,85 @@ +import { + TenantFeatureFlag, + buildFeatureFlags, + getTenantFeatureFlags, +} from "../" +import env from "../../environment" + +const { ONBOARDING_TOUR, LICENSING, USER_GROUPS } = TenantFeatureFlag + +describe("featureFlags", () => { + beforeEach(() => { + env._set("TENANT_FEATURE_FLAGS", "") + }) + + it("Should return no flags when the TENANT_FEATURE_FLAG is empty", async () => { + let features = buildFeatureFlags() + expect(features).toBeUndefined() + }) + + it("Should generate a map of global and named tenant feature flags from the env value", async () => { + env._set( + "TENANT_FEATURE_FLAGS", + `*:${ONBOARDING_TOUR},tenant1:!${ONBOARDING_TOUR},tenant2:${USER_GROUPS},tenant1:${LICENSING}` + ) + + const parsedFlags: Record = { + "*": [ONBOARDING_TOUR], + tenant1: [`!${ONBOARDING_TOUR}`, LICENSING], + tenant2: [USER_GROUPS], + } + + let features = buildFeatureFlags() + + expect(features).toBeDefined() + expect(features).toEqual(parsedFlags) + }) + + it("Should add feature flag flag only to explicitly configured tenant", async () => { + env._set( + "TENANT_FEATURE_FLAGS", + `*:${LICENSING},*:${USER_GROUPS},tenant1:${ONBOARDING_TOUR}` + ) + + let tenant1Flags = getTenantFeatureFlags("tenant1") + let tenant2Flags = getTenantFeatureFlags("tenant2") + + expect(tenant1Flags).toBeDefined() + expect(tenant1Flags).toEqual([LICENSING, USER_GROUPS, ONBOARDING_TOUR]) + + expect(tenant2Flags).toBeDefined() + expect(tenant2Flags).toEqual([LICENSING, USER_GROUPS]) + }) +}) + +it("Should exclude tenant1 from global feature flag", async () => { + env._set( + "TENANT_FEATURE_FLAGS", + `*:${LICENSING},*:${ONBOARDING_TOUR},tenant1:!${ONBOARDING_TOUR}` + ) + + let tenant1Flags = getTenantFeatureFlags("tenant1") + let tenant2Flags = getTenantFeatureFlags("tenant2") + + expect(tenant1Flags).toBeDefined() + expect(tenant1Flags).toEqual([LICENSING]) + + expect(tenant2Flags).toBeDefined() + expect(tenant2Flags).toEqual([LICENSING, ONBOARDING_TOUR]) +}) + +it("Should explicitly add flags to configured tenants only", async () => { + env._set( + "TENANT_FEATURE_FLAGS", + `tenant1:${ONBOARDING_TOUR},tenant1:${LICENSING},tenant2:${LICENSING}` + ) + + let tenant1Flags = getTenantFeatureFlags("tenant1") + let tenant2Flags = getTenantFeatureFlags("tenant2") + + expect(tenant1Flags).toBeDefined() + expect(tenant1Flags).toEqual([ONBOARDING_TOUR, LICENSING]) + + expect(tenant2Flags).toBeDefined() + expect(tenant2Flags).toEqual([LICENSING]) +}) diff --git a/packages/backend-core/src/middleware/logging.ts b/packages/backend-core/src/middleware/logging.ts index 35a9b6c4a6..db9b64b883 100644 --- a/packages/backend-core/src/middleware/logging.ts +++ b/packages/backend-core/src/middleware/logging.ts @@ -64,7 +64,9 @@ const print = (fn: any, data: any[]) => { message = message + ` [identityId=${identityId}]` } - // fn(message, data) + if (!process.env.CI) { + fn(message, data) + } } const logging = (ctx: any, next: any) => { diff --git a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js b/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js index 474d234ee1..d377d602f1 100644 --- a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js @@ -6,8 +6,6 @@ const { DEFAULT_TENANT_ID } = require("../../../constants") const { generateGlobalUserID } = require("../../../db/utils") const { newid } = require("../../../utils") const { doWithGlobalDB, doInTenant } = require("../../../tenancy") -const { default: environment } = require("../../../environment") -environment._set("MULTI_TENANCY", 'TRUE') const done = jest.fn() diff --git a/packages/backend-core/src/migrations/tests/index.spec.js b/packages/backend-core/src/migrations/tests/index.spec.js index 3efb10fbf3..c1915510c3 100644 --- a/packages/backend-core/src/migrations/tests/index.spec.js +++ b/packages/backend-core/src/migrations/tests/index.spec.js @@ -2,9 +2,8 @@ require("../../../tests") const { runMigrations, getMigrationsDoc } = require("../index") const { getGlobalDBName, getDB } = require("../../db") -const { default: environment } = require("../../environment") -const { newid } = require("../../newid") -environment._set("MULTI_TENANCY", 'TRUE') +const { structures, testEnv } = require("../../../tests") +testEnv.multiTenant() let db @@ -21,7 +20,7 @@ describe("migrations", () => { let tenantId beforeEach(() => { - tenantId = `tenant_${newid()}` + tenantId = structures.tenant.id() db = getDB(getGlobalDBName(tenantId)) }) diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index 2d3d40f9d5..b3cd527fb3 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -4,17 +4,12 @@ import * as events from "../../events" import * as db from "../../db" import { Header } from "../../constants" import { doInTenant } from "../../context" -import environment from "../../environment" import { newid } from "../../utils" describe("utils", () => { describe("platformLogout", () => { - beforeEach(() => { - environment._set("MULTI_TENANCY", "TRUE") - }) - it("should call platform logout", async () => { - await doInTenant(`tenant-${newid()}`, async () => { + await doInTenant(structures.tenant.id(), async () => { const ctx = structures.koa.newContext() await utils.platformLogout({ ctx, userId: "test" }) expect(events.auth.logout).toBeCalledTimes(1) @@ -23,10 +18,6 @@ describe("utils", () => { }) describe("getAppIdFromCtx", () => { - beforeEach(() => { - environment._set("MULTI_TENANCY", undefined) - }) - it("gets appId from header", async () => { const ctx = structures.koa.newContext() const expected = db.generateAppID() diff --git a/packages/backend-core/tests/utilities/structures/db.ts b/packages/backend-core/tests/utilities/structures/db.ts new file mode 100644 index 0000000000..e25b707cb9 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/db.ts @@ -0,0 +1,5 @@ +import { newid } from "../../../src/newid" + +export function id() { + return `db_${newid()}` +} diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index 0d0f0c507f..e74751e479 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -8,3 +8,5 @@ export * as apps from "./apps" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" +export * as tenant from "./tenants" +export * as db from "./db" diff --git a/packages/backend-core/tests/utilities/structures/tenants.ts b/packages/backend-core/tests/utilities/structures/tenants.ts new file mode 100644 index 0000000000..b23bc8be75 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/tenants.ts @@ -0,0 +1,5 @@ +import { newid } from "../../../src/newid" + +export function id() { + return `tenant-${newid()}` +} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 4e3b774337..fbfc52c6e8 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -487,11 +487,6 @@ qs "^6.11.0" tough-cookie "^4.1.2" -"@budibase/types@2.2.12-alpha.59": - version "2.2.12-alpha.59" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.2.12-alpha.59.tgz#41635c1e405acfa6162b5ca0f79f0c73f16bc764" - integrity sha512-cEcM0nnTEOEan9UYVspwcdgYgIbtY2zQTe1uDdwys+NFplMrbiwGyQbsafOx2IA0jCxmyqqYGmUAC0eF1napKQ== - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -3126,9 +3121,9 @@ http-assert@^1.3.0: http-errors "~1.8.0" http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-cookie-agent@^4.0.2: version "4.0.2" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index a7b38cdd12..2411b254cb 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": "2.2.12-alpha.59", + "version": "2.2.27-alpha.0", "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": "2.2.12-alpha.59", + "@budibase/string-templates": "2.2.27-alpha.0", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index a8dc989313..663128160f 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -86,7 +86,7 @@ } .is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) { background: var(--spectrum-global-color-gray-300); - border-color: var(--spectrum-global-color-gray-700); + border-color: var(--spectrum-global-color-gray-500); } .noPadding { padding: 0; diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index d99601afeb..abc7188985 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -1,11 +1,21 @@ -export default function positionDropdown( - element, - { anchor, align, maxWidth, useAnchorWidth, offset = 5 } -) { - const update = () => { +export default function positionDropdown(element, opts) { + let resizeObserver + let latestOpts = opts + + // We need a static reference to this function so that we can properly + // clean up the scroll listener. + const scrollUpdate = () => { + updatePosition(latestOpts) + } + + // Updates the position of the dropdown + const updatePosition = opts => { + const { anchor, align, maxWidth, useAnchorWidth, offset = 5 } = opts if (!anchor) { return } + + // Compute bounds const anchorBounds = anchor.getBoundingClientRect() const elementBounds = element.getBoundingClientRect() let styles = { @@ -51,26 +61,47 @@ export default function positionDropdown( }) } + // The actual svelte action callback which creates observers on the relevant + // DOM elements + const update = newOpts => { + latestOpts = newOpts + + // Cleanup old state + if (resizeObserver) { + resizeObserver.disconnect() + } + + // Do nothing if no anchor + const { anchor } = newOpts + if (!anchor) { + return + } + + // Observe both anchor and element and resize the popover as appropriate + resizeObserver = new ResizeObserver(() => updatePosition(newOpts)) + resizeObserver.observe(anchor) + resizeObserver.observe(element) + resizeObserver.observe(document.body) + } + // Apply initial styles which don't need to change element.style.position = "absolute" element.style.zIndex = "9999" - // Observe both anchor and element and resize the popover as appropriate - const resizeObserver = new ResizeObserver(entries => { - entries.forEach(update) - }) - if (anchor) { - resizeObserver.observe(anchor) - } - resizeObserver.observe(element) - resizeObserver.observe(document.body) + // Set up a scroll listener + document.addEventListener("scroll", scrollUpdate, true) - document.addEventListener("scroll", update, true) + // Perform initial update + update(opts) return { + update, destroy() { - resizeObserver.disconnect() - document.removeEventListener("scroll", update, true) + // Cleanup + if (resizeObserver) { + resizeObserver.disconnect() + } + document.removeEventListener("scroll", scrollUpdate, true) }, } } diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 10aae67ec6..2c89a538a3 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -76,13 +76,6 @@ } // If time only set date component to 2000-01-01 if (timeOnly) { - // Classic flackpickr causing issues. - // When selecting a value for the first time for a "time only" field, - // the time is always offset by 1 hour for some reason (regardless of time - // zone) so we need to correct it. - if (!value && newValue) { - newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString() - } newValue = `2000-01-01T${newValue.split("T")[1]}` } @@ -113,7 +106,7 @@ const clearDateOnBackspace = event => { if (["Backspace", "Clear", "Delete"].includes(event.key)) { - dispatch("change", null) + dispatch("change", "") flatpickr.close() } } diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index a3952a9759..f7afc10bbc 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -11,14 +11,31 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionTitle = option => option + export let sort = false const dispatch = createEventDispatcher() const onChange = e => dispatch("change", e.target.value) + + const getSortedOptions = (options, getLabel, sort) => { + if (!options?.length || !Array.isArray(options)) { + return [] + } + if (!sort) { + return options + } + return [...options].sort((a, b) => { + const labelA = getLabel(a) + const labelB = getLabel(b) + return labelA > labelB ? 1 : -1 + }) + } + + $: parsedOptions = getSortedOptions(options, getOptionLabel, sort)
- {#if options && Array.isArray(options)} - {#each options as option} + {#if parsedOptions && Array.isArray(parsedOptions)} + {#each parsedOptions as option}
{#if open} - {#key anchor} - -
{}, - anchor, - }} - on:keydown={handleEscape} - class="spectrum-Popover is-open" - role="presentation" - transition:fly|local={{ y: -20, duration: 200 }} - > - -
-
- {/key} + +
{}, + anchor, + }} + on:keydown={handleEscape} + class="spectrum-Popover is-open" + role="presentation" + transition:fly|local={{ y: -20, duration: 200 }} + > + +
+
{/if} diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte index cb80072694..b5173682d0 100644 --- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte @@ -58,7 +58,7 @@ entries = entries.filter(f => f.name !== originalName) } value = entries.reduce((newVals, current) => { - newVals[current.name] = current.type + newVals[current.name.trim()] = current.type return newVals }, {}) dispatch("change", value) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 4ed77d55b2..6505b2e740 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -12,7 +12,7 @@ Modal, notifications, } from "@budibase/bbui" - import { createEventDispatcher, onMount } from "svelte" + import { createEventDispatcher } from "svelte" import { cloneDeep } from "lodash/fp" import { tables, datasources } from "stores/backend" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" @@ -48,7 +48,22 @@ const { hide } = getContext(Context.Modal) let fieldDefinitions = cloneDeep(FIELDS) - export let field = { + export let field + + let originalName + let linkEditDisabled + let primaryDisplay + let indexes = [...($tables.selected.indexes || [])] + let isCreating + + let table = $tables.selected + let confirmDeleteDialog + let deletion + let savingColumn + let deleteColName + let jsonSchemaModal + + let editableColumn = { type: "string", constraints: fieldDefinitions.STRING.constraints, @@ -56,48 +71,80 @@ fieldName: $tables.selected.name, } - let originalName = field.name - const linkEditDisabled = originalName != null - let primaryDisplay = - $tables.selected.primaryDisplay == null || - $tables.selected.primaryDisplay === field.name - let isCreating = originalName == null + $: if (primaryDisplay) { + editableColumn.constraints.presence = { allowEmpty: false } + } - let table = $tables.selected - let indexes = [...($tables.selected.indexes || [])] - let confirmDeleteDialog - let deletion - let deleteColName - let jsonSchemaModal + $: if (field && !savingColumn) { + editableColumn = cloneDeep(field) + originalName = editableColumn.name ? editableColumn.name + "" : null + linkEditDisabled = originalName != null + isCreating = originalName == null + primaryDisplay = + $tables.selected.primaryDisplay == null || + $tables.selected.primaryDisplay === editableColumn.name + } - $: checkConstraints(field) - $: required = !!field?.constraints?.presence || primaryDisplay + $: checkConstraints(editableColumn) + $: required = !!editableColumn?.constraints?.presence || primaryDisplay $: uneditable = $tables.selected?._id === TableNames.USERS && - UNEDITABLE_USER_FIELDS.includes(field.name) + UNEDITABLE_USER_FIELDS.includes(editableColumn.name) $: invalid = - !field.name || - (field.type === LINK_TYPE && !field.tableId) || + !editableColumn?.name || + (editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) || Object.keys(errors).length !== 0 - $: errors = checkErrors(field) + $: errors = checkErrors(editableColumn) $: datasource = $datasources.list.find( source => source._id === table?.sourceId ) + const getTableAutoColumnTypes = table => { + return Object.keys(table?.schema).reduce((acc, key) => { + let fieldSchema = table?.schema[key] + if (fieldSchema.autocolumn) { + acc.push(fieldSchema.subtype) + } + return acc + }, []) + } + + let autoColumnInfo = getAutoColumnInformation() + + $: tableAutoColumnsTypes = getTableAutoColumnTypes($tables?.selected) + $: availableAutoColumns = Object.keys(autoColumnInfo).reduce((acc, key) => { + if (!tableAutoColumnsTypes.includes(key)) { + acc[key] = autoColumnInfo[key] + } + return acc + }, {}) + + $: availableAutoColumnKeys = availableAutoColumns + ? Object.keys(availableAutoColumns) + : [] + + $: autoColumnOptions = editableColumn.autocolumn + ? autoColumnInfo + : availableAutoColumns + // used to select what different options can be displayed for column type $: canBeSearched = - field.type !== LINK_TYPE && - field.type !== JSON_TYPE && - field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && - field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && - field.type !== FORMULA_TYPE + editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== JSON_TYPE && + editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && + editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && + editableColumn?.type !== FORMULA_TYPE $: canBeDisplay = - field.type !== LINK_TYPE && - field.type !== AUTO_TYPE && - field.type !== JSON_TYPE + editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== AUTO_TYPE && + editableColumn?.type !== JSON_TYPE && + !editableColumn.autocolumn $: canBeRequired = - field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE - $: relationshipOptions = getRelationshipOptions(field) + editableColumn?.type !== LINK_TYPE && + !uneditable && + editableColumn?.type !== AUTO_TYPE && + !editableColumn.autocolumn + $: relationshipOptions = getRelationshipOptions(editableColumn) $: external = table.type === "external" // in the case of internal tables the sourceId will just be undefined $: tableOptions = $tables.list.filter( @@ -108,76 +155,90 @@ ) $: typeEnabled = !originalName || - (originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1) + (originalName && + SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 && + !editableColumn?.autocolumn) async function saveColumn() { - if (field.type === AUTO_TYPE) { - field = buildAutoColumn($tables.selected.name, field.name, field.subtype) + savingColumn = true + if (errors?.length) { + return } - if (field.type !== LINK_TYPE) { - delete field.fieldName + + let saveColumn = cloneDeep(editableColumn) + + if (saveColumn.type === AUTO_TYPE) { + saveColumn = buildAutoColumn( + $tables.draft.name, + saveColumn.name, + saveColumn.subtype + ) + } + if (saveColumn.type !== LINK_TYPE) { + delete saveColumn.fieldName } try { await tables.saveField({ originalName, - field, + field: saveColumn, primaryDisplay, indexes, }) dispatch("updatecolumns") } catch (err) { - notifications.error("Error saving column") + console.log(err) + notifications.error(`Error saving column: ${err.message}`) } } function cancelEdit() { - field.name = originalName + editableColumn.name = originalName } function deleteColumn() { try { - field.name = deleteColName - if (field.name === $tables.selected.primaryDisplay) { + editableColumn.name = deleteColName + if (editableColumn.name === $tables.selected.primaryDisplay) { notifications.error("You cannot delete the display column") } else { - tables.deleteField(field) - notifications.success(`Column ${field.name} deleted.`) + tables.deleteField(editableColumn) + notifications.success(`Column ${editableColumn.name} deleted.`) confirmDeleteDialog.hide() hide() deletion = false dispatch("updatecolumns") } } catch (error) { - notifications.error("Error deleting column") + notifications.error(`Error deleting column: ${error.message}`) } } function handleTypeChange(event) { // remove any extra fields that may not be related to this type - delete field.autocolumn - delete field.subtype - delete field.tableId - delete field.relationshipType - delete field.formulaType + delete editableColumn.autocolumn + delete editableColumn.subtype + delete editableColumn.tableId + delete editableColumn.relationshipType + delete editableColumn.formulaType // Add in defaults and initial definition const definition = fieldDefinitions[event.detail?.toUpperCase()] if (definition?.constraints) { - field.constraints = definition.constraints + editableColumn.constraints = definition.constraints } // Default relationships many to many - if (field.type === LINK_TYPE) { - field.relationshipType = RelationshipTypes.MANY_TO_MANY + if (editableColumn.type === LINK_TYPE) { + editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY } - if (field.type === FORMULA_TYPE) { - field.formulaType = "dynamic" + if (editableColumn.type === FORMULA_TYPE) { + editableColumn.formulaType = "dynamic" } } function onChangeRequired(e) { const req = e.detail - field.constraints.presence = req ? { allowEmpty: false } : false + editableColumn.constraints.presence = req ? { allowEmpty: false } : false required = req } @@ -185,17 +246,17 @@ const isPrimary = e.detail // primary display is always required if (isPrimary) { - field.constraints.presence = { allowEmpty: false } + editableColumn.constraints.presence = { allowEmpty: false } } } function onChangePrimaryIndex(e) { - indexes = e.detail ? [field.name] : [] + indexes = e.detail ? [editableColumn.name] : [] } function onChangeSecondaryIndex(e) { if (e.detail) { - indexes[1] = field.name + indexes[1] = editableColumn.name } else { indexes = indexes.slice(0, 1) } @@ -246,11 +307,14 @@ } function getAllowedTypes() { - if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) { + if ( + originalName && + ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1 + ) { return ALLOWABLE_STRING_OPTIONS } else if ( originalName && - ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1 + ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1 ) { return ALLOWABLE_NUMBER_OPTIONS } else if (!external) { @@ -275,6 +339,9 @@ } function checkConstraints(fieldToCheck) { + if (!fieldToCheck) { + return + } // most types need this, just make sure its always present if (fieldToCheck && !fieldToCheck.constraints) { fieldToCheck.constraints = {} @@ -296,10 +363,16 @@ } function checkErrors(fieldInfo) { + if (!editableColumn) { + return {} + } function inUse(tbl, column, ogName = null) { - return Object.keys(tbl?.schema || {}).some( - key => key !== ogName && key === column - ) + const parsedColumn = column ? column.toLowerCase().trim() : column + + return Object.keys(tbl?.schema || {}).some(key => { + let lowerKey = key.toLowerCase() + return lowerKey !== ogName?.toLowerCase() && lowerKey === parsedColumn + }) } const newError = {} if (!external && fieldInfo.name?.startsWith("_")) { @@ -313,6 +386,11 @@ } else if (inUse($tables.selected, fieldInfo.name, originalName)) { newError.name = `Column name already in use.` } + + if (fieldInfo.type == "auto" && !fieldInfo.subtype) { + newError.subtype = `Auto Column requires a type` + } + if (fieldInfo.fieldName && fieldInfo.tableId) { const relatedTable = $tables.list.find( tbl => tbl._id === fieldInfo.tableId @@ -323,12 +401,6 @@ } return newError } - - onMount(() => { - if (primaryDisplay) { - field.constraints.presence = { allowEmpty: false } - } - }) - {:else if field.type === "options"} + {:else if editableColumn.type === "options"} - {:else if field.type === "longform"} + {:else if editableColumn.type === "longform"}
- {:else if field.type === "array"} + {:else if editableColumn.type === "array"} - {:else if field.type === "datetime"} + {:else if editableColumn.type === "datetime" && !editableColumn.autocolumn} + - {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
- +
{/if} - {:else if field.type === "number"} + {:else if editableColumn.type === "number" && !editableColumn.autocolumn} - {:else if field.type === "link"} + {:else if editableColumn.type === "link"} - {:else if field.type === FORMULA_TYPE} + {:else if editableColumn.type === FORMULA_TYPE} {#if !table.sql} (field.subtype = e.detail)} - options={Object.entries(getAutoColumnInformation())} - getOptionLabel={option => option[1].name} - getOptionValue={option => option[0]} - /> - {:else if field.type === JSON_TYPE} + {:else if editableColumn.type === JSON_TYPE} {/if} + {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} + { - fromColumn = tableOptions.find(opt => opt.value === e.detail)?.label || "" - if (errors.fromTable === relationshipAlreadyExists) { - errors.toColumn = null - } - errors.fromTable = null - errors.fromColumn = null - errors.toTable = null - errors.throughTable = null - }} - /> - {#if isManyToOne && fromTable} + {#if !selectedFromTable} (errors.fromPrimary = null)} + on:change={changed} /> {/if} { - errors.fromTable = null - errors.toTable = null - errors.throughTable = null - }} + on:change={() => + changed(() => { + throughToKey = null + throughFromKey = null + })} /> - {#if fromTable && toTable && throughTable} + {#if fromId && toId && throughId} { - if (throughToKey === e.detail) { - throughToKey = null - } - errors.throughFromKey = null - }} + on:change={e => + changed(() => { + if (throughToKey === e.detail) { + throughToKey = null + } + })} /> {/if} - {:else if isManyToOne && toTable} + {:else if isManyToOne && toId} (errors.toColumn = e.detail?.length > 0 ? null : colNotSet)} + on:change={changed} />
{#if originalFromColumnName != null} diff --git a/packages/builder/src/components/backend/Datasources/relationshipErrors.js b/packages/builder/src/components/backend/Datasources/relationshipErrors.js new file mode 100644 index 0000000000..0dc9b264b9 --- /dev/null +++ b/packages/builder/src/components/backend/Datasources/relationshipErrors.js @@ -0,0 +1,103 @@ +import { RelationshipTypes } from "constants/backend" + +const typeMismatch = "Column type of the foreign key must match the primary key" +const columnBeingUsed = "Column name cannot be an existing column" +const mustBeDifferentTables = "From/to/through tables must be different" +const primaryKeyNotSet = "Please pick the primary key" +const throughNotNullable = + "Ensure non-key columns are nullable or auto-generated" +const noRelationshipType = "Please specify a relationship type" +const tableNotSet = "Please specify a table" +const foreignKeyNotSet = "Please pick a foreign key" +const relationshipAlreadyExists = + "A relationship between these tables already exists" + +function isColumnNameBeingUsed(table, columnName, originalName) { + if (!table || !columnName || columnName === originalName) { + return false + } + const keys = Object.keys(table.schema).map(key => key.toLowerCase()) + return keys.indexOf(columnName.toLowerCase()) !== -1 +} + +function typeMismatchCheck(fromTable, toTable, primary, foreign) { + let fromType, toType + if (primary && foreign) { + fromType = fromTable?.schema[primary]?.type + toType = toTable?.schema[foreign]?.type + } + return fromType && toType && fromType !== toType ? typeMismatch : null +} + +export class RelationshipErrorChecker { + constructor(invalidThroughTableFn, relationshipExistsFn) { + this.invalidThroughTable = invalidThroughTableFn + this.relationshipExists = relationshipExistsFn + } + + setType(type) { + this.type = type + } + + isMany() { + return this.type === RelationshipTypes.MANY_TO_MANY + } + + relationshipTypeSet(type) { + return !type ? noRelationshipType : null + } + + tableSet(table) { + return !table ? tableNotSet : null + } + + throughTableSet(table) { + return this.isMany() && !table ? tableNotSet : null + } + + manyForeignKeySet(key) { + return this.isMany() && !key ? foreignKeyNotSet : null + } + + foreignKeySet(key) { + return !this.isMany() && !key ? foreignKeyNotSet : null + } + + primaryKeySet(key) { + return !this.isMany() && !key ? primaryKeyNotSet : null + } + + throughIsNullable() { + return this.invalidThroughTable() ? throughNotNullable : null + } + + doesRelationshipExists() { + return this.isMany() && this.relationshipExists() + ? relationshipAlreadyExists + : null + } + + differentTables(table1, table2, table3) { + // currently don't support relationships back onto the table itself, needs to relate out + const error = table1 && (table1 === table2 || (table3 && table1 === table3)) + return error ? mustBeDifferentTables : null + } + + columnBeingUsed(table, column, ogName) { + return isColumnNameBeingUsed(table, column, ogName) ? columnBeingUsed : null + } + + typeMismatch(fromTable, toTable, primary, foreign) { + if (this.isMany()) { + return null + } + return typeMismatchCheck(fromTable, toTable, primary, foreign) + } + + manyTypeMismatch(table, throughTable, primary, foreign) { + if (!this.isMany()) { + return null + } + return typeMismatchCheck(table, throughTable, primary, foreign) + } +} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte index c929f1039e..9a05b90567 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte @@ -22,6 +22,7 @@ const dispatch = createEventDispatcher() let bindingDrawer + let valid = true $: readableValue = runtimeToReadableBinding(bindings, value) $: tempValue = readableValue @@ -76,12 +77,15 @@ Add the objects on the left to enrich your text. - + (tempValue = event.detail)} {bindings} {allowJS} diff --git a/packages/builder/src/components/common/bindings/ModalBindableInput.svelte b/packages/builder/src/components/common/bindings/ModalBindableInput.svelte index a3fddac3a5..dc851eb270 100644 --- a/packages/builder/src/components/common/bindings/ModalBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/ModalBindableInput.svelte @@ -106,4 +106,8 @@ border: var(--border-light); border-radius: 4px; } + + .control :global(.spectrum-Textfield-input) { + padding-right: 40px; + } diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index f9f4295c17..65a21f368d 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -56,7 +56,7 @@ const componentMap = { "field/link": FormFieldSelect, "field/array": FormFieldSelect, "field/json": FormFieldSelect, - "field/barcode/qr": FormFieldSelect, + "field/barcodeqr": FormFieldSelect, // Some validation types are the same as others, so not all types are // explicitly listed here. e.g. options uses string validation "validation/string": ValidationEditor, diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index 1d18fa3a92..8f2c286261 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -11,7 +11,10 @@ } from "@budibase/bbui" import { getAvailableActions } from "./index" import { generate } from "shortid" - import { getEventContextBindings } from "builderStore/dataBinding" + import { + getEventContextBindings, + makeStateBinding, + } from "builderStore/dataBinding" import { currentAsset, store } from "builderStore" const flipDurationMs = 150 @@ -52,7 +55,7 @@ actions, selectedAction?.id ) - $: allBindings = eventContexBindings.concat(bindings) + $: allBindings = getAllBindings(bindings, eventContexBindings, actions) $: { // Ensure each action has a unique ID if (actions) { @@ -74,8 +77,18 @@ } const deleteAction = index => { + // Check if we're deleting the selected action + const selectedIndex = actions.indexOf(selectedAction) + const isSelected = index === selectedIndex + + // Delete the action actions.splice(index, 1) actions = actions + + // Select a new action if we deleted the selected one + if (isSelected) { + selectedAction = actions?.length ? actions[0] : null + } } const toggleActionList = () => { @@ -111,6 +124,37 @@ function handleDndFinalize(e) { actions = e.detail.items } + + const getAllBindings = (bindings, eventContextBindings, actions) => { + let allBindings = eventContextBindings.concat(bindings) + + if (!actions) { + return [] + } + + // Ensure bindings are generated for all "update state" action keys + actions + .filter(action => { + // Find all "Update State" actions which set values + return ( + action[EVENT_TYPE_KEY] === "Update State" && + action.parameters?.type === "set" && + action.parameters.key + ) + }) + .forEach(action => { + // Check we have a binding for this action, and generate one if not + const stateBinding = makeStateBinding(action.parameters.key) + const hasKey = allBindings.some(binding => { + return binding.runtimeBinding === stateBinding.runtimeBinding + }) + if (!hasKey) { + allBindings.push(stateBinding) + } + }) + + return allBindings + } @@ -186,7 +230,7 @@
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte index 21768ac461..a2ffb144c0 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte @@ -36,7 +36,13 @@ $: selectedSchema = selectedAutomation?.schema const onFieldsChanged = e => { - parameters.fields = e.detail + parameters.fields = Object.entries(e.detail || {}).reduce( + (acc, [key, value]) => { + acc[key.trim()] = value + return acc + }, + {} + ) } const setNew = () => { diff --git a/packages/builder/src/components/design/settings/controls/FormFieldSelect.svelte b/packages/builder/src/components/design/settings/controls/FormFieldSelect.svelte index a02ea41099..806abc4e92 100644 --- a/packages/builder/src/components/design/settings/controls/FormFieldSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/FormFieldSelect.svelte @@ -25,7 +25,7 @@ const getOptions = (schema, type) => { let entries = Object.entries(schema ?? {}) let types = [] - if (type === "field/options" || type === "field/barcode/qr") { + if (type === "field/options") { // allow options to be used on both options and string fields types = [type, "field/string"] } else { @@ -35,6 +35,7 @@ types = types.map(type => type.slice(type.indexOf("/") + 1)) entries = entries.filter(entry => types.includes(entry[1].type)) + return entries.map(entry => entry[0]) } diff --git a/packages/builder/src/components/portal/onboarding/TourPopover.svelte b/packages/builder/src/components/portal/onboarding/TourPopover.svelte index 2e225c6a1c..e6a4ed3b27 100644 --- a/packages/builder/src/components/portal/onboarding/TourPopover.svelte +++ b/packages/builder/src/components/portal/onboarding/TourPopover.svelte @@ -108,50 +108,52 @@ } -{#key tourStepKey} - -
- -
- {tourStep?.title || "-"} -
{`${tourStepIdx + 1}/${tourSteps?.length}`}
-
- - - {#if tourStep.layout} - - {:else} - {tourStep?.body || ""} - {/if} - - - + + {/key} +{/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte index c301bfdc20..096e47430c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte @@ -30,7 +30,7 @@ {#if $selectedComponent} {#key $selectedComponent._id} - {#if componentDefinition.info} + {#if componentDefinition?.info} {/if} $goto("../")}> - Forgotten your password? + Forgot your password?
@@ -83,7 +83,12 @@
-
@@ -92,7 +97,7 @@ diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 032cf850fa..80122c23a5 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -66,7 +66,7 @@ - + {#if loaded} logo @@ -124,14 +124,19 @@ - diff --git a/packages/builder/src/pages/builder/invite/index.svelte b/packages/builder/src/pages/builder/invite/index.svelte index 4b786db497..35231117c4 100644 --- a/packages/builder/src/pages/builder/invite/index.svelte +++ b/packages/builder/src/pages/builder/invite/index.svelte @@ -68,7 +68,7 @@ - + logo Join {company} @@ -175,6 +175,7 @@