diff --git a/hosting/couchdb/Dockerfile b/hosting/couchdb/Dockerfile index 792856cac7..f83df7038b 100644 --- a/hosting/couchdb/Dockerfile +++ b/hosting/couchdb/Dockerfile @@ -29,7 +29,6 @@ WORKDIR /opt/couchdb ADD couch/vm.args couch/local.ini ./etc/ WORKDIR / -ADD build-target-paths.sh . ADD runner.sh ./bbcouch-runner.sh -RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh +RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau CMD ["./bbcouch-runner.sh"] diff --git a/hosting/couchdb/build-target-paths.sh b/hosting/couchdb/build-target-paths.sh deleted file mode 100644 index 34227011f4..0000000000 --- a/hosting/couchdb/build-target-paths.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -echo ${TARGETBUILD} > /buildtarget.txt -if [[ "${TARGETBUILD}" = "aas" ]]; then - # Azure AppService uses /home for persistent data & SSH on port 2222 - DATA_DIR="${DATA_DIR:-/home}" - WEBSITES_ENABLE_APP_SERVICE_STORAGE=true - mkdir -p $DATA_DIR/{search,minio,couch} - mkdir -p $DATA_DIR/couch/{dbs,views} - chown -R couchdb:couchdb $DATA_DIR/couch/ - apt update - apt-get install -y openssh-server - echo "root:Docker!" | chpasswd - mkdir -p /tmp - chmod +x /tmp/ssh_setup.sh \ - && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) - cp /etc/sshd_config /etc/ssh/sshd_config - /etc/init.d/ssh restart - sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini -else - sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini -fi \ No newline at end of file diff --git a/hosting/couchdb/runner.sh b/hosting/couchdb/runner.sh index 4102d2a751..b576c886c2 100644 --- a/hosting/couchdb/runner.sh +++ b/hosting/couchdb/runner.sh @@ -1,14 +1,73 @@ #!/bin/bash DATA_DIR=${DATA_DIR:-/data} + mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/search chown -R couchdb:couchdb ${DATA_DIR}/couch -/build-target-paths.sh + +echo ${TARGETBUILD} > /buildtarget.txt +if [[ "${TARGETBUILD}" = "aas" ]]; then + # Azure AppService uses /home for persistent data & SSH on port 2222 + DATA_DIR="${DATA_DIR:-/home}" + WEBSITES_ENABLE_APP_SERVICE_STORAGE=true + mkdir -p $DATA_DIR/{search,minio,couch} + mkdir -p $DATA_DIR/couch/{dbs,views} + chown -R couchdb:couchdb $DATA_DIR/couch/ + apt update + apt-get install -y openssh-server + echo "root:Docker!" | chpasswd + mkdir -p /tmp + chmod +x /tmp/ssh_setup.sh \ + && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) + cp /etc/sshd_config /etc/ssh/sshd_config + /etc/init.d/ssh restart + sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini +elif [[ "${TARGETBUILD}" = "single" ]]; then + # In the single image build, the Dockerfile specifies /data as a volume + # mount, so we use that for all persistent data. + sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini +elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then + # In Kubernetes the directory /opt/couchdb/data has a persistent volume + # mount for storing database data. + sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini + + # We remove the database_dir and view_index_dir settings from the local.ini + # in Kubernetes because it will default to /opt/couchdb/data which is what + # our Helm chart was using prior to us switching to using our own CouchDB + # image. + sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini + sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini + + # We remove the -name setting from the vm.args file in Kubernetes because + # it will default to the pod FQDN, which is what's required for clustering + # to work. + sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args +else + # For all other builds, we use /data for persistent data. + sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini +fi + +# Start Clouseau. Budibase won't function correctly without Clouseau running, it +# powers the search API endpoints which are used to do all sorts, including +# populating app grids. /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & + +# Start CouchDB. /docker-entrypoint.sh /opt/couchdb/bin/couchdb & -sleep 10 + +# Wati for CouchDB to start up. +while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do + echo 'Waiting for CouchDB to start...'; + sleep 5; +done + +# CouchDB needs the `_users` and `_replicator` databases to exist before it will +# function correctly, so we create them here. curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator sleep infinity \ No newline at end of file diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index ec03a1b5a2..e9ff6c6596 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -94,8 +94,6 @@ RUN chmod +x ./healthcheck.sh # For Azure App Service install SSH & point data locations to /home COPY hosting/single/ssh/sshd_config /etc/ COPY hosting/single/ssh/ssh_setup.sh /tmp -RUN /build-target-paths.sh - # setup letsencrypt certificate RUN apt-get install -y certbot python3-certbot-nginx diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index 87201c95c0..f4b2b5b127 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -22,11 +22,11 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME # Azure App Service customisations if [[ "${TARGETBUILD}" = "aas" ]]; then - DATA_DIR="${DATA_DIR:-/home}" + export DATA_DIR="${DATA_DIR:-/home}" WEBSITES_ENABLE_APP_SERVICE_STORAGE=true /etc/init.d/ssh start else - DATA_DIR=${DATA_DIR:-/data} + export DATA_DIR=${DATA_DIR:-/data} fi mkdir -p ${DATA_DIR} # Mount NFS or GCP Filestore if env vars exist for it diff --git a/lerna.json b/lerna.json index faba64ce90..e54b76c5e9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.15", + "version": "2.13.19", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index cdaf19fa55..1971c09e9d 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -260,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) { } /** - * Generate a presigned url with a default TTL of 1 hour + * Generate a presigned url with a default TTL of 36 hours */ export function getPresignedUrl( bucketName: string, key: string, - durationSeconds: number = 3600 + durationSeconds: number = 129600 ) { const objectStore = ObjectStore(bucketName, { presigning: true }) const params = { diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index fe4095d210..98704f16c6 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -160,4 +160,5 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) { // utility as a lot of things need simply the builder permission export const BUILDER = PermissionType.BUILDER +export const CREATOR = PermissionType.CREATOR export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 2b6fd52d44..326bed3cc5 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -146,12 +146,12 @@ export class UserDB { static async allUsers() { const db = getGlobalDB() - const response = await db.allDocs( + const response = await db.allDocs( dbUtils.getGlobalUserParams(null, { include_docs: true, }) ) - return response.rows.map((row: any) => row.doc) + return response.rows.map(row => row.doc!) } static async countUsersByApp(appId: string) { @@ -209,13 +209,6 @@ export class UserDB { throw new Error("_id or email is required") } - if ( - user.builder?.apps?.length && - !(await UserDB.features.isAppBuildersEnabled()) - ) { - throw new Error("Unable to update app builders, please check license") - } - let dbUser: User | undefined if (_id) { // try to get existing user from db diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 6aed45371a..cc2b4fc27f 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -25,6 +25,7 @@ import { import { getGlobalDB } from "../context" import * as context from "../context" import { isCreator } from "./utils" +import { UserDB } from "./db" type GetOpts = { cleanup?: boolean } @@ -336,3 +337,20 @@ export function cleanseUserObject(user: User | ContextUser, base?: User) { } return user } + +export async function addAppBuilder(user: User, appId: string) { + const prodAppId = getProdAppID(appId) + user.builder ??= {} + user.builder.creator = true + user.builder.apps ??= [] + user.builder.apps.push(prodAppId) + await UserDB.save(user, { hashPassword: false }) +} + +export async function removeAppBuilder(user: User, appId: string) { + const prodAppId = getProdAppID(appId) + if (user.builder && user.builder.apps?.includes(prodAppId)) { + user.builder.apps = user.builder.apps.filter(id => id !== prodAppId) + } + await UserDB.save(user, { hashPassword: false }) +} diff --git a/packages/bbui/src/ButtonGroup/ButtonGroup.svelte b/packages/bbui/src/ButtonGroup/ButtonGroup.svelte index 3768fc1954..7c53f642b0 100644 --- a/packages/bbui/src/ButtonGroup/ButtonGroup.svelte +++ b/packages/bbui/src/ButtonGroup/ButtonGroup.svelte @@ -2,7 +2,7 @@ import "@spectrum-css/buttongroup/dist/index-vars.css" export let vertical = false - export let gap = "" + export let gap = "M" $: gapStyle = gap === "L" diff --git a/packages/bbui/src/FancyForm/FancySelect.svelte b/packages/bbui/src/FancyForm/FancySelect.svelte index e015f51570..14911f10ab 100644 --- a/packages/bbui/src/FancyForm/FancySelect.svelte +++ b/packages/bbui/src/FancyForm/FancySelect.svelte @@ -12,11 +12,13 @@ export let error = null export let validate = null export let options = [] + export let footer = null export let isOptionEnabled = () => true export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") export let getOptionSubtitle = option => extractProperty(option, "subtitle") export let getOptionColour = () => null + const dispatch = createEventDispatcher() let open = false @@ -100,6 +102,7 @@ {error} {disabled} {options} + {footer} {getOptionLabel} {getOptionValue} {getOptionSubtitle} diff --git a/packages/bbui/src/Form/Core/InputDropdown.svelte b/packages/bbui/src/Form/Core/InputDropdown.svelte index 153501ce5a..128353b7b9 100644 --- a/packages/bbui/src/Form/Core/InputDropdown.svelte +++ b/packages/bbui/src/Form/Core/InputDropdown.svelte @@ -17,7 +17,7 @@ export let options = [] export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") - + export let getOptionSubtitle = option => option?.subtitle export let isOptionSelected = () => false const dispatch = createEventDispatcher() @@ -135,7 +135,7 @@ class="spectrum-Textfield-input spectrum-InputGroup-input" /> -
+
- {#if open} -
-
    - {#each options as option, idx} -
  • onPick(getOptionValue(option, idx))} - > - - {getOptionLabel(option, idx)} - - -
  • - {/each} -
-
- {/if}
+ {#if open} +
+
    + {#each options as option, idx} +
  • onPick(getOptionValue(option, idx))} + > + + {getOptionLabel(option, idx)} + {#if getOptionSubtitle(option, idx)} + + {getOptionSubtitle(option, idx)} + + {/if} + + +
  • + {/each} +
+
+ {/if}
diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 6361b345d1..94fbe73cf2 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -224,13 +224,12 @@ {/if} - {#if getOptionSubtitle(option, idx)} - {getOptionSubtitle(option, idx)} - {/if} - {getOptionLabel(option, idx)} + {#if getOptionSubtitle(option, idx)} + + {getOptionSubtitle(option, idx)} + + {/if} {#if option.tag} @@ -275,10 +274,9 @@ font-size: 12px; line-height: 15px; font-weight: 500; - top: 10px; color: var(--spectrum-global-color-gray-600); display: block; - margin-bottom: var(--spacing-s); + margin-top: var(--spacing-s); } .spectrum-Picker-label.auto-width { diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index e928ae689d..be22000f0b 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -10,8 +10,9 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionIcon = () => null - export let useOptionIconImage = false export let getOptionColour = () => null + export let getOptionSubtitle = () => null + export let useOptionIconImage = false export let isOptionEnabled export let readonly = false export let quiet = false @@ -82,8 +83,9 @@ {getOptionLabel} {getOptionValue} {getOptionIcon} - {useOptionIconImage} {getOptionColour} + {getOptionSubtitle} + {useOptionIconImage} {isOptionEnabled} {autocomplete} {sort} diff --git a/packages/bbui/src/Form/InputDropdown.svelte b/packages/bbui/src/Form/InputDropdown.svelte index 93766495b8..54b07cf0d7 100644 --- a/packages/bbui/src/Form/InputDropdown.svelte +++ b/packages/bbui/src/Form/InputDropdown.svelte @@ -43,6 +43,7 @@ {quiet} {autofocus} {options} + isOptionSelected={option => option === dropdownValue} on:change={onChange} on:pick={onPick} on:click diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index f9d9532ba5..8235b68faf 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -13,9 +13,10 @@ export let options = [] export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") + export let getOptionSubtitle = option => option?.subtitle export let getOptionIcon = option => option?.icon - export let useOptionIconImage = false export let getOptionColour = option => option?.colour + export let useOptionIconImage = false export let isOptionEnabled export let quiet = false export let autoWidth = false @@ -58,6 +59,7 @@ {getOptionValue} {getOptionIcon} {getOptionColour} + {getOptionSubtitle} {useOptionIconImage} {isOptionEnabled} {autocomplete} diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index 86235297ae..3fd2708186 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -64,7 +64,7 @@ {:else if schema.type === "link"} onChange(e, field)} useLabel={false} diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 61b706e28e..1ec32cb3fd 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -70,7 +70,12 @@ options={meta.constraints.inclusion} /> {:else if type === "link"} - + (value = e.detail)} + /> {:else if type === "longform"} {#if meta.useRichText} diff --git a/packages/builder/src/components/common/LinkedRowSelector.svelte b/packages/builder/src/components/common/LinkedRowSelector.svelte index 839075ba89..bf851eadb6 100644 --- a/packages/builder/src/components/common/LinkedRowSelector.svelte +++ b/packages/builder/src/components/common/LinkedRowSelector.svelte @@ -56,12 +56,12 @@ /> {:else} row._id} sort - on:change={() => dispatch("change", linkedIds)} + on:change /> {/if} diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 2df61926e1..1158107fd6 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -20,73 +20,91 @@ export let allowedRoles = null export let allowCreator = false export let fancySelect = false + export let labelPrefix = null const dispatch = createEventDispatcher() const RemoveID = "remove" + $: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label) $: options = getOptions( $roles, allowPublic, allowRemove, allowedRoles, - allowCreator + allowCreator, + enrichLabel ) + const getOptions = ( roles, allowPublic, allowRemove, allowedRoles, - allowCreator + allowCreator, + enrichLabel ) => { + // Use roles whitelist if specified if (allowedRoles?.length) { - const filteredRoles = roles.filter(role => - allowedRoles.includes(role._id) - ) - return [ - ...filteredRoles, - ...(allowedRoles.includes(Constants.Roles.CREATOR) - ? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }] - : []), - ] - } - let newRoles = [...roles] - - if (allowCreator) { - newRoles = [ - { + let options = roles + .filter(role => allowedRoles.includes(role._id)) + .map(role => ({ + name: enrichLabel(role.name), + _id: role._id, + })) + if (allowedRoles.includes(Constants.Roles.CREATOR)) { + options.push({ _id: Constants.Roles.CREATOR, - name: "Creator", - tag: - !$licensing.perAppBuildersEnabled && - capitalise(Constants.PlanType.BUSINESS), - }, - ...newRoles, - ] + name: "Can edit", + enabled: false, + }) + } + return options } + + // Allow all core roles + let options = roles.map(role => ({ + name: enrichLabel(role.name), + _id: role._id, + })) + + // Add creator if required + if (allowCreator) { + options.unshift({ + _id: Constants.Roles.CREATOR, + name: "Can edit", + tag: + !$licensing.perAppBuildersEnabled && + capitalise(Constants.PlanType.BUSINESS), + }) + } + + // Add remove option if required if (allowRemove) { - newRoles = [ - ...newRoles, - { - _id: RemoveID, - name: "Remove", - }, - ] + options.push({ + _id: RemoveID, + name: "Remove", + }) } - if (allowPublic) { - return newRoles + + // Remove public if not allowed + if (!allowPublic) { + options = options.filter(role => role._id !== Constants.Roles.PUBLIC) } - return newRoles.filter(role => role._id !== Constants.Roles.PUBLIC) + + return options } const getColor = role => { - if (allowRemove && role._id === RemoveID) { + // Creator and remove options have no colors + if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) { return null } return RoleUtils.getRoleColour(role._id) } const getIcon = role => { - if (allowRemove && role._id === RemoveID) { + // Only remove option has an icon + if (role._id === RemoveID) { return "Close" } return null 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 c3bb5171a4..b937d69fd7 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -364,7 +364,10 @@ const payload = [ { email: newUserEmail, - builder: { global: creationRoleType === Constants.BudibaseRoles.Admin }, + builder: { + global: creationRoleType === Constants.BudibaseRoles.Admin, + creator: creationRoleType === Constants.BudibaseRoles.Creator, + }, admin: { global: creationRoleType === Constants.BudibaseRoles.Admin }, }, ] @@ -471,10 +474,6 @@ await users.removeAppBuilder(userId, prodAppId) } - const addGroupAppBuilder = async groupId => { - await groups.actions.addGroupAppBuilder(groupId, prodAppId) - } - const removeGroupAppBuilder = async groupId => { await groups.actions.removeGroupAppBuilder(groupId, prodAppId) } @@ -495,14 +494,12 @@ } const getInviteRoleValue = invite => { - if (invite.info?.admin?.global && invite.info?.builder?.global) { - return Constants.Roles.ADMIN - } - - if (invite.info?.builder?.apps?.includes(prodAppId)) { + if ( + (invite.info?.admin?.global && invite.info?.builder?.global) || + invite.info?.builder?.apps?.includes(prodAppId) + ) { return Constants.Roles.CREATOR } - return invite.info.apps?.[prodAppId] } @@ -512,7 +509,7 @@ return `This user has been given ${role?.name} access from the ${user.group} group` } if (user.isAdminOrGlobalBuilder) { - return "This user's role grants admin access to all apps" + return "Account admins can edit all apps" } return null } @@ -523,6 +520,18 @@ } return user.role } + + const checkAppAccess = e => { + // Ensure we don't get into an invalid combo of tenant role and app access + if ( + e.detail === Constants.BudibaseRoles.AppUser && + creationAccessType === Constants.Roles.CREATOR + ) { + creationAccessType = Constants.Roles.BASIC + } else if (e.detail === Constants.BudibaseRoles.Admin) { + creationAccessType = Constants.Roles.CREATOR + } + } @@ -650,8 +659,9 @@ autoWidth align="right" allowedRoles={user.isAdminOrGlobalBuilder - ? [Constants.Roles.ADMIN] + ? [Constants.Roles.CREATOR] : null} + labelPrefix="Can use as" /> @@ -695,19 +705,16 @@ allowRemove={group.role} allowPublic={false} quiet={true} - allowCreator={true} + allowCreator={group.role === Constants.Roles.CREATOR} on:change={e => { - if (e.detail === Constants.Roles.CREATOR) { - addGroupAppBuilder(group._id) - } else { - onUpdateGroup(group, e.detail) - } + onUpdateGroup(group, e.detail) }} on:remove={() => { onUpdateGroup(group) }} autoWidth align="right" + labelPrefix="Can use as" /> @@ -753,6 +760,7 @@ allowedRoles={user.isAdminOrGlobalBuilder ? [Constants.Roles.CREATOR] : null} + labelPrefix="Can use as" /> @@ -804,33 +812,34 @@ option.value !== Constants.BudibaseRoles.Admin )} - label="Access" + label="Role" + on:change={checkAppAccess} /> - {#if creationRoleType !== Constants.BudibaseRoles.Admin} - - - - {/if} + + + - {#if creationRoleType === Constants.BudibaseRoles.Admin} -
- - Admins will get full access to all apps and settings -
- {/if}