diff --git a/.eslintignore b/.eslintignore index 8d4c64d960..f2c53c2fdc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,6 +6,7 @@ packages/server/coverage packages/worker/coverage packages/backend-core/coverage packages/server/client +packages/server/coverage packages/builder/.routify packages/sdk/sdk packages/account-portal/packages/server/build diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 3060660d47..5c474aa826 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -107,9 +107,9 @@ jobs: - name: Test run: | if ${{ env.USE_NX_AFFECTED }}; then - yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }} + yarn test --ignore=@budibase/worker --ignore=@budibase/server --since=${{ env.NX_BASE_BRANCH }} else - yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro + yarn test --ignore=@budibase/worker --ignore=@budibase/server fi test-worker: @@ -160,31 +160,6 @@ jobs: yarn test --scope=@budibase/server fi - test-pro: - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' - steps: - - name: Checkout repo and submodules - uses: actions/checkout@v4 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - fetch-depth: 0 - - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - cache: yarn - - run: yarn --frozen-lockfile - - name: Test - run: | - if ${{ env.USE_NX_AFFECTED }}; then - yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }} - else - yarn test --scope=@budibase/pro - fi - integration-test: runs-on: ubuntu-latest steps: diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index 177b0a129c..202e52e70e 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -7,11 +7,12 @@ module.exports = { if ( /^@budibase\/[^/]+\/.*$/.test(importPath) && - importPath !== "@budibase/backend-core/tests" + importPath !== "@budibase/backend-core/tests" && + importPath !== "@budibase/string-templates/test/utils" ) { context.report({ node, - message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`, + message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`, }) } }, diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index ee98b0729d..be01056b53 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -12,8 +12,6 @@ COPY .yarnrc . COPY packages/server/package.json packages/server/package.json COPY packages/worker/package.json packages/worker/package.json -# string-templates does not get bundled during the esbuild process, so we want to use the local version -COPY packages/string-templates/package.json packages/string-templates/package.json COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh @@ -26,7 +24,7 @@ RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json RUN echo '' > scripts/syncProPackage.js RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json RUN ./scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile # copy the actual code COPY packages/server/dist packages/server/dist @@ -35,7 +33,6 @@ COPY packages/server/client packages/server/client COPY packages/server/builder packages/server/builder COPY packages/worker/dist packages/worker/dist COPY packages/worker/pm2.config.js packages/worker/pm2.config.js -COPY packages/string-templates packages/string-templates FROM budibase/couchdb:v3.3.3 as runner @@ -52,11 +49,11 @@ RUN apt-get update && \ # Install postgres client for pg_dump utils RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \ - && curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \ - && echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \ - && apt update -y \ - && apt install postgresql-client-15 -y \ - && apt remove software-properties-common apt-transport-https gpg -y + && curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \ + && echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \ + && apt update -y \ + && apt install postgresql-client-15 -y \ + && apt remove software-properties-common apt-transport-https gpg -y # We use pm2 in order to run multiple node processes in a single container RUN npm install --global pm2 @@ -100,9 +97,6 @@ COPY --from=build /app/node_modules /node_modules COPY --from=build /app/package.json /package.json COPY --from=build /app/packages/server /app COPY --from=build /app/packages/worker /worker -COPY --from=build /app/packages/string-templates /string-templates - -RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates EXPOSE 80 diff --git a/lerna.json b/lerna.json index 0f6121bb18..d191854fac 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.21.9", + "version": "2.22.1", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 0c050591c2..23a1219732 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 0c050591c21d3b67dc0c9225d60cc9e2324c8dac +Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index ae86695168..6cea7efeba 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -10,7 +10,7 @@ import { StaticDatabases, DEFAULT_TENANT_ID, } from "../constants" -import { Database, IdentityContext } from "@budibase/types" +import { Database, IdentityContext, Snippet, App } from "@budibase/types" import { ContextMap } from "./types" let TEST_APP_ID: string | null = null @@ -122,10 +122,10 @@ export async function doInAutomationContext(params: { automationId: string task: () => T }): Promise { - const tenantId = getTenantIDFromAppID(params.appId) + await ensureSnippetContext() return newContext( { - tenantId, + tenantId: getTenantIDFromAppID(params.appId), appId: params.appId, automationId: params.automationId, }, @@ -281,6 +281,27 @@ export function doInScimContext(task: any) { return newContext(updates, task) } +export async function ensureSnippetContext() { + const ctx = getCurrentContext() + + // If we've already added snippets to context, continue + if (!ctx || ctx.snippets) { + return + } + + // Otherwise get snippets for this app and update context + let snippets: Snippet[] | undefined + const db = getAppDB() + if (db && !env.isTest()) { + const app = await db.get(DocumentType.APP_METADATA) + snippets = app.snippets + } + + // Always set snippets to a non-null value so that we can tell we've attempted + // to load snippets + ctx.snippets = snippets || [] +} + export function getEnvironmentVariables() { const context = Context.get() if (!context.environmentVariables) { diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 8ea544a53c..f297d3089f 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -1,4 +1,4 @@ -import { IdentityContext, VM } from "@budibase/types" +import { IdentityContext, Snippet, VM } from "@budibase/types" // keep this out of Budibase types, don't want to expose context info export type ContextMap = { @@ -11,4 +11,5 @@ export type ContextMap = { isMigrating?: boolean vm?: VM cleanup?: (() => void | Promise)[] + snippets?: Snippet[] } diff --git a/packages/backend-core/src/events/publishers/app.ts b/packages/backend-core/src/events/publishers/app.ts index d08d59b5f1..af26b09e72 100644 --- a/packages/backend-core/src/events/publishers/app.ts +++ b/packages/backend-core/src/events/publishers/app.ts @@ -13,6 +13,7 @@ import { AppVersionRevertedEvent, AppRevertedEvent, AppExportedEvent, + AppDuplicatedEvent, } from "@budibase/types" const created = async (app: App, timestamp?: string | number) => { @@ -77,6 +78,17 @@ async function fileImported(app: App) { await publishEvent(Event.APP_FILE_IMPORTED, properties) } +async function duplicated(app: App, duplicateAppId: string) { + const properties: AppDuplicatedEvent = { + duplicateAppId, + appId: app.appId, + audited: { + name: app.name, + }, + } + await publishEvent(Event.APP_DUPLICATED, properties) +} + async function templateImported(app: App, templateKey: string) { const properties: AppTemplateImportedEvent = { appId: app.appId, @@ -147,6 +159,7 @@ export default { published, unpublished, fileImported, + duplicated, templateImported, versionUpdated, versionReverted, diff --git a/packages/backend-core/tests/core/utilities/mocks/events.ts b/packages/backend-core/tests/core/utilities/mocks/events.ts index fef730768a..96f351de10 100644 --- a/packages/backend-core/tests/core/utilities/mocks/events.ts +++ b/packages/backend-core/tests/core/utilities/mocks/events.ts @@ -15,6 +15,7 @@ beforeAll(async () => { jest.spyOn(events.app, "created") jest.spyOn(events.app, "updated") + jest.spyOn(events.app, "duplicated") jest.spyOn(events.app, "deleted") jest.spyOn(events.app, "published") jest.spyOn(events.app, "unpublished") diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte index 642ec4932a..c55d1cb43d 100644 --- a/packages/bbui/src/ActionMenu/ActionMenu.svelte +++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte @@ -38,7 +38,7 @@
- + diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 62416ae88d..12c4c4d002 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -33,8 +33,8 @@ const handleClick = event => { } // Ignore clicks for drawers, unless the handler is registered from a drawer - const sourceInDrawer = handler.anchor.closest(".drawer-container") != null - const clickInDrawer = event.target.closest(".drawer-container") != null + const sourceInDrawer = handler.anchor.closest(".drawer-wrapper") != null + const clickInDrawer = event.target.closest(".drawer-wrapper") != null if (clickInDrawer && !sourceInDrawer) { return } diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 757f86b471..89ee92726d 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -57,8 +57,10 @@ - - - -
(showTooltip = true)} - on:focus={() => (showTooltip = true)} - on:mouseleave={() => (showTooltip = false)} - on:click={() => (showTooltip = false)} + - - - - {#if tooltip && showTooltip} -
- -
- {/if} -
+
+ + + +
+ diff --git a/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte b/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte new file mode 100644 index 0000000000..c68699fc0f --- /dev/null +++ b/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte @@ -0,0 +1,278 @@ + + + + +
+ +
+ {#if enableSnippets} + {#if searching} +
+ +
+ + {:else} +
Snippets
+ + + {/if} + {:else} +
+ Snippets + + Premium + +
+ {/if} +
+
+ {#if enableSnippets && filteredSnippets?.length} + {#each filteredSnippets as snippet} +
showSnippet(snippet, e.target)} + on:mouseleave={hidePopover} + on:click={() => addSnippet(snippet)} + > + {snippet.name} + editSnippet(e, snippet)} + /> +
+ {/each} + {:else} +
+ + Snippets let you create reusable JS functions and values that can + all be managed in one place + + {#if enableSnippets} + + {:else} + + {/if} +
+ {/if} +
+
+
+ + +
+ {#key hoveredSnippet} + + {/key} +
+
+ + + + diff --git a/packages/builder/src/components/common/bindings/utils.js b/packages/builder/src/components/common/bindings/utils.js index a086cd0394..c60374f0f7 100644 --- a/packages/builder/src/components/common/bindings/utils.js +++ b/packages/builder/src/components/common/bindings/utils.js @@ -38,4 +38,11 @@ export class BindingHelpers { this.insertAtPos({ start, end, value: insertVal }) } } + + // Adds a snippet to the expression + onSelectSnippet(snippet) { + const pos = this.getCaretPosition() + const { start, end } = pos + this.insertAtPos({ start, end, value: `snippets.${snippet.name}` }) + } } diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index 9e5de1d3bf..de8b45c98b 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -19,7 +19,7 @@ import ConfirmDialog from "components/common/ConfirmDialog.svelte" import analytics, { Events, EventSource } from "analytics" import { API } from "api" - import { apps } from "stores/portal" + import { appsStore } from "stores/portal" import { previewStore, builderStore, @@ -45,7 +45,7 @@ let appActionPopoverAnchor let publishing = false - $: filteredApps = $apps.filter(app => app.devId === application) + $: filteredApps = $appsStore.apps.filter(app => app.devId === application) $: selectedApp = filteredApps?.length ? filteredApps[0] : null $: latestDeployments = $deploymentStore .filter(deployment => deployment.status === "SUCCESS") @@ -129,7 +129,7 @@ } try { await API.unpublishApp(selectedApp.prodId) - await apps.load() + await appsStore.load() notifications.send("App unpublished", { type: "success", icon: "GlobeStrike", @@ -141,7 +141,7 @@ const completePublish = async () => { try { - await apps.load() + await appsStore.load() await deploymentStore.load() } catch (err) { notifications.error("Error refreshing app") diff --git a/packages/builder/src/components/deploy/DeleteModal.svelte b/packages/builder/src/components/deploy/DeleteModal.svelte index 855f6a0757..75204b55ce 100644 --- a/packages/builder/src/components/deploy/DeleteModal.svelte +++ b/packages/builder/src/components/deploy/DeleteModal.svelte @@ -2,10 +2,17 @@ import { Input, notifications } from "@budibase/bbui" import { goto } from "@roxi/routify" import ConfirmDialog from "components/common/ConfirmDialog.svelte" - import { apps } from "stores/portal" - import { appStore } from "stores/builder" + import { appsStore } from "stores/portal" import { API } from "api" + export let appId + export let appName + export let onDeleteSuccess = () => { + $goto("/builder") + } + + let deleting = false + export const show = () => { deletionModal.show() } @@ -17,32 +24,52 @@ let deletionModal let deletionConfirmationAppName + const copyName = () => { + deletionConfirmationAppName = appName + } + const deleteApp = async () => { + if (!appId) { + console.error("No app id provided") + return + } + deleting = true try { - await API.deleteApp($appStore.appId) - apps.load() + await API.deleteApp(appId) + appsStore.load() notifications.success("App deleted successfully") - $goto("/builder") + onDeleteSuccess() } catch (err) { notifications.error("Error deleting app") + deleting = false } } + (deletionConfirmationAppName = null)} - disabled={deletionConfirmationAppName !== $appStore.name} + disabled={deletionConfirmationAppName !== appName || deleting} > - Are you sure you want to delete {$appStore.name}? + Are you sure you want to delete + + {appName} + ? +
Please enter the app name below to confirm.

- +
+ + diff --git a/packages/builder/src/components/portal/licensing/AppLimitModal.svelte b/packages/builder/src/components/portal/licensing/AppLimitModal.svelte index 39f553517e..bdecbcab3d 100644 --- a/packages/builder/src/components/portal/licensing/AppLimitModal.svelte +++ b/packages/builder/src/components/portal/licensing/AppLimitModal.svelte @@ -31,17 +31,11 @@ : null} > - You are currently on our Free plan. Upgrade - to our Pro plan to get unlimited apps and additional features. + You have exceeded the app limit for your current plan. Upgrade to get + unlimited apps and additional features! {#if !$auth.user.accountPortalAccess} Please contact the account holder to upgrade. {/if} - - diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte index 32bddd58a6..6dd4030f3a 100644 --- a/packages/builder/src/components/start/AppRow.svelte +++ b/packages/builder/src/components/start/AppRow.svelte @@ -5,10 +5,14 @@ import { goto } from "@roxi/routify" import { UserAvatars } from "@budibase/frontend-core" import { sdk } from "@budibase/shared-core" + import AppRowContext from "./AppRowContext.svelte" + import FavouriteAppButton from "pages/builder/portal/apps/FavouriteAppButton.svelte" export let app export let lockedAction + let actionsOpen = false + $: editing = app.sessions?.length $: isBuilder = sdk.users.isBuilder($auth.user, app?.devId) $: unclickable = !isBuilder && !app.deployed @@ -42,8 +46,10 @@
@@ -74,21 +80,35 @@ {app.deployed ? "Published" : "Unpublished"}
- {#if isBuilder} +
- - + {#if isBuilder} +
+ +
+
+ { + actionsOpen = true + }} + on:close={() => { + actionsOpen = false + }} + /> +
+ {:else} + + + {/if}
- {:else if app.deployed} - -
- + +
+
- {/if} +
diff --git a/packages/builder/src/components/start/ExportAppModal.svelte b/packages/builder/src/components/start/ExportAppModal.svelte index 734e4448a1..ec0cf42fe0 100644 --- a/packages/builder/src/components/start/ExportAppModal.svelte +++ b/packages/builder/src/components/start/ExportAppModal.svelte @@ -121,6 +121,7 @@ { - const applications = svelteGet(apps) + const applications = svelteGet(appsStore).apps appValidation.name(validation, { apps: applications, currentApp: { @@ -62,7 +62,7 @@ async function updateApp() { try { - await apps.update(app.appId, { + await appsStore.save(app.appId, { name: $values.name?.trim(), url: $values.url?.trim(), icon: { diff --git a/packages/builder/src/global.css b/packages/builder/src/global.css index bc1f55d9d3..adf4a47070 100644 --- a/packages/builder/src/global.css +++ b/packages/builder/src/global.css @@ -22,6 +22,7 @@ body { --grey-7: var(--spectrum-global-color-gray-700); --grey-8: var(--spectrum-global-color-gray-800); --grey-9: var(--spectrum-global-color-gray-900); + --spectrum-global-color-yellow-1000: #d8b500; color: var(--ink); background-color: var(--background-alt); diff --git a/packages/builder/src/helpers/duplicate.js b/packages/builder/src/helpers/duplicate.js index 1547fcd4d1..c2a924f97b 100644 --- a/packages/builder/src/helpers/duplicate.js +++ b/packages/builder/src/helpers/duplicate.js @@ -48,3 +48,53 @@ export const duplicateName = (name, allNames) => { return `${baseName} ${number}` } + +/** + * More flexible alternative to the above function, which handles getting the + * next sequential name from an array of existing items while accounting for + * any type of prefix, and being able to deeply retrieve that name from the + * existing item array. + * + * Examples with a prefix of "foo": + * [] => "foo" + * ["foo"] => "foo2" + * ["foo", "foo6"] => "foo7" + * + * Examples with a prefix of "foo " (space at the end): + * [] => "foo" + * ["foo"] => "foo 2" + * ["foo", "foo 6"] => "foo 7" + * + * @param items the array of existing items + * @param prefix the string prefix of each name, including any spaces desired + * @param getName optional function to extract the name for an item, if not a + * flat array of strings + */ +export const getSequentialName = (items, prefix, getName = x => x) => { + if (!prefix?.length || !getName) { + return null + } + const trimmedPrefix = prefix.trim() + if (!items?.length) { + return trimmedPrefix + } + let max = 0 + items.forEach(item => { + const name = getName(item) + if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) { + return + } + const split = name.split(trimmedPrefix) + if (split.length !== 2) { + return + } + if (split[1].trim() === "") { + split[1] = "1" + } + const num = parseInt(split[1]) + if (num > max) { + max = num + } + }) + return max === 0 ? trimmedPrefix : `${prefix}${max + 1}` +} diff --git a/packages/builder/src/helpers/tests/duplicate.test.js b/packages/builder/src/helpers/tests/duplicate.test.js index 400abed0aa..7e51c5ff2a 100644 --- a/packages/builder/src/helpers/tests/duplicate.test.js +++ b/packages/builder/src/helpers/tests/duplicate.test.js @@ -1,5 +1,5 @@ import { expect, describe, it } from "vitest" -import { duplicateName } from "../duplicate" +import { duplicateName, getSequentialName } from "../duplicate" describe("duplicate", () => { describe("duplicates a name ", () => { @@ -40,3 +40,64 @@ describe("duplicate", () => { }) }) }) + +describe("getSequentialName", () => { + it("handles nullish items", async () => { + const name = getSequentialName(null, "foo", () => {}) + expect(name).toBe("foo") + }) + + it("handles nullish prefix", async () => { + const name = getSequentialName([], null, () => {}) + expect(name).toBe(null) + }) + + it("handles nullish getName function", async () => { + const name = getSequentialName([], "foo", null) + expect(name).toBe(null) + }) + + it("handles just the prefix", async () => { + const name = getSequentialName(["foo"], "foo", x => x) + expect(name).toBe("foo2") + }) + + it("handles continuous ranges", async () => { + const name = getSequentialName(["foo", "foo2", "foo3"], "foo", x => x) + expect(name).toBe("foo4") + }) + + it("handles discontinuous ranges", async () => { + const name = getSequentialName(["foo", "foo3"], "foo", x => x) + expect(name).toBe("foo4") + }) + + it("handles a space inside the prefix", async () => { + const name = getSequentialName(["foo", "foo 2", "foo 3"], "foo ", x => x) + expect(name).toBe("foo 4") + }) + + it("handles a space inside the prefix with just the prefix", async () => { + const name = getSequentialName(["foo"], "foo ", x => x) + expect(name).toBe("foo 2") + }) + + it("handles no matches", async () => { + const name = getSequentialName(["aaa", "bbb"], "foo", x => x) + expect(name).toBe("foo") + }) + + it("handles similar names", async () => { + const name = getSequentialName( + ["fooo1", "2foo", "a3foo4", "5foo5"], + "foo", + x => x + ) + expect(name).toBe("foo") + }) + + it("handles non-string names", async () => { + const name = getSequentialName([null, 4123, [], {}], "foo", x => x) + expect(name).toBe("foo") + }) +}) diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 7c1cc583e1..d714bafc70 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -15,7 +15,14 @@ FancySelect, } from "@budibase/bbui" import { builderStore, appStore, roles } from "stores/builder" - import { groups, licensing, apps, users, auth, admin } from "stores/portal" + import { + groups, + licensing, + appsStore, + users, + auth, + admin, + } from "stores/portal" import { fetchData, Constants, @@ -54,7 +61,7 @@ let inviteFailureResponse = "" $: validEmail = emailValidator(email) === true - $: prodAppId = apps.getProdAppID($appStore.appId) + $: prodAppId = appsStore.getProdAppID($appStore.appId) $: promptInvite = showInvite( filteredInvites, filteredUsers, diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 45b10d3d9e..fd6a97560d 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -8,7 +8,7 @@ userStore, deploymentStore, } from "stores/builder" - import { auth, apps } from "stores/portal" + import { auth, appsStore } from "stores/portal" import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags" import { Icon, @@ -52,7 +52,7 @@ const pkg = await API.fetchAppPackage(application) await initialise(pkg) - await apps.load() + await appsStore.load() await deploymentStore.load() loaded = true diff --git a/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte index c4ee060149..57180625b1 100644 --- a/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte @@ -40,7 +40,7 @@
-
+
{#if $automationStore.automations?.length} {:else} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index a910036a4a..95e7a66be9 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -10,6 +10,7 @@ navigationStore, selectedScreen, hoverStore, + snippets, } from "stores/builder" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { @@ -68,6 +69,7 @@ hostname: window.location.hostname, port: window.location.port, }, + snippets: $snippets, } // Refresh the preview when required diff --git a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte index da4f743f04..67befddcb9 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte @@ -3,7 +3,7 @@ import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui" import { url, isActive } from "@roxi/routify" import DeleteModal from "components/deploy/DeleteModal.svelte" - import { isOnlyUser } from "stores/builder" + import { isOnlyUser, appStore } from "stores/builder" let deleteModal @@ -67,7 +67,11 @@
- + diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte index 8810edca9c..00719dc6d5 100644 --- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte @@ -2,7 +2,7 @@ import { notifications } from "@budibase/bbui" import { admin, - apps, + appsStore, templates, licensing, groups, @@ -14,7 +14,7 @@ import PortalSideBar from "./_components/PortalSideBar.svelte" // Don't block loading if we've already hydrated state - let loaded = !!$apps?.length + let loaded = !!$appsStore.apps?.length onMount(async () => { try { @@ -34,7 +34,10 @@ } // Go to new app page if no apps exists - if (!$apps.length && sdk.users.hasBuilderPermissions($auth.user)) { + if ( + !$appsStore.apps.length && + sdk.users.hasBuilderPermissions($auth.user) + ) { $redirect("./onboarding") } } catch (error) { @@ -46,7 +49,7 @@ {#if loaded}
- {#if $apps.length > 0} + {#if $appsStore.apps.length > 0} {/if} diff --git a/packages/builder/src/pages/builder/portal/apps/create.svelte b/packages/builder/src/pages/builder/portal/apps/create.svelte index 1f2c579071..1248c41cf8 100644 --- a/packages/builder/src/pages/builder/portal/apps/create.svelte +++ b/packages/builder/src/pages/builder/portal/apps/create.svelte @@ -5,7 +5,7 @@ import CreateAppModal from "components/start/CreateAppModal.svelte" import TemplateDisplay from "components/common/TemplateDisplay.svelte" import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte" - import { apps, templates, licensing } from "stores/portal" + import { appsStore, templates, licensing } from "stores/portal" import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page" let template @@ -35,7 +35,7 @@ } -{#if !$apps.length} +{#if !$appsStore.apps.length} {:else} diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index a1aa242a36..adea7600a6 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -19,13 +19,18 @@ import { automationStore, initialise } from "stores/builder" import { API } from "api" import { onMount } from "svelte" - import { apps, auth, admin, licensing, environment } from "stores/portal" + import { + appsStore, + auth, + admin, + licensing, + environment, + enrichedApps, + } from "stores/portal" import { goto } from "@roxi/routify" import AppRow from "components/start/AppRow.svelte" - import { AppStatus } from "constants" import Logo from "assets/bb-space-man.svg" - let sortBy = "name" let template let creationModal let appLimitModal @@ -33,56 +38,27 @@ let searchTerm = "" let creatingFromTemplate = false let automationErrors - let accessFilterList = null $: welcomeHeader = `Welcome ${$auth?.user?.firstName || "back"}` - $: enrichedApps = enrichApps($apps, $auth.user, sortBy) - $: filteredApps = enrichedApps.filter( - app => - (searchTerm - ? app?.name?.toLowerCase().includes(searchTerm.toLowerCase()) - : true) && - (accessFilterList !== null - ? accessFilterList?.includes( - `${app?.type}_${app?.tenantId}_${app?.appId}` - ) - : true) - ) - $: automationErrors = getAutomationErrors(enrichedApps) + $: filteredApps = filterApps($enrichedApps, searchTerm) + $: automationErrors = getAutomationErrors(filteredApps || []) $: isOwner = $auth.accountPortalAccess && $admin.cloud + const filterApps = (apps, searchTerm) => { + return apps?.filter(app => { + const query = searchTerm?.trim()?.replace(/\s/g, "") + if (query) { + return app?.name?.toLowerCase().includes(query.toLowerCase()) + } else { + return true + } + }) + } + const usersLimitLockAction = $licensing?.errUserLimit ? () => accountLockedModal.show() : null - const enrichApps = (apps, user, sortBy) => { - const enrichedApps = apps.map(app => ({ - ...app, - deployed: app.status === AppStatus.DEPLOYED, - lockedYou: app.lockedBy && app.lockedBy.email === user?.email, - lockedOther: app.lockedBy && app.lockedBy.email !== user?.email, - })) - - if (sortBy === "status") { - return enrichedApps.sort((a, b) => { - if (a.status === b.status) { - return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1 - } - return a.status === AppStatus.DEPLOYED ? -1 : 1 - }) - } else if (sortBy === "updated") { - return enrichedApps.sort((a, b) => { - const aUpdated = a.updatedAt || "9999" - const bUpdated = b.updatedAt || "9999" - return aUpdated < bUpdated ? 1 : -1 - }) - } else { - return enrichedApps.sort((a, b) => { - return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1 - }) - } - } - const getAutomationErrors = apps => { const automationErrors = {} for (let app of apps) { @@ -117,7 +93,7 @@ const initiateAppCreation = async () => { if ($licensing?.usageMetrics?.apps >= 100) { appLimitModal.show() - } else if ($apps?.length) { + } else if ($appsStore.apps?.length) { $goto("/builder/portal/apps/create") } else { template = null @@ -136,7 +112,7 @@ const templateKey = template.key.split("/")[1] let appName = templateKey.replace(/-/g, " ") - const appsWithSameName = $apps.filter(app => + const appsWithSameName = $appsStore.apps.filter(app => app.name?.startsWith(appName) ) appName = `${appName} ${appsWithSameName.length + 1}` @@ -217,7 +193,7 @@ : "View error"} on:dismiss={async () => { await automationStore.actions.clearLogErrors({ appId }) - await apps.load() + await appsStore.load() }} message={automationErrorMessage(appId)} /> @@ -233,7 +209,7 @@
- {#if enrichedApps.length} + {#if $appsStore.apps.length}
{#if $auth.user && sdk.users.canCreateApps($auth.user)} @@ -245,7 +221,7 @@ > Create new app - {#if $apps?.length > 0 && !$admin.offlineMode} + {#if $appsStore.apps?.length > 0 && !$admin.offlineMode}