diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index d6bbf19940..3060660d47 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -33,13 +33,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -50,14 +50,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} fetch-depth: 0 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -92,14 +92,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} fetch-depth: 0 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -116,14 +116,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} fetch-depth: 0 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -140,14 +140,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} fetch-depth: 0 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -165,14 +165,14 @@ jobs: 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@v3 + 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@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -189,13 +189,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: yarn @@ -219,7 +219,7 @@ jobs: if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') steps: - name: Checkout repo and submodules - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} @@ -249,7 +249,7 @@ jobs: - name: Check submodule merged to base branch if: ${{ steps.get_pro_commits.outputs.base_commit != '' }} - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -269,7 +269,7 @@ jobs: if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') steps: - name: Checkout repo and submodules - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} @@ -299,7 +299,7 @@ jobs: - name: Check submodule merged to base branch if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }} - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/close-featurebranch.yml b/.github/workflows/close-featurebranch.yml index 5da3eb52cd..0439aec443 100644 --- a/.github/workflows/close-featurebranch.yml +++ b/.github/workflows/close-featurebranch.yml @@ -17,7 +17,7 @@ jobs: github.event.label.name == 'feature-branch' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: passeidireto/trigger-external-workflow-action@main env: PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }} diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml index a5636fe912..eccc783dfb 100644 --- a/.github/workflows/deploy-featurebranch.yml +++ b/.github/workflows/deploy-featurebranch.yml @@ -17,7 +17,7 @@ jobs: contains(github.event.pull_request.labels.*.name, 'feature-branch') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: passeidireto/trigger-external-workflow-action@main env: PAYLOAD_BRANCH: ${{ github.head_ref }} diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 13d59d1019..483e895e98 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -28,7 +28,7 @@ jobs: run: | echo "Ref is not master, you must run this job from master." exit 1 - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} @@ -53,7 +53,7 @@ jobs: needs: [tag-release] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: peter-evans/repository-dispatch@v2 with: diff --git a/lerna.json b/lerna.json index 3ab56dc0b1..65f04ecf2c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.15.2", + "version": "2.15.7", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 05c90ce551..dd9cec2275 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 05c90ce55144e260da6688335c16783eab79bf96 +Subproject commit dd9cec22751405e042ba0fe58e3c05f7223c3723 diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 0fec786c31..b3179cbeea 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -179,6 +179,7 @@ const environment = { ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, OFFLINE_MODE: process.env.OFFLINE_MODE, + SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 7bf26f3688..8001017092 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -2,6 +2,7 @@ export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" +export * as userUtils from "./users/utils" export * as roles from "./security/roles" export * as permissions from "./security/permissions" export * as accounts from "./accounts" diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts index a86a829b17..8d7b43d5b6 100644 --- a/packages/backend-core/src/security/sessions.ts +++ b/packages/backend-core/src/security/sessions.ts @@ -1,8 +1,8 @@ -const redis = require("../redis/init") -const { v4: uuidv4 } = require("uuid") -const { logWarn } = require("../logging") - +import * as redis from "../redis/init" +import { v4 as uuidv4 } from "uuid" +import { logWarn } from "../logging" import env from "../environment" +import { Duration } from "../utils" import { Session, ScannedSession, @@ -10,8 +10,10 @@ import { CreateSession, } from "@budibase/types" -// a week in seconds -const EXPIRY_SECONDS = 86400 * 7 +// a week expiry is the default +const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS + ? parseInt(env.SESSION_EXPIRY_SECONDS) + : Duration.fromDays(7).toSeconds() function makeSessionID(userId: string, sessionId: string) { return `${userId}/${sessionId}` diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 4d0d216603..136cb4b8ad 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -251,7 +251,8 @@ export class UserDB { } const change = dbUser ? 0 : 1 // no change if there is existing user - const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0 + const creatorsChange = + (await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 return UserDB.quotas.addUsers(change, creatorsChange, async () => { await validateUniqueUser(email, tenantId) @@ -335,7 +336,7 @@ export class UserDB { } newUser.userGroups = groups || [] newUsers.push(newUser) - if (isCreator(newUser)) { + if (await isCreator(newUser)) { newCreators.push(newUser) } } @@ -432,12 +433,16 @@ export class UserDB { _deleted: true, })) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) - const creatorsToDelete = usersToDelete.filter(isCreator) + + const creatorsEval = await Promise.all(usersToDelete.map(isCreator)) + const creatorsToDeleteCount = creatorsEval.filter( + creator => !!creator + ).length for (let user of usersToDelete) { await bulkDeleteProcessing(user) } - await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length) + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) // Build Response // index users by id @@ -486,7 +491,7 @@ export class UserDB { await db.remove(userId, dbUser._rev) - const creatorsToDelete = isCreator(dbUser) ? 1 : 0 + const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 await UserDB.quotas.removeUsers(1, creatorsToDelete) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) diff --git a/packages/backend-core/src/users/test/utils.spec.ts b/packages/backend-core/src/users/test/utils.spec.ts new file mode 100644 index 0000000000..0fe27f57a6 --- /dev/null +++ b/packages/backend-core/src/users/test/utils.spec.ts @@ -0,0 +1,67 @@ +import { User, UserGroup } from "@budibase/types" +import { generator, structures } from "../../../tests" +import { DBTestConfiguration } from "../../../tests/extra" +import { getGlobalDB } from "../../context" +import { isCreator } from "../utils" + +const config = new DBTestConfiguration() + +describe("Users", () => { + it("User is a creator if it is configured as a global builder", async () => { + const user: User = structures.users.user({ builder: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured as a global admin", async () => { + const user: User = structures.users.user({ admin: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured with creator permission", async () => { + const user: User = structures.users.user({ builder: { creator: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is a builder in some application", async () => { + const user: User = structures.users.user({ builder: { apps: ["app1"] } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has CREATOR permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "CREATOR" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has ADMIN permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "ADMIN" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it remains to a group with ADMIN permissions", async () => { + const usersInGroup = 10 + const groupId = "gr_17abffe89e0b40268e755b952f101a59" + const group: UserGroup = { + ...structures.userGroups.userGroup(), + ...{ _id: groupId, roles: { app1: "ADMIN" } }, + } + const users: User[] = [] + for (const _ of Array.from({ length: usersInGroup })) { + const userId = `us_${generator.guid()}` + const user: User = structures.users.user({ + _id: userId, + userGroups: [groupId], + }) + users.push(user) + } + + await config.doInTenant(async () => { + const db = getGlobalDB() + await db.put(group) + for (let user of users) { + await db.put(user) + const creator = await isCreator(user) + expect(creator).toBe(true) + } + }) + }) +}) diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index cc2b4fc27f..638da4a5b1 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -309,7 +309,8 @@ export async function getCreatorCount() { let creators = 0 async function iterate(startPage?: string) { const page = await paginatedUsers({ bookmark: startPage }) - creators += page.data.filter(isCreator).length + const creatorsEval = await Promise.all(page.data.map(isCreator)) + creators += creatorsEval.filter(creator => !!creator).length if (page.hasNextPage) { await iterate(page.nextPage) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 0ef4b77998..348ad1532f 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,4 +1,4 @@ -import { CloudAccount } from "@budibase/types" +import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" import { getPlatformUser } from "./lookup" @@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors" import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" import { getAccountByTenantId } from "../accounts" +import { BUILTIN_ROLE_IDS } from "../security/roles" +import * as context from "../context" // extract from shared-core to make easily accessible from backend-core export const isBuilder = sdk.users.isBuilder export const isAdmin = sdk.users.isAdmin -export const isCreator = sdk.users.isCreator export const isGlobalBuilder = sdk.users.isGlobalBuilder export const isAdminOrBuilder = sdk.users.isAdminOrBuilder export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions +export async function isCreator(user?: User | ContextUser) { + const isCreatorByUserDefinition = sdk.users.isCreator(user) + if (!isCreatorByUserDefinition && user) { + return await isCreatorByGroupMembership(user) + } + return isCreatorByUserDefinition +} + +async function isCreatorByGroupMembership(user?: User | ContextUser) { + const userGroups = user?.userGroups || [] + if (userGroups.length > 0) { + const db = context.getGlobalDB() + const groups: UserGroup[] = [] + for (let groupId of userGroups) { + try { + const group = await db.get(groupId) + groups.push(group) + } catch (e: any) { + if (e.error !== "not_found") { + throw e + } + } + } + return groups.some(group => + Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN) + ) + } + return false +} + export async function validateUniqueUser(email: string, tenantId: string) { // check budibase users in other tenants if (env.MULTI_TENANCY) { diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index f2018272f6..cc169eac09 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) { useAnchorWidth, offset = 5, customUpdate, - offsetBelow, } = opts if (!anchor) { return @@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) { styles.top = anchorBounds.top - elementBounds.height - offset styles.maxHeight = maxHeight || 240 } else { - styles.top = anchorBounds.bottom + (offsetBelow || offset) + styles.top = anchorBounds.bottom + offset styles.maxHeight = maxHeight || window.innerHeight - anchorBounds.bottom - 20 } diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index d5d6515d2d..2243570cd5 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -15,8 +15,6 @@ export let autoWidth = false export let searchTerm = null export let customPopoverHeight - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let open = false export let loading @@ -98,7 +96,5 @@ {sort} {autoWidth} {customPopoverHeight} - {customPopoverOffsetBelow} - {customPopoverMaxHeight} {loading} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 94fbe73cf2..cfb1654403 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -37,8 +37,6 @@ export let sort = false export let searchTerm = null export let customPopoverHeight - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let align = "left" export let footer = null export let customAnchor = null @@ -156,9 +154,7 @@ on:close={() => (open = false)} useAnchorWidth={!autoWidth} maxWidth={autoWidth ? 400 : null} - maxHeight={customPopoverMaxHeight} customHeight={customPopoverHeight} - offsetBelow={customPopoverOffsetBelow} >
null export let getOptionColour = () => null export let getOptionSubtitle = () => null + export let compare = null export let useOptionIconImage = false export let isOptionEnabled export let readonly = false @@ -23,8 +24,6 @@ export let footer = null export let open = false export let tag = null - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let searchTerm = null export let loading @@ -34,13 +33,19 @@ $: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldColour = getFieldAttribute(getOptionColour, value, options) + function compareOptionAndValue(option, value) { + return typeof compare === "function" + ? compare(option, value) + : option === value + } + const getFieldAttribute = (getAttribute, value, options) => { // Wait for options to load if there is a value but no options if (!options?.length) { return "" } - const index = options.findIndex( - (option, idx) => getOptionValue(option, idx) === value + const index = options.findIndex((option, idx) => + compareOptionAndValue(getOptionValue(option, idx), value) ) return index !== -1 ? getAttribute(options[index], index) : null } @@ -90,11 +95,9 @@ {autocomplete} {sort} {tag} - {customPopoverOffsetBelow} - {customPopoverMaxHeight} isPlaceholder={value == null || value === ""} placeholderOption={placeholder === false ? null : placeholder} - isOptionSelected={option => option === value} + isOptionSelected={option => compareOptionAndValue(option, value)} onSelectOption={selectOption} {loading} /> diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index 8235b68faf..2119a37980 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -28,6 +28,7 @@ export let footer = null export let tag = null export let helpText = null + export let compare const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -65,6 +66,7 @@ {autocomplete} {customPopoverHeight} {tag} + {compare} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index a68430e973..5066e3aa05 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -18,7 +18,6 @@ export let useAnchorWidth = false export let dismissible = true export let offset = 5 - export let offsetBelow export let customHeight export let animate = true export let customZindex @@ -89,7 +88,6 @@ maxWidth, useAnchorWidth, offset, - offsetBelow, customUpdate: handlePostionUpdate, }} use:clickOutside={{ diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 52368a0723..b03065f153 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -1035,11 +1035,48 @@ export const getAllStateVariables = () => { getAllAssets().forEach(asset => { findAllMatchingComponents(asset.props, component => { const settings = getComponentSettings(component._component) - settings - .filter(setting => setting.type === "event") - .forEach(setting => { - eventSettings.push(component[setting.key]) - }) + + const parseEventSettings = (settings, comp) => { + settings + .filter(setting => setting.type === "event") + .forEach(setting => { + eventSettings.push(comp[setting.key]) + }) + } + + const parseComponentSettings = (settings, component) => { + // Parse the nested button configurations + settings + .filter(setting => setting.type === "buttonConfiguration") + .forEach(setting => { + const buttonConfig = component[setting.key] + + if (Array.isArray(buttonConfig)) { + buttonConfig.forEach(button => { + const nestedSettings = getComponentSettings(button._component) + parseEventSettings(nestedSettings, button) + }) + } + }) + + parseEventSettings(settings, component) + } + + // Parse the base component settings + parseComponentSettings(settings, component) + + // Parse step configuration + const stepSetting = settings.find( + setting => setting.type === "stepConfiguration" + ) + const steps = stepSetting ? component[stepSetting.key] : [] + const stepDefinition = getComponentSettings( + "@budibase/standard-components/multistepformblockstep" + ) + + steps.forEach(step => { + parseComponentSettings(stepDefinition, step) + }) }) }) diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index dd54dcf13e..b58d196024 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" import { createHistoryStore } from "builderStore/store/history" import { cloneDeep } from "lodash/fp" +import { getHoverStore } from "./store/hover" export const store = getFrontendStore() export const automationStore = getAutomationStore() @@ -16,6 +17,7 @@ export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() export const userStore = getUserStore() export const deploymentStore = getDeploymentStore() +export const hoverStore = getHoverStore() // Setup history for screens export const screenHistoryStore = createHistoryStore({ diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index b05b127b1c..ff7c0d74b8 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = { // Onboarding onboarding: false, tourNodes: null, - - // UI state - hoveredComponentId: null, } export const getFrontendStore = () => { @@ -1415,18 +1412,6 @@ export const getFrontendStore = () => { return state }) }, - hover: (componentId, notifyClient = true) => { - if (componentId === get(store).hoveredComponentId) { - return - } - store.update(state => { - state.hoveredComponentId = componentId - return state - }) - if (notifyClient) { - store.actions.preview.sendEvent("hover-component", componentId) - } - }, }, links: { save: async (url, title) => { diff --git a/packages/builder/src/builderStore/store/hover.js b/packages/builder/src/builderStore/store/hover.js new file mode 100644 index 0000000000..5db9272975 --- /dev/null +++ b/packages/builder/src/builderStore/store/hover.js @@ -0,0 +1,27 @@ +import { get, writable } from "svelte/store" +import { store as builder } from "builderStore" + +export const getHoverStore = () => { + const initialValue = { + componentId: null, + } + + const store = writable(initialValue) + + const update = (componentId, notifyClient = true) => { + if (componentId === get(store).componentId) { + return + } + store.update(state => { + state.componentId = componentId + return state + }) + if (notifyClient) { + builder.actions.preview.sendEvent("hover-component", componentId) + } + } + return { + subscribe: store.subscribe, + actions: { update }, + } +} diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index a5a3165aeb..af54e4d2da 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -184,8 +184,9 @@ } if ( - (idx === 0 && automation.trigger?.event === "row:update") || - automation.trigger?.event === "row:save" + idx === 0 && + (automation.trigger?.event === "row:update" || + automation.trigger?.event === "row:save") ) { if (name !== "id" && name !== "revision") return `trigger.row.${name}` } diff --git a/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte b/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte index 161757c570..09553c1bcd 100644 --- a/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte @@ -13,6 +13,7 @@ Icon, } from "@budibase/bbui" import { capitalise } from "helpers" + import { getFormattedPlanName } from "helpers/planTitle" import { get } from "svelte/store" export let resourceId @@ -99,7 +100,9 @@ {#if requiresPlanToModify} - {capitalise(requiresPlanToModify)} + {getFormattedPlanName(requiresPlanToModify)} {/if} diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index c837247986..f9b688210a 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -88,8 +88,12 @@ hasValidated = false }) } + $: valid = - getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType) + getErrorCount(errors) === 0 && + allRequiredAttributesSet(relationshipType) && + fromId && + toId $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE || diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte index 63bfecf386..fade2db761 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte @@ -5,6 +5,7 @@ import { store } from "builderStore" import { Helpers } from "@budibase/bbui" import { getEventContextBindings } from "builderStore/dataBinding" + import { cloneDeep, isEqual } from "lodash/fp" export let componentInstance export let componentBindings @@ -17,8 +18,13 @@ const dispatch = createEventDispatcher() let focusItem + let cachedValue - $: buttonList = sanitizeValue(value) || [] + $: if (!isEqual(value, cachedValue)) { + cachedValue = cloneDeep(value) + } + + $: buttonList = sanitizeValue(cachedValue) || [] $: buttonCount = buttonList.length $: eventContextBindings = getEventContextBindings({ componentInstance, diff --git a/packages/builder/src/components/integration/KeyValueBuilder.svelte b/packages/builder/src/components/integration/KeyValueBuilder.svelte index 096d5c0f71..8dac07bcec 100644 --- a/packages/builder/src/components/integration/KeyValueBuilder.svelte +++ b/packages/builder/src/components/integration/KeyValueBuilder.svelte @@ -35,6 +35,7 @@ export let bindingDrawerLeft export let allowHelpers = true export let customButtonText = null + export let compare = (option, value) => option === value let fields = Object.entries(object || {}).map(([name, value]) => ({ name, @@ -112,7 +113,12 @@ on:blur={changed} /> {#if options} - {:else if bindings && bindings.length} import KeyValueBuilder from "../KeyValueBuilder.svelte" - import { SchemaTypeOptions } from "constants/backend" + import { SchemaTypeOptionsExpanded } from "constants/backend" export let schema export let onSchemaChange = () => {} @@ -24,6 +24,7 @@ object={schema} name="field" headings - options={SchemaTypeOptions} + options={SchemaTypeOptionsExpanded} + compare={(option, value) => option.type === value.type} /> {/key} diff --git a/packages/builder/src/components/integration/RestQueryViewer.svelte b/packages/builder/src/components/integration/RestQueryViewer.svelte index 64834c1b0b..d6a8fe6fc3 100644 --- a/packages/builder/src/components/integration/RestQueryViewer.svelte +++ b/packages/builder/src/components/integration/RestQueryViewer.svelte @@ -33,7 +33,7 @@ PaginationTypes, RawRestBodyTypes, RestBodyTypes as bodyTypes, - SchemaTypeOptions, + SchemaTypeOptionsExpanded, } from "constants/backend" import JSONPreview from "components/integration/JSONPreview.svelte" import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" @@ -97,9 +97,7 @@ $: schemaReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) - $: hasSchema = - Object.keys(schema || {}).length !== 0 || - Object.keys(query?.schema || {}).length !== 0 + $: hasSchema = Object.keys(schema || {}).length !== 0 $: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs) @@ -161,7 +159,7 @@ newQuery.fields.queryString = queryString newQuery.fields.authConfigId = authConfigId newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders) - newQuery.schema = restUtils.fieldsToSchema(schema) + newQuery.schema = schema return newQuery } @@ -231,6 +229,14 @@ notifications.info("Request did not return any data") } else { response.info = response.info || { code: 200 } + // if existing schema, copy over what it is + if (schema) { + for (let [name, field] of Object.entries(schema)) { + if (response.schema[name]) { + response.schema[name] = field + } + } + } schema = response.schema notifications.success("Request sent successfully") } @@ -386,6 +392,7 @@ onMount(async () => { query = getSelectedQuery() + schema = query.schema try { // Clear any unsaved changes to the datasource @@ -416,7 +423,6 @@ query.fields.path = `${datasource.config.url}/${path ? path : ""}` } url = buildUrl(query.fields.path, breakQs) - schema = restUtils.schemaToFields(query.schema) requestBindings = restUtils.queryParametersToKeyValue(query.parameters) authConfigId = getAuthConfigId() if (!query.fields.disabledHeaders) { @@ -682,10 +688,11 @@ bind:object={schema} name="schema" headings - options={SchemaTypeOptions} + options={SchemaTypeOptionsExpanded} menuItems={schemaMenuItems} showMenu={!schemaReadOnly} readOnly={schemaReadOnly} + compare={(option, value) => option.type === value.type} /> {/if} diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index ac4079b69e..eb47ac97fe 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -271,6 +271,11 @@ export const SchemaTypeOptions = [ { label: "Datetime", value: "datetime" }, ] +export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({ + ...el, + value: { type: el.value }, +})) + export const RawRestBodyTypes = { NONE: "none", FORM: "form", diff --git a/packages/builder/src/helpers/data/utils.js b/packages/builder/src/helpers/data/utils.js index d1ff4c5f80..a29ce8db6d 100644 --- a/packages/builder/src/helpers/data/utils.js +++ b/packages/builder/src/helpers/data/utils.js @@ -1,26 +1,6 @@ import { IntegrationTypes } from "constants/backend" import { findHBSBlocks } from "@budibase/string-templates" -export function schemaToFields(schema) { - const response = {} - if (schema && typeof schema === "object") { - for (let [field, value] of Object.entries(schema)) { - response[field] = value?.type || "string" - } - } - return response -} - -export function fieldsToSchema(fields) { - const response = {} - if (fields && typeof fields === "object") { - for (let [name, type] of Object.entries(fields)) { - response[name] = { name, type } - } - } - return response -} - export function breakQueryString(qs) { if (!qs) { return {} @@ -184,10 +164,8 @@ export const parseToCsv = (headers, rows) => { export default { breakQueryString, buildQueryString, - fieldsToSchema, flipHeaderState, keyValueToQueryParameters, parseToCsv, queryParametersToKeyValue, - schemaToFields, } diff --git a/packages/builder/src/helpers/planTitle.js b/packages/builder/src/helpers/planTitle.js new file mode 100644 index 0000000000..098bfb4529 --- /dev/null +++ b/packages/builder/src/helpers/planTitle.js @@ -0,0 +1,27 @@ +import { PlanType } from "@budibase/types" + +export function getFormattedPlanName(userPlanType) { + let planName + switch (userPlanType) { + case PlanType.PRO: + planName = "Pro" + break + case PlanType.TEAM: + planName = "Team" + break + case PlanType.PREMIUM: + case PlanType.PREMIUM_PLUS: + planName = "Premium" + break + case PlanType.BUSINESS: + planName = "Business" + break + case PlanType.ENTERPRISE_BASIC: + case PlanType.ENTERPRISE: + planName = "Enterprise" + break + default: + planName = "Free" // Default to "Free" if the type is not explicitly handled + } + return `${planName} Plan` +} 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 b937d69fd7..33116094eb 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -392,6 +392,10 @@ } const openInviteFlow = () => { + // prevent email from getting overwritten if changes are made + if (!email) { + email = query + } $licensing.userLimitReached ? userLimitReachedModal.show() : (invitingFlow = true) 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 2127392bb9..011980bbe2 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 @@ -1,7 +1,7 @@
    @@ -111,7 +112,7 @@ on:dragover={dragover(component, index)} on:iconClick={() => toggleNodeOpen(component._id)} on:drop={onDrop} - hovering={$store.hoveredComponentId === component._id} + hovering={$hoverStore.componentId === component._id} on:mouseenter={() => hover(component._id)} on:mouseleave={() => hover(null)} text={getComponentText(component)} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte index d2ffc5de74..13f2e73853 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte @@ -1,7 +1,12 @@
    @@ -60,7 +65,7 @@ icon="WebPage" on:drop={onDrop} on:click={() => ($store.selectedComponentId = screenComponentId)} - hovering={$store.hoveredComponentId === screenComponentId} + hovering={$hoverStore.componentId === screenComponentId} on:mouseenter={() => hover(screenComponentId)} on:mouseleave={() => hover(null)} id="component-screen" @@ -79,7 +84,7 @@ : "VisibilityOff"} on:drop={onDrop} on:click={() => ($store.selectedComponentId = navComponentId)} - hovering={$store.hoveredComponentId === navComponentId} + hovering={$hoverStore.componentId === navComponentId} on:mouseenter={() => hover(navComponentId)} on:mouseleave={() => hover(null)} id="component-nav" 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 899b58ecf0..65a13f0bd9 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte @@ -15,9 +15,9 @@ app.devId === $store.appId?.includes(app.appId)) $: licensePlan = $auth.user?.license?.plan $: page = $pageInfo.page $: fetchLogs(automationId, status, page, timeRange) + $: isCloud = $admin.cloud + + $: chainAutomations = app?.automations?.chainAutomations ?? !isCloud const timeOptions = [ { value: "90-d", label: "Past 90 days" }, @@ -124,6 +130,18 @@ sidePanel.open() } + async function save({ detail }) { + try { + await apps.update($store.appId, { + automations: { + chainAutomations: detail, + }, + }) + } catch (error) { + notifications.error("Error updating automation chaining setting") + } + } + onMount(async () => { await automationStore.actions.fetch() const params = new URLSearchParams(window.location.search) @@ -150,11 +168,30 @@ - Automation History - View the automations your app has executed + Automations + See your automation history and edit advanced settings + + Chain automations + Allow automations to trigger from other automations +
    + { + save(e) + }} + value={chainAutomations} + /> +
    +
    + + + + History + Free plan stores up to 1 day of automation history +