diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index cf0d6f848..57bd5a964 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -9,7 +9,6 @@ env: POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} jobs: release: diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml index 5047f5519..7ec2725a7 100644 --- a/.github/workflows/release-selfhost.yml +++ b/.github/workflows/release-selfhost.yml @@ -7,7 +7,6 @@ env: POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} jobs: release: diff --git a/.vscode/launch.json b/.vscode/launch.json index 1587bfc53..34951b631 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,39 +4,27 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}/app.js", - "skipFiles": [ - "/**" - ] - }, - { - "type": "node", - "request": "launch", - "name": "Debug External", - "program": "${workspaceFolder}/packages/cli/bin/budi", - "args": [], - "cwd":"C:/code/my-apps", - "console": "externalTerminal" - }, { "name": "Budibase Server", "type": "node", "request": "launch", - "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], - "args": ["${workspaceFolder}/packages/server/src/index.ts"], + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "${workspaceFolder}/packages/server/src/index.ts" + ], "cwd": "${workspaceFolder}/packages/server" - }, - { + }, + { "name": "Budibase Worker", "type": "node", "request": "launch", "program": "${workspaceFolder}/packages/worker/src/index.js", "cwd": "${workspaceFolder}/packages/worker" - } + } ], "compounds": [ { diff --git a/README.md b/README.md index 9f9092b39..3f9cedba4 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,19 @@

- Build, automate and self-host internal tools in minutes + The low code platform you'll enjoy using

- Budibase is an open-source low-code platform, helping developers and IT professionals build, automate, and ship internal tools on their own infrastructure in minutes. + Budibase is an open source low-code platform, and the easiest way to build internal tools that improve productivity.

🤖 🎨 🚀

+

- Budibase design ui + Budibase design ui

@@ -65,68 +66,25 @@ - **Admin paradise.** Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager. -
- ---- - -
+


## 🏁 Get started -Currently there are two ways to get started with Budibase; Digital Ocean, and Docker. -

- -### Get started with Digital Ocean -The easiest and quickest way to get started, is to use Digital Ocean: -1-click Digital Ocean deploy - - - digital ocean badge - -

- -### Get started with Docker -To get started, you must have docker and docker compose installed on your machine. -Once you have Docker installed, the process takes 5 minutes, with these four steps: - -1. Install the Budibase CLI. -``` -$ npm i -g @budibase/cli -``` + +Deploy Budibase self-Hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. +Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly. -2. Setup Budibase (select where to store Budibase, and the port to run it on) - -``` -budi hosting --init -``` - - -3. Run Budibase - -``` -budi hosting --start -``` - - -4. Create your admin user - -Enter the email and password for the new admin user. - -Done! You are now ready to build powerful internal tools in minutes. For additional information on how to get started and learn Budibase, visit our [docs](https://docs.budibase.com/getting-started). - -
+### [Get started with Budibase](https://budibase.com) ---- -
+

## 🎓 Learning Budibase The Budibase documentation [lives here](https://docs.budibase.com).
----

@@ -134,22 +92,17 @@ The Budibase documentation [lives here](https://docs.budibase.com). If you have a question or would like to talk with other Budibase users and join our community, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) - - -

- ---- +


-
## ❗ Code of conduct Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
---- -
+

+ ## 🙌 Contributing to Budibase @@ -168,21 +121,15 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi - [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa 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. For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) -

- ----

+ ## 📝 License Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps that you build can be licensed however you like. -

- ---- - -
+

## ⭐ Stargazers over time @@ -190,10 +137,6 @@ Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3 If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. -
- ---- -

## Contributors ✨ diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 18b93fdf6..66b24f4e4 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -48,10 +48,10 @@ services: COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984 + SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131 INTERNAL_API_KEY: ${INTERNAL_API_KEY} REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} - ACCOUNT_PORTAL_URL: https://portal.budi.live volumes: - ./logs:/logs depends_on: diff --git a/hosting/envoy.dev.yaml.hbs b/hosting/envoy.dev.yaml.hbs index 01d5a09ef..59363fab5 100644 --- a/hosting/envoy.dev.yaml.hbs +++ b/hosting/envoy.dev.yaml.hbs @@ -41,6 +41,7 @@ static_resources: - match: { prefix: "/api/" } route: cluster: server-dev + timeout: 120s - match: { prefix: "/app_" } route: diff --git a/hosting/envoy.yaml b/hosting/envoy.yaml index d5f9ebee2..d9f838468 100644 --- a/hosting/envoy.yaml +++ b/hosting/envoy.yaml @@ -58,6 +58,7 @@ static_resources: - match: { prefix: "/api/" } route: cluster: app-service + timeout: 120s - match: { prefix: "/worker/" } route: diff --git a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml index 31cdca2ae..d7d870256 100644 --- a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml +++ b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml @@ -91,6 +91,8 @@ spec: {{ end }} - name: SELF_HOSTED value: {{ .Values.globals.selfHosted | quote }} + - name: SENTRY_DSN + value: {{ .Values.globals.sentryDSN }} - name: ACCOUNT_PORTAL_URL value: {{ .Values.globals.accountPortalUrl | quote }} - name: ACCOUNT_PORTAL_API_KEY diff --git a/lerna.json b/lerna.json index bf2656cc6..aebd39aca 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.172", + "version": "0.9.173-alpha.3", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index c74bc1bee..5f6866e9e 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.172", + "version": "0.9.173-alpha.3", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", @@ -18,6 +18,7 @@ "jsonwebtoken": "^8.5.1", "koa-passport": "^4.1.4", "lodash": "^4.17.21", + "lodash.isarguments": "^3.1.0", "node-fetch": "^2.6.1", "passport-google-auth": "^1.0.2", "passport-google-oauth": "^2.0.0", diff --git a/packages/auth/src/db/Replication.js b/packages/auth/src/db/Replication.js index 931bc3d49..7af3c2eb9 100644 --- a/packages/auth/src/db/Replication.js +++ b/packages/auth/src/db/Replication.js @@ -45,22 +45,6 @@ class Replication { return this.replication } - /** - * Set up an ongoing live sync between 2 CouchDB databases. - * @param {Object} opts - PouchDB replication options - */ - subscribe(opts = {}) { - this.replication = this.source.replicate - .to(this.target, { - live: true, - retry: true, - ...opts, - }) - .on("error", function (err) { - throw new Error(`Replication Error: ${err}`) - }) - } - /** * Rollback the target DB back to the state of the source DB */ diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js index 477968975..ecdaae5ba 100644 --- a/packages/auth/src/db/constants.js +++ b/packages/auth/src/db/constants.js @@ -13,6 +13,7 @@ exports.DocumentTypes = { APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, ROLE: "role", + MIGRATIONS: "migrations", } exports.StaticDatabases = { diff --git a/packages/auth/src/db/views.js b/packages/auth/src/db/views.js index 1b48786e2..fd004ca0c 100644 --- a/packages/auth/src/db/views.js +++ b/packages/auth/src/db/views.js @@ -21,7 +21,7 @@ exports.createUserEmailView = async db => { // if using variables in a map function need to inject them before use map: `function(doc) { if (doc._id.startsWith("${DocumentTypes.USER}")) { - emit(doc.email, doc._id) + emit(doc.email.toLowerCase(), doc._id) } }`, } diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index e2ad9a930..3a3c55bfa 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -1,6 +1,6 @@ // Mock data -require("./utilities/test-config") +require("../../../tests/utilities/dbConfig") const database = require("../../../db") const { authenticateThirdParty } = require("../third-party-common") @@ -72,7 +72,6 @@ describe("third party common", () => { const expectUserIsSynced = (user, thirdPartyUser) => { expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.email).toBe(thirdPartyUser.email) expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) @@ -135,6 +134,24 @@ describe("third party common", () => { }) }) + describe("exists by email with different casing", () => { + beforeEach(async () => { + id = generateGlobalUserID(newid()) // random id + email = thirdPartyUser.email.toUpperCase() // matching email except for casing + await createUser() + }) + + it("syncs and authenticates the user", async () => { + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) + + const user = expectUserIsAuthenticated() + expectUserIsSynced(user, thirdPartyUser) + expectUserIsUpdated(user) + expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) + }) + }) + + describe("exists by id", () => { beforeEach(async () => { id = generateGlobalUserID(thirdPartyUser.userId) // matching id diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 54a550471..b467c0b10 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -66,12 +66,16 @@ exports.authenticateThirdParty = async function ( // setup a blank user using the third party id dbUser = { _id: userId, + email: thirdPartyUser.email, roles: {}, } } dbUser = await syncUser(dbUser, thirdPartyUser) + // never prompt for password reset + dbUser.forceResetPassword = false + // create or sync the user let response try { @@ -122,9 +126,6 @@ async function syncUser(user, thirdPartyUser) { user.provider = thirdPartyUser.provider user.providerType = thirdPartyUser.providerType - // email - user.email = thirdPartyUser.email - if (thirdPartyUser.profile) { const profile = thirdPartyUser.profile diff --git a/packages/auth/src/migrations/index.js b/packages/auth/src/migrations/index.js new file mode 100644 index 000000000..7492e9451 --- /dev/null +++ b/packages/auth/src/migrations/index.js @@ -0,0 +1,61 @@ +const { DocumentTypes } = require("../db/constants") +const { getGlobalDB } = require("../tenancy") + +exports.MIGRATION_DBS = { + GLOBAL_DB: "GLOBAL_DB", +} + +exports.MIGRATIONS = { + USER_EMAIL_VIEW_CASING: "user_email_view_casing", +} + +const DB_LOOKUP = { + [exports.MIGRATION_DBS.GLOBAL_DB]: [ + exports.MIGRATIONS.USER_EMAIL_VIEW_CASING, + ], +} + +exports.getMigrationsDoc = async db => { + // get the migrations doc + try { + return await db.get(DocumentTypes.MIGRATIONS) + } catch (err) { + if (err.status && err.status === 404) { + return { _id: DocumentTypes.MIGRATIONS } + } + } +} + +exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { + try { + let db + if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { + db = getGlobalDB() + } else { + throw new Error(`Unrecognised migration db [${migrationDb}]`) + } + + if (!DB_LOOKUP[migrationDb].includes(migrationName)) { + throw new Error( + `Unrecognised migration name [${migrationName}] for db [${migrationDb}]` + ) + } + + const doc = await exports.getMigrationsDoc(db) + // exit if the migration has been performed + if (doc[migrationName]) { + return + } + + console.log(`Performing migration: ${migrationName}`) + await migrateFn() + console.log(`Migration complete: ${migrationName}`) + + // mark as complete + doc[migrationName] = Date.now() + await db.put(doc) + } catch (err) { + console.error(`Error performing migration: ${migrationName}: `, err) + throw err + } +} diff --git a/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap b/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..e9a18eadd --- /dev/null +++ b/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`migrations should match snapshot 1`] = ` +Object { + "_id": "migrations", + "_rev": "1-af6c272fe081efafecd2ea49a8fcbb40", + "user_email_view_casing": 1487076708000, +} +`; diff --git a/packages/auth/src/migrations/tests/index.spec.js b/packages/auth/src/migrations/tests/index.spec.js new file mode 100644 index 000000000..0ed16fc18 --- /dev/null +++ b/packages/auth/src/migrations/tests/index.spec.js @@ -0,0 +1,60 @@ +require("../../tests/utilities/dbConfig") + +const { migrateIfRequired, MIGRATION_DBS, MIGRATIONS, getMigrationsDoc } = require("../index") +const database = require("../../db") +const { + StaticDatabases, +} = require("../../db/utils") + +Date.now = jest.fn(() => 1487076708000) +let db + +describe("migrations", () => { + + const migrationFunction = jest.fn() + + beforeEach(() => { + db = database.getDB(StaticDatabases.GLOBAL.name) + }) + + afterEach(async () => { + jest.clearAllMocks() + await db.destroy() + }) + + const validMigration = () => { + return migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) + } + + it("should run a new migration", async () => { + await validMigration() + expect(migrationFunction).toHaveBeenCalled() + }) + + it("should match snapshot", async () => { + await validMigration() + const doc = await getMigrationsDoc(db) + expect(doc).toMatchSnapshot() + }) + + it("should skip a previously run migration", async () => { + await validMigration() + await validMigration() + expect(migrationFunction).toHaveBeenCalledTimes(1) + }) + + it("should reject an unknown migration name", async () => { + expect(async () => { + await migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, "bogus_name", migrationFunction) + }).rejects.toThrow() + expect(migrationFunction).not.toHaveBeenCalled() + }) + + it("should reject an unknown database name", async () => { + expect(async () => { + await migrateIfRequired("bogus_db", MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) + }).rejects.toThrow() + expect(migrationFunction).not.toHaveBeenCalled() + }) + +}) \ No newline at end of file diff --git a/packages/auth/src/middleware/passport/tests/utilities/db.js b/packages/auth/src/tests/utilities/db.js similarity index 87% rename from packages/auth/src/middleware/passport/tests/utilities/db.js rename to packages/auth/src/tests/utilities/db.js index e83784471..bb99592d1 100644 --- a/packages/auth/src/middleware/passport/tests/utilities/db.js +++ b/packages/auth/src/tests/utilities/db.js @@ -1,5 +1,5 @@ const PouchDB = require("pouchdb") -const env = require("../../../../environment") +const env = require("../../environment") let POUCH_DB_DEFAULTS diff --git a/packages/auth/src/middleware/passport/tests/utilities/test-config.js b/packages/auth/src/tests/utilities/dbConfig.js similarity index 53% rename from packages/auth/src/middleware/passport/tests/utilities/test-config.js rename to packages/auth/src/tests/utilities/dbConfig.js index 57768d407..45b9ff33f 100644 --- a/packages/auth/src/middleware/passport/tests/utilities/test-config.js +++ b/packages/auth/src/tests/utilities/dbConfig.js @@ -1,3 +1,3 @@ -const packageConfiguration = require("../../../../index") +const packageConfiguration = require("../../index") const CouchDB = require("./db") packageConfiguration.init(CouchDB) diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 823fd0632..e1df289d6 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -20,6 +20,9 @@ const { hash } = require("./hashing") const userCache = require("./cache/user") const env = require("./environment") const { getUserSessions, invalidateSessions } = require("./security/sessions") +const { migrateIfRequired } = require("./migrations") +const { USER_EMAIL_VIEW_CASING } = require("./migrations").MIGRATIONS +const { GLOBAL_DB } = require("./migrations").MIGRATION_DBS const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -128,10 +131,16 @@ exports.getGlobalUserByEmail = async email => { throw "Must supply an email address to view" } const db = getGlobalDB() + + await migrateIfRequired(GLOBAL_DB, USER_EMAIL_VIEW_CASING, async () => { + // re-create the view with latest changes + await createUserEmailView(db) + }) + try { let users = ( await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { - key: email, + key: email.toLowerCase(), include_docs: true, }) ).rows diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 35f892669..fc69b7f76 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -3038,6 +3038,11 @@ lodash.includes@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index d66a6851f..80bacada0 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": "0.9.172", + "version": "0.9.173-alpha.3", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 176db9f49..8edb68a38 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -31,7 +31,11 @@ const handleChange = event => { const [dates] = event.detail - dispatch("change", dates[0]) + let newValue = dates[0] + if (newValue) { + newValue = newValue.toISOString() + } + dispatch("change", newValue) } const clearDateOnBackspace = event => { @@ -57,11 +61,36 @@ const els = document.querySelectorAll(`#${flatpickrId} input`) els.forEach(el => el.blur()) } + + const parseDate = val => { + if (!val) { + return null + } + let date + if (val instanceof Date) { + // Use real date obj if already parsed + date = val + } else if (isNaN(val)) { + // Treat as date string of some sort + date = new Date(val) + } else { + // Treat as numerical timestamp + date = new Date(parseInt(val)) + } + const time = date.getTime() + if (isNaN(time)) { + return null + } + // By rounding to the nearest second we avoid locking up in an endless + // loop in the builder, caused by potentially enriching {{ now }} to every + // millisecond. + return new Date(Math.floor(time / 1000) * 1000) + } (open = false)} - transition:fly={{ y: -20, duration: 200 }} + transition:fly|local={{ y: -20, duration: 200 }} class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" class:auto-width={autoWidth} > diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index 041054e40..7d5656a22 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -13,10 +13,10 @@ export let appendTo = undefined const dispatch = createEventDispatcher() + const onChange = e => { - const isoString = e.detail.toISOString() - value = isoString - dispatch("change", isoString) + value = e.detail + dispatch("change", e.detail) } diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 11deaa831..9a53fd016 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -5,6 +5,7 @@ import RelationshipRenderer from "./RelationshipRenderer.svelte" import AttachmentRenderer from "./AttachmentRenderer.svelte" import ArrayRenderer from "./ArrayRenderer.svelte" + import InternalRenderer from "./InternalRenderer.svelte" export let row export let schema @@ -22,8 +23,8 @@ number: StringRenderer, longform: StringRenderer, array: ArrayRenderer, + internal: InternalRenderer, } - $: type = schema?.type ?? "string" $: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer diff --git a/packages/bbui/src/Tabs/Tab.svelte b/packages/bbui/src/Tabs/Tab.svelte index 3644280ed..86f2c0ee5 100644 --- a/packages/bbui/src/Tabs/Tab.svelte +++ b/packages/bbui/src/Tabs/Tab.svelte @@ -8,11 +8,19 @@ const selected = getContext("tab") let tab let tabInfo + const setTabInfo = () => { - tabInfo = tab.getBoundingClientRect() - if ($selected.title === title) { - $selected.info = tabInfo - } + // If the tabs are being rendered inside a component which uses + // a svelte transition to enter, then this initial getBoundingClientRect + // will return an incorrect position. + // We just need to get this off the main thread to fix this, by using + // a 0ms timeout. + setTimeout(() => { + tabInfo = tab.getBoundingClientRect() + if ($selected.title === title) { + $selected.info = tabInfo + } + }, 0) } onMount(() => { diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index dad1cd5c3..96a1bd75a 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -31,7 +31,7 @@ context("Create a Table", () => { cy.contains("nameupdated ").should("contain", "nameupdated") }) - /* + it("edits a row", () => { cy.contains("button", "Edit").click({ force: true }) cy.wait(1000) @@ -40,7 +40,7 @@ context("Create a Table", () => { cy.contains("Save").click() cy.contains("Updated").should("have.text", "Updated") }) - */ + it("deletes a row", () => { cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.contains("Delete 1 row(s)").click() diff --git a/packages/builder/cypress/integration/customThemingProperties.spec.js b/packages/builder/cypress/integration/customThemingProperties.spec.js index 308a5c796..5b7922bde 100644 --- a/packages/builder/cypress/integration/customThemingProperties.spec.js +++ b/packages/builder/cypress/integration/customThemingProperties.spec.js @@ -1,16 +1,16 @@ -context("Custom Theming Properties", () => { +xcontext("Custom Theming Properties", () => { before(() => { cy.login() cy.createTestApp() cy.navigateToFrontend() }) - // Default Values - // Button roundness = Large - // Accent colour = Blue 600 - // Accent colour (hover) = Blue 500 - // Navigation bar background colour = Gray 100 - // Navigation bar text colour = Gray 800 + /* Default Values: + Button roundness = Large + Accent colour = Blue 600 + Accent colour (hover) = Blue 500 + Navigation bar background colour = Gray 100 + Navigation bar text colour = Gray 800 */ it("should reset the color property values", () => { // Open Theme modal and change colours cy.get(".spectrum-ActionButton-label").contains("Theme").click() @@ -24,6 +24,29 @@ context("Custom Theming Properties", () => { checkThemeColorDefaults() }) + /* Button Roundness Values: + None = 0 + Small = 4px + Medium = 8px + Large = 16px */ + it("should test button roundness", () => { + const buttonRoundnessValues = ["0", "4px", "8px", "16px"] + cy.wait(1000) + // Add button, change roundness and confirm value + cy.addComponent("Button", null).then((componentId) => { + buttonRoundnessValues.forEach(function (item, index){ + cy.get(".spectrum-ActionButton-label").contains("Theme").click() + cy.get(".setting").contains("Button roundness").parent() + .get(".select-wrapper").click() + cy.get(".spectrum-Popover").find('li').eq(index).click() + cy.get(".spectrum-Button").contains("View changes").click({force: true}) + cy.reload() + cy.getComponent(componentId) + .parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`) + }) + }) + }) + const changeThemeColors = () => { // Changes the theme colours cy.get(".spectrum-FieldLabel").contains("Accent color") diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js new file mode 100644 index 000000000..b9da64989 --- /dev/null +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -0,0 +1,102 @@ +context("Rename an App", () => { + beforeEach(() => { + cy.login() + cy.createTestApp() + }) + +it("should rename an unpublished application", () => { + const appRename = "Cypress Renamed" + // Rename app, Search for app, Confirm name was changed + cy.get(".home-logo").click() + renameApp(appRename) + cy.searchForApplication(appRename) + cy.get(".appGrid").find(".wrapper").should("have.length", 1) + }) + +xit("Should rename a published application", () => { + // It is not possible to rename a published application + const appRename = "Cypress Renamed" + // Publish the app + cy.get(".toprightnav") + cy.get(".spectrum-Button").contains("Publish").click({force: true}) + cy.get(".spectrum-Dialog-grid") + .within(() => { + // Click publish again within the modal + cy.get(".spectrum-Button").contains("Publish").click({force: true}) + }) + // Rename app, Search for app, Confirm name was changed + cy.get(".home-logo").click() + renameApp(appRename, true) + cy.searchForApplication(appRename) + cy.get(".appGrid").find(".wrapper").should("have.length", 1) +}) + +it("Should try to rename an application to have no name", () => { + cy.get(".home-logo").click() + renameApp(" ", false, true) + // Close modal and confirm name has not been changed + cy.get(".spectrum-Dialog-grid").contains("Cancel").click() + cy.searchForApplication("Cypress Tests") + cy.get(".appGrid").find(".wrapper").should("have.length", 1) +}) + +xit("Should create two applications with the same name", () => { + // It is not possible to have applications with the same name + const appName = "Cypress Tests" + cy.visit(`localhost:${Cypress.env("PORT")}/builder`) + cy.wait(500) + cy.get(".spectrum-Button").contains("Create app").click({force: true}) + cy.contains(/Start from scratch/).click() + cy.get(".spectrum-Modal") + .within(() => { + cy.get("input").eq(0).type(appName) + cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true}) + cy.get(".error").should("have.text", "Another app with the same name already exists") + }) +}) + +it("should validate application names", () => { + // App name must be letters, numbers and spaces only + // This test checks numbers and special characters specifically + const numberName = 12345 + const specialCharName = "£$%^" + cy.get(".home-logo").click() + renameApp(numberName) + cy.searchForApplication(numberName) + cy.get(".appGrid").find(".wrapper").should("have.length", 1) + renameApp(specialCharName) + cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") +}) + + const renameApp = (appName, published, noName) => { + cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length > 0) { + cy.get(".title > :nth-child(3) > .spectrum-Icon").click() + // Check for when an app is published + if (published == true){ + // Should not have Edit as option, will unpublish app + cy.should("not.have.value", "Edit") + cy.get(".spectrum-Menu").contains("Unpublish").click() + cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() + cy.get(".title > :nth-child(3) > .spectrum-Icon").click() + } + cy.contains("Edit").click() + cy.get(".spectrum-Modal") + .within(() => { + if (noName == true){ + cy.get("input").clear() + cy.get(".spectrum-Dialog-grid").click() + .contains("App name must be letters, numbers and spaces only") + return cy + } + cy.get("input").clear() + cy.get("input").eq(0).type(appName).should("have.value", appName).blur() + cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true}) + cy.wait(500) + }) + } + }) +} +}) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index f179a2472..82e3c45a1 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -35,19 +35,12 @@ Cypress.Commands.add("login", () => { Cypress.Commands.add("createApp", name => { cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.wait(500) - cy.contains(/Start from scratch/).click() - cy.get(".spectrum-Modal") - .within(() => { - cy.get("input").eq(0).type(name).should("have.value", name).blur() - cy.get(".spectrum-ButtonGroup").contains("Create app").click() - cy.wait(7000) - }) - .then(() => { - // Because we show the datasource modal on entry, we need to create a table to get rid of the modal in the future - cy.createInitialDatasource("initialTable") - cy.expandBudibaseConnection() - cy.get(".nav-item.selected > .content").should("be.visible") - }) + cy.contains(/Start from scratch/).dblclick() + cy.get(".spectrum-Modal").within(() => { + cy.get("input").eq(0).type(name).should("have.value", name).blur() + cy.get(".spectrum-ButtonGroup").contains("Create app").click() + cy.wait(7000) + }) }) Cypress.Commands.add("deleteApp", () => { @@ -77,22 +70,6 @@ Cypress.Commands.add("createTestTableWithData", () => { cy.addColumn("dog", "age", "Number") }) -Cypress.Commands.add("createInitialDatasource", tableName => { - // Enter table name - cy.get(".spectrum-Modal").within(() => { - cy.contains("Budibase DB").trigger("mouseover").click().click() - cy.wait(1000) - cy.contains("Continue").click() - }) - - cy.get(".spectrum-Modal").within(() => { - cy.wait(1000) - cy.get("input").first().type(tableName).blur() - cy.get(".spectrum-ButtonGroup").contains("Create").click() - }) - cy.contains(tableName).should("be.visible") -}) - Cypress.Commands.add("createTable", tableName => { cy.contains("Budibase DB").click() cy.contains("Create new table").click() @@ -247,3 +224,9 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => { cy.get(".spectrum-Button").contains("Save").click({ force: true }) }) }) + +Cypress.Commands.add("searchForApplication", appName => { + cy.get(".spectrum-Textfield").within(() => { + cy.get("input").eq(0).type(appName) + }) +}) diff --git a/packages/builder/package.json b/packages/builder/package.json index 80c9570cb..d262839f1 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.172", + "version": "0.9.173-alpha.3", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.172", - "@budibase/client": "^0.9.172", + "@budibase/bbui": "^0.9.173-alpha.3", + "@budibase/client": "^0.9.173-alpha.3", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.172", + "@budibase/string-templates": "^0.9.173-alpha.3", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js index 66125a314..4bcb9b74c 100644 --- a/packages/builder/src/builderStore/api.js +++ b/packages/builder/src/builderStore/api.js @@ -15,7 +15,7 @@ const apiCall = if (resp.status === 403) { removeCookie(Cookies.Auth) // reload after removing cookie, go to login - if (!url.includes("self")) { + if (!url.includes("self") && !url.includes("login")) { location.reload() } } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index dc1e40e51..a19646e6f 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -7,11 +7,17 @@ import { } from "./storeUtils" import { store } from "builderStore" import { queries as queriesStores, tables as tablesStore } from "stores/backend" -import { makePropSafe } from "@budibase/string-templates" +import { + makePropSafe, + isJSBinding, + decodeJSBinding, + encodeJSBinding, +} from "@budibase/string-templates" import { TableNames } from "../constants" // Regex to match all instances of template strings const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g +const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g /** @@ -430,6 +436,15 @@ function replaceBetween(string, start, end, replacement) { * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { + // Decide from base64 if using JS + const isJS = isJSBinding(textWithBindings) + if (isJS) { + textWithBindings = decodeJSBinding(textWithBindings) + } + + // Determine correct regex to find bindings to replace + const regex = isJS ? CAPTURE_VAR_INSIDE_JS : CAPTURE_VAR_INSIDE_TEMPLATE + const convertFrom = convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding" if (typeof textWithBindings !== "string") { @@ -441,7 +456,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { .sort((a, b) => { return b.length - a.length }) - const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] + const boundValues = textWithBindings.match(regex) || [] let result = textWithBindings for (let boundValue of boundValues) { let newBoundValue = boundValue @@ -449,7 +464,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { // in the search, working from longest to shortest so always use best match first let searchString = newBoundValue for (let from of convertFromProps) { - if (shouldReplaceBinding(newBoundValue, from, convertTo)) { + if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) { const binding = bindableProperties.find(el => el[convertFrom] === from) let idx do { @@ -457,7 +472,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { idx = searchString.indexOf(from) if (idx !== -1) { let end = idx + from.length, - searchReplace = Array(binding[convertTo].length).join("*") + searchReplace = Array(binding[convertTo].length + 1).join("*") // blank out parts of the search string searchString = replaceBetween(searchString, idx, end, searchReplace) newBoundValue = replaceBetween( @@ -472,6 +487,12 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { } result = result.replace(boundValue, newBoundValue) } + + // Re-encode to base64 if using JS + if (isJS) { + result = encodeJSBinding(result) + } + return result } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index b076a8da8..5ae031e03 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -1,11 +1,26 @@

- {#each $queries.list as query} diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index d1ab19761..fc812e028 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -1,6 +1,12 @@ @@ -144,30 +152,36 @@ {getOptionLabel} {getOptionValue} /> - {#if filter.key && isMultipleChoice(filter.key)} - + {#if showValue(filter)} + {#if filter.key && isMultipleChoice(filter.key)} + + {:else} + + {/if} + removeFilter(idx)} /> {:else} - + removeFilter(idx)} /> + +
{/if} - removeFilter(idx)} /> {/each}
{:else} diff --git a/packages/builder/src/components/common/CodeMirrorEditor.svelte b/packages/builder/src/components/common/CodeMirrorEditor.svelte new file mode 100644 index 000000000..e99fed0d4 --- /dev/null +++ b/packages/builder/src/components/common/CodeMirrorEditor.svelte @@ -0,0 +1,159 @@ + + + + +{#if label} +
+ +
+{/if} +
+