diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index cf0d6f848c..57bd5a9640 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 5047f55197..7ec2725a7f 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 1587bfc537..34951b6310 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 9f9092b399..3f9cedba4f 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. + + + +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. + +### [Get started with Budibase](https://budibase.com) + +

-### 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 -``` - - -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). - -
- ---- - -
- ## 🎓 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,32 +121,22 @@ 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 [![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) 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 18b93fdf61..66b24f4e49 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 01d5a09efa..59363fab5e 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 d5f9ebee28..d9f8384688 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 31cdca2aef..d7d8702567 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 bf2656cc64..aebd39acab 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 c74bc1bee4..5f6866e9ed 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 931bc3d496..7af3c2eb9d 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 477968975a..ecdaae5bad 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 1b48786e24..fd004ca0c2 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 e2ad9a9300..3a3c55bfa0 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 54a5504712..b467c0b10b 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 0000000000..7492e94511 --- /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 0000000000..e9a18eadde --- /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 0000000000..0ed16fc184 --- /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 e83784471b..bb99592d1c 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 57768d4071..45b9ff33f9 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 823fd06322..e1df289d6e 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 35f892669a..fc69b7f76f 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 d66a6851f5..80bacada00 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 176db9f497..8edb68a38e 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 041054e406..7d5656a22d 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 11deaa8318..9a53fd0169 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 3644280edf..86f2c0ee52 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 dad1cd5c37..96a1bd75aa 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 308a5c7966..5b7922bde7 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 0000000000..b9da64989b --- /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 f179a24729..82e3c45a1f 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 80c9570cb7..d262839f12 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 66125a314a..4bcb9b74c6 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 dc1e40e517..a19646e6fd 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 b076a8da86..5ae031e033 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 d1ab19761b..fc812e0289 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 0000000000..e99fed0d41 --- /dev/null +++ b/packages/builder/src/components/common/CodeMirrorEditor.svelte @@ -0,0 +1,159 @@ + + + + +{#if label} +
+ +
+{/if} +
+