diff --git a/lerna.json b/lerna.json index b648e6ca22..f34ab2a420 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.1.24", + "version": "1.1.29-alpha.2", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index d8885ce717..17436357e0 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.1.24", + "version": "1.1.29-alpha.2", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "^1.1.24", + "@budibase/types": "^1.1.29-alpha.2", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b60144a0de..9ae29a3cbd 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -18,6 +18,8 @@ const { ssoCallbackUrl, csrf, internalApi, + adminOnly, + joiValidator, } = require("./middleware") const { invalidateUser } = require("./cache/user") @@ -173,4 +175,6 @@ module.exports = { refreshOAuthToken, updateUserOAuth, ssoCallbackUrl, + adminOnly, + joiValidator, } diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index 716762dd45..9c6be25424 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -11,6 +11,7 @@ export enum AutomationViewModes { } export enum ViewNames { + USER_BY_APP = "by_app", USER_BY_EMAIL = "by_email2", BY_API_KEY = "by_api_key", USER_BY_BUILDERS = "by_builders", @@ -28,6 +29,7 @@ export const DeprecatedViews = { export enum DocumentTypes { USER = "us", + GROUP = "gr", WORKSPACE = "workspace", CONFIG = "config", TEMPLATE = "template", diff --git a/packages/backend-core/src/db/conversions.js b/packages/backend-core/src/db/conversions.js index 455cc712d8..90c04e9251 100644 --- a/packages/backend-core/src/db/conversions.js +++ b/packages/backend-core/src/db/conversions.js @@ -50,3 +50,8 @@ exports.getProdAppID = appId => { const rest = split.join(APP_DEV_PREFIX) return `${APP_PREFIX}${rest}` } + +exports.extractAppUUID = id => { + const split = id?.split("_") || [] + return split.length ? split[split.length - 1] : null +} diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index ba3f1dd3e9..8ab6fa6e98 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -8,7 +8,7 @@ import { doWithDB, allDbs } from "./index" import { getCouchInfo } from "./pouch" import { getAppMetadata } from "../cache/appMetadata" import { checkSlashesInUrl } from "../helpers" -import { isDevApp, isDevAppID } from "./conversions" +import { isDevApp, isDevAppID, getProdAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" @@ -107,6 +107,15 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) { } } +export function getUsersByAppParams(appId: any, otherProps: any = {}) { + const prodAppId = getProdAppID(appId) + return { + ...otherProps, + startkey: prodAppId, + endkey: `${prodAppId}${UNICODE_MAX}`, + } +} + /** * Generates a template ID. * @param ownerId The owner/user of the template, this could be global or a workspace level. @@ -115,6 +124,10 @@ export function generateTemplateID(ownerId: any) { return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` } +export function generateAppUserID(prodAppId: string, userId: string) { + return `${prodAppId}${SEPARATOR}${userId}` +} + /** * Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level. */ @@ -442,15 +455,29 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { export function pagination( data: any[], pageSize: number, - { paginate, property } = { paginate: true, property: "_id" } + { + paginate, + property, + getKey, + }: { + paginate: boolean + property: string + getKey?: (doc: any) => string | undefined + } = { + paginate: true, + property: "_id", + } ) { if (!paginate) { return { data, hasNextPage: false } } const hasNextPage = data.length > pageSize let nextPage = undefined + if (!getKey) { + getKey = (doc: any) => (property ? doc?.[property] : doc?._id) + } if (hasNextPage) { - nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + nextPage = getKey(data[pageSize]) } return { data: data.slice(0, pageSize), diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js index 1e8dd7ee77..baf1807ca5 100644 --- a/packages/backend-core/src/db/views.js +++ b/packages/backend-core/src/db/views.js @@ -56,6 +56,33 @@ exports.createNewUserEmailView = async () => { await db.put(designDoc) } +exports.createUserAppView = async () => { + const db = getGlobalDB() + let designDoc + try { + designDoc = await db.get("_design/database") + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } + const view = { + // if using variables in a map function need to inject them before use + map: `function(doc) { + if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}") && doc.roles) { + for (let prodAppId of Object.keys(doc.roles)) { + let emitted = prodAppId + "${SEPARATOR}" + doc._id + emit(emitted, null) + } + } + }`, + } + designDoc.views = { + ...designDoc.views, + [ViewNames.USER_BY_APP]: view, + } + await db.put(designDoc) +} + exports.createApiKeyView = async () => { const db = getGlobalDB() let designDoc @@ -106,6 +133,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => { [ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView, [ViewNames.BY_API_KEY]: exports.createApiKeyView, [ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView, + [ViewNames.USER_BY_APP]: exports.createUserAppView, } // can pass DB in if working with something specific if (!db) { diff --git a/packages/backend-core/src/errors/index.js b/packages/backend-core/src/errors/index.js index 58b4eea8c5..31ffd739a0 100644 --- a/packages/backend-core/src/errors/index.js +++ b/packages/backend-core/src/errors/index.js @@ -37,6 +37,7 @@ module.exports = { types, errors: { UsageLimitError: licensing.UsageLimitError, + FeatureDisabledError: licensing.FeatureDisabledError, HTTPError: http.HTTPError, }, getPublicError, diff --git a/packages/backend-core/src/errors/licensing.js b/packages/backend-core/src/errors/licensing.js index 0d8ce08146..85d207ac35 100644 --- a/packages/backend-core/src/errors/licensing.js +++ b/packages/backend-core/src/errors/licensing.js @@ -4,6 +4,7 @@ const type = "license_error" const codes = { USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", + FEATURE_DISABLED: "feature_disabled", } const context = { @@ -12,6 +13,11 @@ const context = { limitName: err.limitName, } }, + [codes.FEATURE_DISABLED]: err => { + return { + featureName: err.featureName, + } + }, } class UsageLimitError extends HTTPError { @@ -21,9 +27,17 @@ class UsageLimitError extends HTTPError { } } +class FeatureDisabledError extends HTTPError { + constructor(message, featureName) { + super(message, 400, codes.FEATURE_DISABLED, type) + this.featureName = featureName + } +} + module.exports = { type, codes, context, UsageLimitError, + FeatureDisabledError, } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts new file mode 100644 index 0000000000..d300873725 --- /dev/null +++ b/packages/backend-core/src/events/publishers/group.ts @@ -0,0 +1,64 @@ +import { publishEvent } from "../events" +import { + Event, + UserGroup, + GroupCreatedEvent, + GroupDeletedEvent, + GroupUpdatedEvent, + GroupUsersAddedEvent, + GroupUsersDeletedEvent, + GroupAddedOnboardingEvent, + UserGroupRoles, +} from "@budibase/types" + +export async function created(group: UserGroup, timestamp?: number) { + const properties: GroupCreatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) +} + +export async function updated(group: UserGroup) { + const properties: GroupUpdatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_UPDATED, properties) +} + +export async function deleted(group: UserGroup) { + const properties: GroupDeletedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_DELETED, properties) +} + +export async function usersAdded(count: number, group: UserGroup) { + const properties: GroupUsersAddedEvent = { + count, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) +} + +export async function usersDeleted(emails: string[], group: UserGroup) { + const properties: GroupUsersDeletedEvent = { + count: emails.length, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) +} + +export async function createdOnboarding(groupId: string) { + const properties: GroupAddedOnboardingEvent = { + groupId: groupId, + onboarding: true, + } + await publishEvent(Event.USER_GROUP_ONBOARDING, properties) +} + +export async function permissionsEdited(roles: UserGroupRoles) { + const properties: UserGroupRoles = { + ...roles, + } + await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties) +} diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 65785d4d8b..57fd0bf8e2 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -17,3 +17,4 @@ export * as user from "./user" export * as view from "./view" export * as installation from "./installation" export * as backfill from "./backfill" +export * as group from "./group" diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index ab89eed3b2..35777ae817 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -3,6 +3,7 @@ const errorClasses = errors.errors import * as events from "./events" import * as migrations from "./migrations" import * as users from "./users" +import * as roles from "./security/roles" import * as accounts from "./cloud/accounts" import * as installation from "./installation" import env from "./environment" @@ -51,6 +52,7 @@ const core = { installation, errors, logging, + roles, ...errorClasses, } diff --git a/packages/backend-core/src/middleware/adminOnly.js b/packages/backend-core/src/middleware/adminOnly.js new file mode 100644 index 0000000000..4bfdf83848 --- /dev/null +++ b/packages/backend-core/src/middleware/adminOnly.js @@ -0,0 +1,9 @@ +module.exports = async (ctx, next) => { + if ( + !ctx.internal && + (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) + ) { + ctx.throw(403, "Admin user only endpoint.") + } + return next() +} diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 4e6e0b7ba2..d86af773c3 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -127,7 +127,7 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } - } else { + } else if (user) { delete user.password } // be explicit diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 1721d56a3c..9d94bf5763 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -9,7 +9,8 @@ const tenancy = require("./tenancy") const internalApi = require("./internalApi") const datasourceGoogle = require("./passport/datasource/google") const csrf = require("./csrf") - +const adminOnly = require("./adminOnly") +const joiValidator = require("./joi-validator") module.exports = { google, oidc, @@ -25,4 +26,6 @@ module.exports = { google: datasourceGoogle, }, csrf, + adminOnly, + joiValidator, } diff --git a/packages/backend-core/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js new file mode 100644 index 0000000000..1686b0e727 --- /dev/null +++ b/packages/backend-core/src/middleware/joi-validator.js @@ -0,0 +1,28 @@ +function validate(schema, property) { + // Return a Koa middleware function + return (ctx, next) => { + if (!schema) { + return next() + } + let params = null + if (ctx[property] != null) { + params = ctx[property] + } else if (ctx.request[property] != null) { + params = ctx.request[property] + } + const { error } = schema.validate(params) + if (error) { + ctx.throw(400, `Invalid ${property} - ${error.message}`) + return + } + return next() + } +} + +module.exports.body = schema => { + return validate(schema, "body") +} + +module.exports.params = schema => { + return validate(schema, "params") +} diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 7c57cadcbf..44dc4f2d3e 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -76,7 +76,7 @@ function isBuiltin(role) { /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ -function builtinRoleToNumber(id) { +exports.builtinRoleToNumber = id => { const builtins = exports.getBuiltinRoles() const MAX = Object.values(BUILTIN_IDS).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { @@ -104,7 +104,8 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { if (!roleId2) { return roleId1 } - return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) + return exports.builtinRoleToNumber(roleId1) > + exports.builtinRoleToNumber(roleId2) ? roleId2 : roleId1 } diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 0c1350a674..34d546a8bb 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,4 +1,9 @@ -const { ViewNames } = require("./db/utils") +const { + ViewNames, + getUsersByAppParams, + getProdAppID, + generateAppUserID, +} = require("./db/utils") const { queryGlobalView } = require("./db/views") const { UNICODE_MAX } = require("./db/constants") @@ -13,12 +18,32 @@ exports.getGlobalUserByEmail = async email => { throw "Must supply an email address to view" } - const response = await queryGlobalView(ViewNames.USER_BY_EMAIL, { + return await queryGlobalView(ViewNames.USER_BY_EMAIL, { key: email.toLowerCase(), include_docs: true, }) +} - return response +exports.searchGlobalUsersByApp = async (appId, opts) => { + if (typeof appId !== "string") { + throw new Error("Must provide a string based app ID") + } + const params = getUsersByAppParams(appId, { + include_docs: true, + }) + params.startkey = opts && opts.startkey ? opts.startkey : params.startkey + let response = await queryGlobalView(ViewNames.USER_BY_APP, params) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} + +exports.getGlobalUserByAppPage = (appId, user) => { + if (!user) { + return + } + return generateAppUserID(getProdAppID(appId), user._id) } /** diff --git a/packages/backend-core/tests/utilities/mocks/events.js b/packages/backend-core/tests/utilities/mocks/events.js index a4055cc5ea..415d59019d 100644 --- a/packages/backend-core/tests/utilities/mocks/events.js +++ b/packages/backend-core/tests/utilities/mocks/events.js @@ -89,6 +89,14 @@ jest.spyOn(events.user, "passwordUpdated") jest.spyOn(events.user, "passwordResetRequested") jest.spyOn(events.user, "passwordReset") +jest.spyOn(events.group, "created") +jest.spyOn(events.group, "updated") +jest.spyOn(events.group, "deleted") +jest.spyOn(events.group, "usersAdded") +jest.spyOn(events.group, "usersDeleted") +jest.spyOn(events.group, "createdOnboarding") +jest.spyOn(events.group, "permissionsEdited") + jest.spyOn(events.serve, "servedBuilder") jest.spyOn(events.serve, "servedApp") jest.spyOn(events.serve, "servedAppPreview") diff --git a/packages/bbui/package.json b/packages/bbui/package.json index f2706bb76c..f9d152370b 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.1.24", + "version": "1.1.29-alpha.2", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.1.24", + "@budibase/string-templates": "^1.1.29-alpha.2", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index 53ba6c7e51..cfc810807e 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -84,6 +84,7 @@ } :global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) { margin-left: 0; + transition: color ease-out 130ms; } .is-selected:not(.spectrum-ActionButton--emphasized) { background: var(--spectrum-global-color-gray-300); @@ -92,4 +93,10 @@ padding: 0; min-width: 0; } + .spectrum-ActionButton--quiet { + padding: 0 8px; + } + .is-selected:not(.emphasized) .spectrum-Icon { + color: var(--spectrum-global-color-gray-900); + } diff --git a/packages/bbui/src/Avatar/Avatar.svelte b/packages/bbui/src/Avatar/Avatar.svelte index f8acd9024c..136a4fe24b 100644 --- a/packages/bbui/src/Avatar/Avatar.svelte +++ b/packages/bbui/src/Avatar/Avatar.svelte @@ -4,7 +4,7 @@ ["XXS", "--spectrum-alias-avatar-size-50"], ["XS", "--spectrum-alias-avatar-size-75"], ["S", "--spectrum-alias-avatar-size-200"], - ["M", "--spectrum-alias-avatar-size-300"], + ["M", "--spectrum-alias-avatar-size-400"], ["L", "--spectrum-alias-avatar-size-500"], ["XL", "--spectrum-alias-avatar-size-600"], ["XXL", "--spectrum-alias-avatar-size-700"], @@ -13,6 +13,19 @@ export let url = "" export let disabled = false export let initials = "JD" + + const DefaultColor = "#3aab87" + + $: color = getColor(initials) + + const getColor = initials => { + if (!initials?.length) { + return DefaultColor + } + const code = initials[0].toLowerCase().charCodeAt(0) + const hue = ((code % 26) / 26) * 360 + return `hsl(${hue}, 50%, 50%)` + } {#if url} @@ -25,10 +38,11 @@ /> {:else}
{initials || ""}
@@ -40,7 +54,6 @@ display: grid; place-items: center; font-weight: 600; - background: #3aab87; border-radius: 50%; overflow: hidden; user-select: none; diff --git a/packages/bbui/src/Form/Core/InputDropdown.svelte b/packages/bbui/src/Form/Core/InputDropdown.svelte new file mode 100644 index 0000000000..723b8ba9b1 --- /dev/null +++ b/packages/bbui/src/Form/Core/InputDropdown.svelte @@ -0,0 +1,218 @@ + + +
+
+ +
+
+ + {#if open} +
(open = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + > +
    + {#each options as option, idx} +
  • onPick(getOptionValue(option, idx))} + > + + {getOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+
+ + diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 3eb1add267..9dd5a25a4f 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -13,6 +13,7 @@ export let readonly = false export let autocomplete = false export let sort = false + export let autoWidth = false const dispatch = createEventDispatcher() $: selectedLookupMap = getSelectedLookupMap(value) @@ -85,4 +86,5 @@ {getOptionValue} onSelectOption={toggleOption} {sort} + {autoWidth} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index fc9f801be2..cdaf00aded 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -87,10 +87,15 @@ on:mousedown={onClick} > {#if fieldIcon} - + {/if} + {#if fieldColour} + + + + {/if} {/if} - {#if fieldColour} - - - - {/if} onSelectOption(getOptionValue(option, idx))} > {#if getOptionIcon(option, idx)} - + {/if} + {#if getOptionColour(option, idx)} + + + + {/if} {getOptionLabel(option, idx)} @@ -180,11 +185,6 @@ > - {#if getOptionColour(option, idx)} - - - - {/if} {/each} {/if} @@ -209,6 +209,9 @@ width: 100%; box-shadow: none; } + .spectrum-Picker-label.auto-width { + margin-right: var(--spacing-xs); + } .spectrum-Picker-label:not(.auto-width) { overflow: hidden; text-overflow: ellipsis; @@ -221,16 +224,16 @@ .spectrum-Picker-label.auto-width.is-placeholder { padding-right: 2px; } + .auto-width .spectrum-Menu-item { + padding-right: var(--spacing-xl); + } /* Icon and colour alignment */ .spectrum-Menu-checkmark { align-self: center; margin-top: 0; } - .option-colour { - padding-left: 8px; - } - .option-icon { + .option-extra { padding-right: 8px; } diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte new file mode 100644 index 0000000000..863403ee0c --- /dev/null +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -0,0 +1,430 @@ + + +
+
+ {#if iconData} + + + + {/if} + (primaryOpen = true)} + on:blur + on:focus + on:input + on:keyup + on:blur={onBlur} + on:input={onInput} + on:keyup={updateValueOnEnter} + value={primaryLabel || ""} + placeholder={placeholder || ""} + {disabled} + {readonly} + class="spectrum-Textfield-input spectrum-InputGroup-input" + class:labelPadding={iconData} + /> + {#if primaryValue} + + {/if} +
+ {#if primaryOpen} +
(primaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + class:auto-width={autoWidth} + class:is-full-width={!secondaryOptions.length} + > +
    + {#if placeholderOption} +
  • onSelectOption(null)} + > + {placeholderOption} + +
  • + {/if} + {#each groupTitles as title} +
    + {title} +
    + {#if primaryOptions} + {#each primaryOptions[title].data as option, idx} +
  • + onPickPrimary({ + value: primaryOptions[title].getValue(option), + label: primaryOptions[title].getLabel(option), + })} + > + {#if primaryOptions[title].getIcon(option)} +
    +
    + +
    +
    + {:else if getPrimaryOptionColour(option, idx)} + + + + {/if} + + + {primaryOptions[title].getLabel(option)} + + + + {#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)} + + + + {/if} + +
  • + {/each} + {/if} + {/each} +
+
+ {/if} + {#if secondaryOptions.length} +
+ + {#if secondaryOpen} +
(secondaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + style="width: 30%" + > +
    + {#each secondaryOptions as option, idx} +
  • + onPickSecondary(getSecondaryOptionValue(option, idx))} + > + {#if getSecondaryOptionColour(option, idx)} + + + + {/if} + + + {getSecondaryOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+ {/if} +
+ + diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 81d7ec8e6c..f549f58d0c 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -17,7 +17,6 @@ export let autoWidth = false export let autocomplete = false export let sort = false - const dispatch = createEventDispatcher() let open = false $: fieldText = getFieldText(value, options, placeholder) diff --git a/packages/bbui/src/Form/InputDropdown.svelte b/packages/bbui/src/Form/InputDropdown.svelte new file mode 100644 index 0000000000..73516ea37c --- /dev/null +++ b/packages/bbui/src/Form/InputDropdown.svelte @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/bbui/src/Form/Multiselect.svelte b/packages/bbui/src/Form/Multiselect.svelte index 957dcccddf..7bcf22aa06 100644 --- a/packages/bbui/src/Form/Multiselect.svelte +++ b/packages/bbui/src/Form/Multiselect.svelte @@ -14,7 +14,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let sort = false - + export let autoWidth = false const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -33,6 +33,7 @@ {sort} {getOptionLabel} {getOptionValue} + {autoWidth} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Form/PickerDropdown.svelte b/packages/bbui/src/Form/PickerDropdown.svelte new file mode 100644 index 0000000000..4ffb8248d0 --- /dev/null +++ b/packages/bbui/src/Form/PickerDropdown.svelte @@ -0,0 +1,125 @@ + + + + + diff --git a/packages/bbui/src/IconPicker/IconPicker.svelte b/packages/bbui/src/IconPicker/IconPicker.svelte new file mode 100644 index 0000000000..0e71be2c33 --- /dev/null +++ b/packages/bbui/src/IconPicker/IconPicker.svelte @@ -0,0 +1,177 @@ + + +
+
(open = true)}> +
+ +
+
+ {#if open} +
(open = false)} + transition:fly={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + class:spectrum-Popover--align-right={alignRight} + > + {#each iconList as icon} +
+
{icon.label}
+
+ {#each icon.icons as icon} +
{ + onChange(icon) + }} + > + +
+ {/each} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/packages/bbui/src/List/Items/DetailSummary.svench b/packages/bbui/src/List/Items/DetailSummary.svench deleted file mode 100644 index 48fb8f7df8..0000000000 --- a/packages/bbui/src/List/Items/DetailSummary.svench +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
- - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
diff --git a/packages/bbui/src/List/List.svelte b/packages/bbui/src/List/List.svelte new file mode 100644 index 0000000000..243b04da50 --- /dev/null +++ b/packages/bbui/src/List/List.svelte @@ -0,0 +1,28 @@ + + +
+ {#if title} +
+ {title} +
+ {/if} +
+ +
+
+ + diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte new file mode 100644 index 0000000000..76a83e7b08 --- /dev/null +++ b/packages/bbui/src/List/ListItem.svelte @@ -0,0 +1,92 @@ + + +
+
+ {#if icon} +
+ +
+ {/if} + {#if avatar} + + {/if} + {#if title} + {title} + {/if} + {#if subtitle} + + {/if} +
+
+ +
+
+ + diff --git a/packages/bbui/src/StatusLight/StatusLight.svelte b/packages/bbui/src/StatusLight/StatusLight.svelte index a0c72443a6..5b7257891f 100644 --- a/packages/bbui/src/StatusLight/StatusLight.svelte +++ b/packages/bbui/src/StatusLight/StatusLight.svelte @@ -18,11 +18,16 @@ export let disabled = false export let active = false export let color = null + export let square = false + export let hoverable = false
diff --git a/packages/bbui/src/Table/AttachmentRenderer.svelte b/packages/bbui/src/Table/AttachmentRenderer.svelte index 97ce1394cc..4dff22aef8 100644 --- a/packages/bbui/src/Table/AttachmentRenderer.svelte +++ b/packages/bbui/src/Table/AttachmentRenderer.svelte @@ -1,5 +1,4 @@
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index f77374985d..90e7ab661c 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -1,6 +1,7 @@ + +{#if schemaFields.length && isTestModal} +
+ {#each schemaFields as [field, schema]} + + {/each} +
+{/if} + + diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte index b754f878ce..f19f2279d9 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte @@ -211,7 +211,6 @@ bindings={getAuthBindings()} on:change={e => { form.bearer.token = e.detail - console.log(e.detail) onFieldChange() }} on:blur={() => { diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 4cb67dc9c4..b319560ddd 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -1,5 +1,5 @@ + + {#if $auth.isAdmin} - + {/if} diff --git a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte new file mode 100644 index 0000000000..2bcfd85cb6 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte @@ -0,0 +1,226 @@ + + + +
+ $goto("../groups")} size="S" icon="ArrowLeft"> + Back + +
+
+
+
+
+ +
+
+
+ {group?.name} +
+
+
+ +
+ + + +
+ + + {#if group?.users.length} + {#each group.users as user} + removeUser(user?._id)} + hoverable + size="L" + name="Close" + /> + {/each} + {:else} + + {/if} + +
+ Apps +
+ Manage apps that this User group has been assigned to +
+
+ + + {#if groupApps.length} + {#each groupApps as app} + +
+ +
+ {group.roles[app.appId]} +
+
+
+ {/each} + {:else} + + {/if} +
+
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte new file mode 100644 index 0000000000..22a59c2193 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte @@ -0,0 +1,58 @@ + + + saveGroup(group)} + size="M" + title="Create User Group" + confirmText="Save" +> + + + + + diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte new file mode 100644 index 0000000000..e00123614a --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte @@ -0,0 +1,129 @@ + + +
+
+
+
+ +
+
+
+ {group.name} +
+
+
+
+ +
+ {parseInt(group?.users?.length) || 0} user{parseInt( + group?.users?.length + ) === 1 + ? "" + : "s"} +
+
+
+ + +
+ {parseInt(group?.apps?.length) || 0} app{parseInt(group?.apps?.length) === 1 + ? "" + : "s"} +
+
+
+
+
+ +
+
+ + + + + deleteGroup(group)} icon="Delete" + >Delete + editGroup(group)} icon="Edit">Edit + +
+
+
+ + + + + + diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte new file mode 100644 index 0000000000..a13211a9bb --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/index.svelte b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte new file mode 100644 index 0000000000..131906529d --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte @@ -0,0 +1,145 @@ + + + + +
+ User groups + {#if !hasGroupsLicense} + +
+
+ Pro plan +
+
+
+ {/if} +
+ Easily assign and manage your users access with User Groups +
+
+ + {#if !hasGroupsLicense} + + {/if} +
+ + {#if hasGroupsLicense && $groups.length} +
+ {#each $groups as group} +
+ +
+ {/each} +
+ {/if} +
+ + + + + + diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index a8cb340465..28c5aa2593 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -2,79 +2,102 @@ import { goto } from "@roxi/routify" import { ActionButton, + ActionMenu, + Avatar, Button, Layout, Heading, Body, - Divider, Label, + List, + ListItem, + Icon, Input, + MenuItem, + Popover, Select, - Toggle, Modal, - Table, - ModalContent, notifications, + StatusLight, } from "@budibase/bbui" + import { onMount } from "svelte" + import { fetchData } from "helpers" - import { users, auth } from "stores/portal" - - import TagsRenderer from "./_components/RolesTagsTableRenderer.svelte" - - import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" + import { users, auth, groups, apps } from "stores/portal" + import { Constants } from "@budibase/frontend-core" import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte" + import { RoleUtils } from "@budibase/frontend-core" + import UserGroupPicker from "components/settings/UserGroupPicker.svelte" + import DeleteUserModal from "./_components/DeleteUserModal.svelte" export let userId - let deleteUserModal - let editRolesModal + + let deleteModal let resetPasswordModal + let popoverAnchor + let searchTerm = "" + let popover + let selectedGroups = [] + let allAppList = [] + let user + $: fetchUser(userId) + $: hasGroupsLicense = $auth.user?.license.features.includes( + Constants.Features.USER_GROUPS + ) - const roleSchema = { - name: { displayName: "App" }, - role: {}, - } - - const noRoleSchema = { - name: { displayName: "App" }, - } - - $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "" - // Merge the Apps list and the roles response to get something that makes sense for the table - $: allAppList = Object.keys($apps?.data).map(id => { - const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId - const role = $apps?.data?.[id].roles.find(role => role._id === roleId) - return { - ...$apps?.data?.[id], - _id: id, - role: [role], - } + $: allAppList = $apps + .filter(x => { + if ($userFetch.data?.roles) { + return Object.keys($userFetch.data.roles).find(y => { + return x.appId === apps.extractAppId(y) + }) + } + }) + .map(app => { + let roles = Object.fromEntries( + Object.entries($userFetch.data.roles).filter(([key]) => { + return apps.extractAppId(key) === app.appId + }) + ) + return { + name: app.name, + devId: app.devId, + icon: app.icon, + roles, + } + }) + // Used for searching through groups in the add group popover + $: filteredGroups = $groups.filter( + group => + selectedGroups && + group?.name?.toLowerCase().includes(searchTerm.toLowerCase()) + ) + $: userGroups = $groups.filter(x => { + return x.users?.find(y => { + return y._id === userId + }) }) - $: appList = allAppList.filter(app => !!app.role[0]) - $: noRoleAppList = allAppList - .filter(app => !app.role[0]) - .map(app => { - delete app.role - return app - }) - - let selectedApp + $: globalRole = $userFetch?.data?.admin?.global + ? "admin" + : $userFetch?.data?.builder?.global + ? "developer" + : "appUser" const userFetch = fetchData(`/api/global/users/${userId}`) - const apps = fetchData(`/api/global/roles`) - async function deleteUser() { - try { - await users.delete(userId) - notifications.success(`User ${$userFetch?.data?.email} deleted.`) - $goto("./") - } catch (error) { - notifications.error("Error deleting user") - } + function getHighestRole(roles) { + let highestRole + let highestRoleNumber = 0 + Object.keys(roles).forEach(role => { + let roleNumber = RoleUtils.getRolePriority(roles[role]) + if (roleNumber > highestRoleNumber) { + highestRoleNumber = roleNumber + highestRole = roles[role] + } + }) + return highestRole } - - let toggleDisabled = false - async function updateUserFirstName(evt) { try { await users.save({ ...$userFetch?.data, firstName: evt.target.value }) @@ -84,6 +107,13 @@ } } + async function removeGroup(id) { + let updatedGroup = $groups.find(x => x._id === id) + let newUsers = updatedGroup.users.filter(user => user._id !== userId) + updatedGroup.users = newUsers + groups.actions.save(updatedGroup) + } + async function updateUserLastName(evt) { try { await users.save({ ...$userFetch?.data, lastName: evt.target.value }) @@ -93,61 +123,95 @@ } } - async function toggleFlag(flagName, detail) { - toggleDisabled = true + async function updateUserRole({ detail }) { + if (detail === "developer") { + toggleFlags({ admin: { global: false }, builder: { global: true } }) + } else if (detail === "admin") { + toggleFlags({ admin: { global: true }, builder: { global: false } }) + } else if (detail === "appUser") { + toggleFlags({ admin: { global: false }, builder: { global: false } }) + } + } + + async function addGroup(groupId) { + let selectedGroup = selectedGroups.includes(groupId) + let group = $groups.find(group => group._id === groupId) + + if (selectedGroup) { + selectedGroups = selectedGroups.filter(id => id === selectedGroup) + let newUsers = group.users.filter(groupUser => user._id !== groupUser._id) + group.users = newUsers + } else { + selectedGroups = [...selectedGroups, groupId] + group.users.push(user) + } + + await groups.actions.save(group) + } + + async function fetchUser(userId) { + let userPromise = users.get(userId) + user = await userPromise + } + + async function toggleFlags(detail) { try { - await users.save({ ...$userFetch?.data, [flagName]: { global: detail } }) + await users.save({ ...$userFetch?.data, ...detail }) await userFetch.refresh() } catch (error) { notifications.error("Error updating user") } - toggleDisabled = false } - async function toggleBuilderAccess({ detail }) { - return toggleFlag("builder", detail) - } - - async function toggleAdminAccess({ detail }) { - return toggleFlag("admin", detail) - } - - async function openUpdateRolesModal({ detail }) { - selectedApp = detail - editRolesModal.show() - } + function addAll() {} + onMount(async () => { + try { + await groups.actions.init() + await apps.load() + } catch (error) { + notifications.error("Error getting User groups") + } + }) - +
- $goto("./")} - quiet - size="S" - icon="BackAndroid" - > - Back to users + $goto("./")} size="S" icon="ArrowLeft"> + Back
- User: {$userFetch?.data?.email} - - Change user settings and update their app roles. Also contains the ability - to delete the user as well as force reset their password. -
- + +
+
+
+ +
+ {$userFetch?.data?.firstName + + " " + + $userFetch?.data?.lastName} + {$userFetch?.data?.email} +
+
+
+
+ + + + + Force Password Reset + Delete + +
+
+
- General
-
- - -
-
- - {#if userId !== $auth.user._id}
- - -
-
- - Role + + + - + {#each userData as input, index} + + {/each} +
+ Add email +
+
- {#if basic} - + {#if hasGroupsLicense} + option.name} + getOptionValue={option => option._id} + /> {/if} - -
-
- - -
-
- - -
-
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte new file mode 100644 index 0000000000..d348082ffa --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte @@ -0,0 +1,22 @@ + + +
+
+ +
+ {parseInt(value?.length) || 0} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte new file mode 100644 index 0000000000..946fa430d2 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte @@ -0,0 +1,31 @@ + + + + + Are you sure you want to delete {user?.email} + + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/EmailSelect.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/EmailSelect.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte new file mode 100644 index 0000000000..772b5fe7b9 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte @@ -0,0 +1,36 @@ + + +
+
+ +
+ {#if value?.length === 0} +
0
+ {:else if value?.length === 1} +
+ {value[0]?.name} +
+ {:else} +
+ {parseInt(value?.length) || 0} groups +
+ {/if} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte new file mode 100644 index 0000000000..64334b4ab2 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte @@ -0,0 +1,157 @@ + + + createUsersFromCsv({ userEmails, usersRole, userGroups })} + disabled={!userEmails.length || !validEmails(userEmails) || !usersRole} +> + Import your users email addrresses from a CSV + +
+ + +
+ + + + {#if hasGroupsLicense} + option.name} + getOptionValue={option => option._id} + /> + {/if} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte new file mode 100644 index 0000000000..af61ea2d57 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte @@ -0,0 +1,38 @@ + + +
+ {#if value} +
+ x[0]) + .join("")} + /> +
+ {value} + {:else} +
Not Available
+ {/if} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte new file mode 100644 index 0000000000..7ec6d338d5 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte @@ -0,0 +1,108 @@ + + + chooseCreationType(selectedOnboardingType)} + disabled={!selectedOnboardingType} +> + +
{ + selectedOnboardingType = emailOnboardingKey + }} + > +
+ +
+ Send email invites +
+
+
+ {#if selectedOnboardingType == emailOnboardingKey} +
+ +
+ {/if} +
+
+ +
{ + selectedOnboardingType = basicOnboaridngKey + }} + > +
+ +
+ Generate passwords for each user +
+
+
+ {#if selectedOnboardingType == basicOnboaridngKey} +
+ +
+ {/if} +
+
+
+
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte new file mode 100644 index 0000000000..00e0c6eeab --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte @@ -0,0 +1,15 @@ + + +
+ {value} +
+ +
+
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte new file mode 100644 index 0000000000..e2995d8a02 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -0,0 +1,94 @@ + + + + All your new users can be accessed through the autogenerated passwords. + Make not of these passwords or download the csv + +
+
+ + +
+ Passwords CSV +
+
+
+ + + + + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte new file mode 100644 index 0000000000..4f481d374c --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte @@ -0,0 +1,16 @@ + + +
+ {value} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index 5a5f6c987a..952acaf324 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -1,52 +1,232 @@ + +
+ + {#if appGroups.length || appUsers.length} +
+ Access +
+ + Assign users to your app and define their access here + +
+
+ {#if hasGroupsLicense && appGroups.length} + + {#each appGroups as group} + + updateGroupRole(e.detail, group)} + autoWidth + quiet + value={group.roles[ + Object.keys(group.roles).find(x => x === fixedAppId) + ]} + /> + removeGroup(group)} + hoverable + size="S" + name="Close" + /> + + {/each} + + {/if} + {#if appUsers.length} + + {#each appUsers as user} + + updateUserRole(e.detail, user)} + autoWidth + quiet + value={user.roles[ + Object.keys(user.roles).find(x => x === fixedAppId) + ]} + /> + removeUser(user)} + hoverable + size="S" + name="Close" + /> + + {/each} + + + {/if} + {:else} +
+ + No users assigned +
+ Assign users to your app and set their access here +
+
+ +
+
+
+ {/if} +
+
+ + + + + + diff --git a/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte b/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte new file mode 100644 index 0000000000..aee7a8aa7d --- /dev/null +++ b/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte @@ -0,0 +1,103 @@ + + + addData(appData)} + showCloseIcon={false} +> + {#each appData as input, index} + group.name} + getPrimaryOptionValue={group => group.name} + getPrimaryOptionIcon={group => group.icon} + getPrimaryOptionColour={group => group.colour} + getSecondaryOptionLabel={role => role.name} + getSecondaryOptionValue={role => role._id} + getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)} + /> + {/each} + +
+ Add email +
+
diff --git a/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte index a1b9530c30..6693c285ff 100644 --- a/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte +++ b/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte @@ -1,16 +1,17 @@
@@ -132,6 +137,37 @@ {/if}
+ { + navigateTab("Access") + }} + dataCy={"access"} + > +
+ {#if $users?.data?.length} + +
+ {#each $users?.data as user} + + {/each} +
+ +
+ {$users?.data.length} users have access to this app +
+
+ {:else} + + No users +
+ No users have been assigned to this app +
+
+ {/if} +
+
{#if false}
@@ -186,6 +222,14 @@ grid-template-columns: repeat(auto-fill, minmax(30%, 1fr)); } + .users-tab { + display: flex; + gap: var(--spacing-m); + } + + .users-text { + color: var(--spectrum-global-color-gray-600); + } .overview-tab .bottom, .automation-metrics { display: grid; diff --git a/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte index a00694624b..8efa5a81e4 100644 --- a/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte +++ b/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte @@ -66,21 +66,26 @@ The app is currently using version {$store.version} - but version {clientPackage.version} is available. + but version {clientPackage.version} is + available. +
+ Updates can contain new features, performance improvements and bug + fixes. +
+ +
{:else} -

+

The app is currently using version {$store.version}. You're running the latest! -

+
+
+ +
{/if} - - Updates can contain new features, performance improvements and bug - fixes. - -
- -
diff --git a/packages/builder/src/pages/builder/portal/settings/theming.svelte b/packages/builder/src/pages/builder/portal/settings/theming.svelte index 2a8e82f0e5..ac5398a032 100644 --- a/packages/builder/src/pages/builder/portal/settings/theming.svelte +++ b/packages/builder/src/pages/builder/portal/settings/theming.svelte @@ -1,7 +1,6 @@ @@ -15,10 +14,11 @@