Merge pull request #7066 from Budibase/prod-user-fixes

Prod user fixes
This commit is contained in:
Andrew Kingston 2022-08-04 09:20:18 +01:00 committed by GitHub
commit feee950c3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 433 additions and 422 deletions

View File

@ -203,15 +203,24 @@ exports.getAllRoles = async appId => {
if (appId) { if (appId) {
return doWithDB(appId, internal) return doWithDB(appId, internal)
} else { } else {
return internal(getAppDB()) let appDB
try {
appDB = getAppDB()
} catch (error) {
// We don't have any apps, so we'll just use the built-in roles
}
return internal(appDB)
} }
async function internal(db) { async function internal(db) {
const body = await db.allDocs( let roles = []
getRoleParams(null, { if (db) {
include_docs: true, const body = await db.allDocs(
}) getRoleParams(null, {
) include_docs: true,
let roles = body.rows.map(row => row.doc) })
)
roles = body.rows.map(row => row.doc)
}
const builtinRoles = exports.getBuiltinRoles() const builtinRoles = exports.getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions) // need to combine builtin with any DB record of them (for sake of permissions)

View File

@ -15,7 +15,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let error = null export let error = null
export let secondaryOptions = [] export let secondaryOptions = []
@ -35,6 +34,7 @@
export let isOptionSelected = () => false export let isOptionSelected = () => false
export let isPlaceholder = false export let isPlaceholder = false
export let placeholderOption = null export let placeholderOption = null
export let showClearIcon = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let primaryOpen = false let primaryOpen = false
@ -50,17 +50,11 @@
} }
const updateValue = newValue => { const updateValue = newValue => {
if (readonly) {
return
}
dispatch("change", newValue) dispatch("change", newValue)
} }
const onClickSecondary = () => { const onClickSecondary = () => {
dispatch("click") dispatch("click")
if (readonly) {
return
}
secondaryOpen = true secondaryOpen = true
} }
@ -80,24 +74,15 @@
} }
const onBlur = event => { const onBlur = event => {
if (readonly) {
return
}
focus = false focus = false
updateValue(event.target.value) updateValue(event.target.value)
} }
const onInput = event => { const onInput = event => {
if (readonly || !updateOnChange) {
return
}
updateValue(event.target.value) updateValue(event.target.value)
} }
const updateValueOnEnter = event => { const updateValueOnEnter = event => {
if (readonly) {
return
}
if (event.key === "Enter") { if (event.key === "Enter") {
updateValue(event.target.value) updateValue(event.target.value)
} }
@ -140,11 +125,12 @@
value={primaryLabel || ""} value={primaryLabel || ""}
placeholder={placeholder || ""} placeholder={placeholder || ""}
{disabled} {disabled}
{readonly} readonly
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"
class:labelPadding={iconData} class:labelPadding={iconData}
class:open={primaryOpen}
/> />
{#if primaryValue} {#if primaryValue && showClearIcon}
<button <button
on:click={() => onClearPrimary()} on:click={() => onClearPrimary()}
type="reset" type="reset"
@ -198,7 +184,7 @@
</li> </li>
{/if} {/if}
{#each groupTitles as title} {#each groupTitles as title}
<div class="spectrum-Menu-item"> <div class="spectrum-Menu-item title">
<Detail>{title}</Detail> <Detail>{title}</Detail>
</div> </div>
{#if primaryOptions} {#if primaryOptions}
@ -433,4 +419,18 @@
.spectrum-Search-clearButton { .spectrum-Search-clearButton {
position: absolute; position: absolute;
} }
/* Fix focus borders to show only when opened */
.spectrum-Textfield-input {
border-color: var(--spectrum-global-color-gray-400) !important;
border-right-width: 1px;
}
.spectrum-Textfield-input.open {
border-color: var(--spectrum-global-color-blue-400) !important;
}
/* Fix being able to hover and select titles */
.spectrum-Menu-item.title {
pointer-events: none;
}
</style> </style>

View File

@ -27,6 +27,7 @@
export let primaryOptions = [] export let primaryOptions = []
export let secondaryOptions = [] export let secondaryOptions = []
export let searchTerm export let searchTerm
export let showClearIcon = true
let primaryLabel let primaryLabel
let secondaryLabel let secondaryLabel
@ -120,6 +121,7 @@
{secondaryValue} {secondaryValue}
{primaryLabel} {primaryLabel}
{secondaryLabel} {secondaryLabel}
{showClearIcon}
on:pickprimary={onPickPrimary} on:pickprimary={onPickPrimary}
on:picksecondary={onPickSecondary} on:picksecondary={onPickSecondary}
on:search={updateSearchTerm} on:search={updateSearchTerm}

View File

@ -9,11 +9,12 @@
export let avatar = false export let avatar = false
export let title = null export let title = null
export let subtitle = null export let subtitle = null
export let hoverable = false
$: initials = avatar ? title?.[0] : null $: initials = avatar ? title?.[0] : null
</script> </script>
<div class="list-item"> <div class="list-item" class:hoverable on:click>
<div class="left"> <div class="left">
{#if icon} {#if icon}
<div class="icon" style="background: {iconBackground || `transparent`};"> <div class="icon" style="background: {iconBackground || `transparent`};">
@ -39,11 +40,12 @@
.list-item { .list-item {
padding: 0 16px; padding: 0 16px;
height: 56px; height: 56px;
background: var(--spectrum-alias-background-color-tertiary); background: var(--spectrum-global-color-gray-50);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
transition: background 130ms ease-out;
} }
.list-item:not(:first-child) { .list-item:not(:first-child) {
border-top: none; border-top: none;
@ -56,6 +58,10 @@
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.hoverable:hover {
cursor: pointer;
background: var(--spectrum-global-color-gray-75);
}
.left, .left,
.right { .right {
display: flex; display: flex;

View File

@ -106,7 +106,9 @@
{/if} {/if}
{#if showCancelButton} {#if showCancelButton}
<Button group secondary on:click={close}>{cancelText}</Button> <Button group secondary newStyles on:click={close}>
{cancelText}
</Button>
{/if} {/if}
{#if showConfirmButton} {#if showConfirmButton}
<span class="confirm-wrap"> <span class="confirm-wrap">

View File

@ -503,12 +503,6 @@
.spectrum-Table-headCell--alignRight { .spectrum-Table-headCell--alignRight {
justify-content: flex-end; justify-content: flex-end;
} }
.spectrum-Table-headCell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
padding-left: var(--cell-padding);
}
.spectrum-Table-headCell--edit { .spectrum-Table-headCell--edit {
position: sticky; position: sticky;
left: 0; left: 0;
@ -580,13 +574,6 @@
background-color: var(--table-bg); background-color: var(--table-bg);
z-index: auto; z-index: auto;
} }
.spectrum-Table-cell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-cell--divider + .spectrum-Table-cell {
padding-left: var(--cell-padding);
}
.spectrum-Table-cell--edit { .spectrum-Table-cell--edit {
position: sticky; position: sticky;
left: 0; left: 0;

View File

@ -5,6 +5,7 @@
export let selectedRows export let selectedRows
export let deleteRows export let deleteRows
export let item = "row"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let modal let modal
@ -14,12 +15,14 @@
modal?.hide() modal?.hide()
dispatch("updaterows") dispatch("updaterows")
} }
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
</script> </script>
<Button icon="Delete" size="s" primary quiet on:click={modal.show}> <Button icon="Delete" size="s" primary quiet on:click={modal.show}>
Delete Delete
{selectedRows.length} {selectedRows.length}
row(s) {text}
</Button> </Button>
<ConfirmDialog <ConfirmDialog
bind:this={modal} bind:this={modal}
@ -29,5 +32,5 @@
> >
Are you sure you want to delete Are you sure you want to delete
{selectedRows.length} {selectedRows.length}
row{selectedRows.length > 1 ? "s" : ""}? {text}?
</ConfirmDialog> </ConfirmDialog>

View File

@ -27,7 +27,6 @@
import { AppStatus } from "constants" import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg" import Logo from "assets/bb-space-man.svg"
import AccessFilter from "./_components/AcessFilter.svelte" import AccessFilter from "./_components/AcessFilter.svelte"
import { Constants } from "@budibase/frontend-core"
let sortBy = "name" let sortBy = "name"
let template let template
@ -69,10 +68,6 @@
$: unlocked = lockedApps?.length === 0 $: unlocked = lockedApps?.length === 0
$: automationErrors = getAutomationErrors(enrichedApps) $: automationErrors = getAutomationErrors(enrichedApps)
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
const enrichApps = (apps, user, sortBy) => { const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({ const enrichedApps = apps.map(app => ({
...app, ...app,
@ -360,7 +355,7 @@
</Button> </Button>
{/if} {/if}
<div class="filter"> <div class="filter">
{#if hasGroupsLicense && $groups.length} {#if $auth.groupsEnabled && $groups.length}
<AccessFilter on:change={accessFilterAction} /> <AccessFilter on:change={accessFilterAction} />
{/if} {/if}
<Select <Select

View File

@ -12,7 +12,6 @@
$: wide = $: wide =
$page.path.includes("email/:template") || $page.path.includes("email/:template") ||
($page.path.includes("users") && !$page.path.includes(":userId")) ||
($page.path.includes("groups") && !$page.path.includes(":groupId")) ($page.path.includes("groups") && !$page.path.includes(":groupId"))
</script> </script>

View File

@ -11,7 +11,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, auth } from "stores/portal" import { groups, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { Constants } from "@budibase/frontend-core"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import UserGroupsRow from "./_components/UserGroupsRow.svelte" import UserGroupsRow from "./_components/UserGroupsRow.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -27,10 +26,6 @@
let modal let modal
let group = cloneDeep(DefaultGroup) let group = cloneDeep(DefaultGroup)
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
async function deleteGroup(group) { async function deleteGroup(group) {
try { try {
groups.actions.delete(group) groups.actions.delete(group)
@ -54,7 +49,7 @@
onMount(async () => { onMount(async () => {
try { try {
if (hasGroupsLicense) { if ($auth.groupsEnabled) {
await groups.actions.init() await groups.actions.init()
} }
} catch (error) { } catch (error) {
@ -67,7 +62,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<div style="display: flex;"> <div style="display: flex;">
<Heading size="M">User groups</Heading> <Heading size="M">User groups</Heading>
{#if !hasGroupsLicense} {#if !$auth.groupsEnabled}
<Tags> <Tags>
<div class="tags"> <div class="tags">
<div class="tag"> <div class="tag">
@ -82,15 +77,15 @@
<div class="align-buttons"> <div class="align-buttons">
<Button <Button
newStyles newStyles
icon={hasGroupsLicense ? "UserGroup" : ""} icon={$auth.groupsEnabled ? "UserGroup" : ""}
cta={hasGroupsLicense} cta={$auth.groupsEnabled}
on:click={hasGroupsLicense on:click={$auth.groupsEnabled
? showCreateGroupModal ? showCreateGroupModal
: window.open("https://budibase.com/pricing/", "_blank")} : window.open("https://budibase.com/pricing/", "_blank")}
> >
{hasGroupsLicense ? "Create user group" : "Upgrade Account"} {$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
</Button> </Button>
{#if !hasGroupsLicense} {#if !$auth.groupsEnabled}
<Button <Button
newStyles newStyles
secondary secondary
@ -101,7 +96,7 @@
{/if} {/if}
</div> </div>
{#if hasGroupsLicense && $groups.length} {#if $auth.groupsEnabled && $groups.length}
<div class="groupTable"> <div class="groupTable">
{#each $groups as group} {#each $groups as group}
<div> <div>

View File

@ -18,6 +18,7 @@
Select, Select,
Modal, Modal,
notifications, notifications,
Divider,
StatusLight, StatusLight,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -41,18 +42,13 @@
let allAppList = [] let allAppList = []
let user let user
let loaded = false let loaded = false
$: fetchUser(userId)
$: fetchUser(userId)
$: fullName = $userFetch?.data?.firstName $: fullName = $userFetch?.data?.firstName
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName ? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
: "" : ""
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: nameLabel = getNameLabel($userFetch) $: nameLabel = getNameLabel($userFetch)
$: initials = getInitials(nameLabel) $: initials = getInitials(nameLabel)
$: allAppList = $apps $: allAppList = $apps
.filter(x => { .filter(x => {
if ($userFetch.data?.roles) { if ($userFetch.data?.roles) {
@ -85,7 +81,6 @@
return y._id === userId return y._id === userId
}) })
}) })
$: globalRole = $userFetch?.data?.admin?.global $: globalRole = $userFetch?.data?.admin?.global
? "admin" ? "admin"
: $userFetch?.data?.builder?.global : $userFetch?.data?.builder?.global
@ -216,15 +211,14 @@
</script> </script>
{#if loaded} {#if loaded}
<Layout gap="L" noPadding> <Layout gap="XL" noPadding>
<Layout gap="XS" noPadding> <div>
<div> <ActionButton on:click={() => $goto("./")} icon="ArrowLeft">
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft"> Back
Back </ActionButton>
</ActionButton> </div>
</div>
</Layout> <Layout noPadding gap="M">
<Layout gap="XS" noPadding>
<div class="title"> <div class="title">
<div> <div>
<div style="display: flex;"> <div style="display: flex;">
@ -232,7 +226,7 @@
<div class="subtitle"> <div class="subtitle">
<Heading size="S">{nameLabel}</Heading> <Heading size="S">{nameLabel}</Heading>
{#if nameLabel !== $userFetch?.data?.email} {#if nameLabel !== $userFetch?.data?.email}
<Body size="XS">{$userFetch?.data?.email}</Body> <Body size="S">{$userFetch?.data?.email}</Body>
{/if} {/if}
</div> </div>
</div> </div>
@ -253,56 +247,62 @@
</div> </div>
{/if} {/if}
</div> </div>
</Layout> <Divider size="S" />
<Layout gap="S" noPadding> <Layout noPadding gap="S">
<div class="fields"> <Heading size="S">Details</Heading>
<div class="field"> <div class="fields">
<Label size="L">First name</Label>
<Input
thin
value={$userFetch?.data?.firstName}
on:blur={updateUserFirstName}
/>
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input
thin
value={$userFetch?.data?.lastName}
on:blur={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field"> <div class="field">
<Label size="L">Role</Label> <Label size="L">Email</Label>
<Select <Input disabled value={$userFetch?.data?.email} />
value={globalRole} </div>
options={Constants.BbRoles} <div class="field">
on:change={updateUserRole} <Label size="L">First name</Label>
<Input
value={$userFetch?.data?.firstName}
on:blur={updateUserFirstName}
/> />
</div> </div>
{/if} <div class="field">
</div> <Label size="L">Last name</Label>
<Input
value={$userFetch?.data?.lastName}
on:blur={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field">
<Label size="L">Role</Label>
<Select
value={globalRole}
options={Constants.BudibaseRoleOptions}
on:change={updateUserRole}
/>
</div>
{/if}
</div>
</Layout>
</Layout> </Layout>
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<!-- User groups --> <!-- User groups -->
<Layout gap="XS" noPadding> <Layout gap="S" noPadding>
<div class="tableTitle"> <div class="tableTitle">
<div> <Heading size="S">User groups</Heading>
<Heading size="XS">User groups</Heading>
<Body size="S">Add or remove this user from user groups</Body>
</div>
<div bind:this={popoverAnchor}> <div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserGroup" cta> <Button
Add user group on:click={popover.show()}
icon="UserGroup"
secondary
newStyles
>
Add to user group
</Button> </Button>
</div> </div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}> <Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker <UserGroupPicker
key={"name"} key={"name"}
title={"Group"} title={"User group"}
bind:searchTerm bind:searchTerm
bind:selected={selectedGroups} bind:selected={selectedGroups}
bind:filtered={filteredGroups} bind:filtered={filteredGroups}
@ -311,7 +311,6 @@
/> />
</Popover> </Popover>
</div> </div>
<List> <List>
{#if userGroups.length} {#if userGroups.length}
{#each userGroups as group} {#each userGroups as group}
@ -319,13 +318,16 @@
title={group.name} title={group.name}
icon={group.icon} icon={group.icon}
iconBackground={group.color} iconBackground={group.color}
><Icon hoverable
on:click={() => $goto(`../groups/${group._id}`)}
>
<Icon
on:click={removeGroup(group._id)} on:click={removeGroup(group._id)}
hoverable hoverable
size="L" size="S"
name="Close" name="Close"
/></ListItem />
> </ListItem>
{/each} {/each}
{:else} {:else}
<ListItem icon="UserGroup" title="No groups" /> <ListItem icon="UserGroup" title="No groups" />
@ -333,37 +335,28 @@
</List> </List>
</Layout> </Layout>
{/if} {/if}
<!-- User Apps -->
<Layout gap="S" noPadding>
<div class="appsTitle">
<Heading weight="light" size="XS">Apps</Heading>
<div style="margin-top: var(--spacing-xs)">
<Body size="S">Manage apps that this user has been assigned to</Body>
</div>
</div>
<Layout gap="S" noPadding>
<Heading size="S">Apps</Heading>
<List> <List>
{#if allAppList.length} {#if allAppList.length}
{#each allAppList as app} {#each allAppList as app}
<div <ListItem
class="pointer" title={app.name}
on:click={$goto(`../../overview/${app.devId}`)} iconBackground={app?.icon?.color || ""}
icon={app?.icon?.name || "Apps"}
hoverable
on:click={() => $goto(`../../overview/${app.devId}`)}
> >
<ListItem <div class="title ">
title={app.name} <StatusLight
iconBackground={app?.icon?.color || ""} square
icon={app?.icon?.name || "Apps"} color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
> >
<div class="title "> {getRoleLabel(getHighestRole(app.roles))}
<StatusLight </StatusLight>
square </div>
color={RoleUtils.getRoleColour(getHighestRole(app.roles))} </ListItem>
>
{getRoleLabel(getHighestRole(app.roles))}
</StatusLight>
</div>
</ListItem>
</div>
{/each} {/each}
{:else} {:else}
<ListItem icon="Apps" title="No apps" /> <ListItem icon="Apps" title="No apps" />
@ -384,16 +377,13 @@
</Modal> </Modal>
<style> <style>
.pointer {
cursor: pointer;
}
.fields { .fields {
display: grid; display: grid;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 32% 1fr; grid-template-columns: 120px 1fr;
align-items: center; align-items: center;
} }
@ -406,7 +396,7 @@
.tableTitle { .tableTitle {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--spacing-m); align-items: flex-end;
} }
.subtitle { .subtitle {
@ -416,9 +406,4 @@
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
} }
.appsTitle {
display: flex;
flex-direction: column;
}
</style> </style>

View File

@ -13,13 +13,10 @@
import { emailValidator } from "helpers/validation" import { emailValidator } from "helpers/validation"
export let showOnboardingTypeModal export let showOnboardingTypeModal
const password = Math.random().toString(36).substring(2, 22) const password = Math.random().toString(36).substring(2, 22)
let disabled let disabled
let userGroups = [] let userGroups = []
$: errors = []
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: userData = [ $: userData = [
{ {
@ -29,6 +26,7 @@
forceResetPassword: true, forceResetPassword: true,
}, },
] ]
$: hasError = userData.find(x => x.error != null)
function removeInput(idx) { function removeInput(idx) {
userData = userData.filter((e, i) => i !== idx) userData = userData.filter((e, i) => i !== idx)
@ -41,38 +39,49 @@
role: "appUser", role: "appUser",
password: Math.random().toString(36).substring(2, 22), password: Math.random().toString(36).substring(2, 22),
forceResetPassword: true, forceResetPassword: true,
error: null,
}, },
] ]
} }
function validateInput(email, index) { function validateInput(email, index) {
if (email) { if (email) {
if (emailValidator(email) === true) { const res = emailValidator(email)
errors[index] = true if (res === true) {
return null delete userData[index].error
} else { } else {
errors[index] = false userData[index].error = res
return emailValidator(email)
} }
} else {
userData[index].error = "Please enter an email address"
} }
return userData[index].error == null
}
const onConfirm = () => {
let valid = true
userData.forEach((input, index) => {
valid = validateInput(input.email, index) && valid
})
if (!valid) {
return false
}
showOnboardingTypeModal({ users: userData, groups: userGroups })
} }
</script> </script>
<ModalContent <ModalContent
onConfirm={async () => {onConfirm}
showOnboardingTypeModal({ users: userData, groups: userGroups })}
size="M" size="M"
title="Add new user" title="Add new users"
confirmText="Add user" confirmText="Add users"
confirmDisabled={disabled} confirmDisabled={disabled}
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
disabled={errors.some(x => x === false) || disabled={hasError || !userData.length}
userData.some(x => x.email === "" || x.email === null)}
> >
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Email Address</Label> <Label>Email address</Label>
{#each userData as input, index} {#each userData as input, index}
<div <div
style="display: flex; style="display: flex;
@ -84,15 +93,12 @@
inputType="email" inputType="email"
bind:inputValue={input.email} bind:inputValue={input.email}
bind:dropdownValue={input.role} bind:dropdownValue={input.role}
options={Constants.BbRoles} options={Constants.BudibaseRoleOptions}
error={validateInput(input.email, index)} error={input.error}
on:blur={() => validateInput(input.email, index)}
/> />
</div> </div>
<div <div class="icon">
class:fix-height={errors.length && !errors[index]}
class:normal-height={errors.length && !!errors[index]}
style="width: 10% "
>
<Icon <Icon
name="Close" name="Close"
hoverable hoverable
@ -107,11 +113,11 @@
</div> </div>
</Layout> </Layout>
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<Multiselect <Multiselect
bind:value={userGroups} bind:value={userGroups}
placeholder="Select User Groups" placeholder="No groups"
label="User Groups" label="Groups"
options={$groups} options={$groups}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option._id} getOptionValue={option => option._id}
@ -120,10 +126,9 @@
</ModalContent> </ModalContent>
<style> <style>
.fix-height { .icon {
margin-bottom: 5%; width: 10%;
} align-self: flex-start;
.normal-height { margin-top: 8px;
margin-bottom: 0%;
} }
</style> </style>

View File

@ -1,5 +1,6 @@
<script> <script>
import { Icon, Body } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
export let value export let value
</script> </script>
@ -7,17 +8,9 @@
<div class="spacing"> <div class="spacing">
<Icon name="UserGroup" /> <Icon name="UserGroup" />
</div> </div>
{#if value?.length === 0} <div class="opacity">
<div class="opacity">0</div> {value?.length || 0}
{:else if value?.length === 1} </div>
<div class="opacity">
<Body size="S">{value[0]?.name}</Body>
</div>
{:else}
<div class="opacity">
{parseInt(value?.length) || 0} groups
</div>
{/if}
</div> </div>
<style> <style>

View File

@ -7,7 +7,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, auth, admin } from "stores/portal" import { groups, auth, admin } from "stores/portal"
import { emailValidator } from "../../../../../../helpers/validation" import { emailValidator } from "helpers/validation"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
@ -22,9 +22,6 @@
let usersRole = null let usersRole = null
$: invalidEmails = [] $: invalidEmails = []
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
const validEmails = userEmails => { const validEmails = userEmails => {
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) { if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
@ -81,7 +78,7 @@
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })} onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
disabled={!userEmails.length || !validEmails(userEmails) || !usersRole} disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
> >
<Body size="S">Import your users email addrresses from a CSV</Body> <Body size="S">Import your users email addresses from a CSV file</Body>
<div class="dropzone"> <div class="dropzone">
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} /> <input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
@ -95,11 +92,11 @@
options={Constants.BuilderRoleDescriptions} options={Constants.BuilderRoleDescriptions}
/> />
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<Multiselect <Multiselect
bind:value={userGroups} bind:value={userGroups}
placeholder="Select User Groups" placeholder="No groups"
label="User Groups" label="Groups"
options={$groups} options={$groups}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option._id} getOptionValue={option => option._id}
@ -122,14 +119,12 @@
label { label {
font-family: var(--font-sans); font-family: var(--font-sans);
cursor: pointer;
font-weight: 600; font-weight: 600;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
color: var(--ink); color: var(--ink);
padding: var(--spacing-m) var(--spacing-l); padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex; display: inline-flex;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
min-width: auto; min-width: auto;
@ -141,10 +136,15 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
background-color: var(--grey-2); background: var(--spectrum-global-color-gray-200);
font-size: var(--font-size-xs); font-size: 12px;
line-height: normal; line-height: normal;
border: var(--border-transparent); border: var(--border-transparent);
transition: background-color 130ms ease-out;
}
label:hover {
background: var(--spectrum-global-color-gray-300);
cursor: pointer;
} }
input[type="file"] { input[type="file"] {

View File

@ -49,10 +49,10 @@
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
> >
<Body size="XS" <Body size="XS">
>All your new users can be accessed through the autogenerated passwords. All your new users can be accessed through the autogenerated passwords. Take
Make not of these passwords or download the csv</Body note of these passwords or download the CSV file.
> </Body>
<div class="container" on:click={downloadCsvFile}> <div class="container" on:click={downloadCsvFile}>
<div class="inner"> <div class="inner">

View File

@ -3,14 +3,20 @@
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
export let row export let row
$: value =
Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label || const TooltipMap = {
"Not Available" appUser: "Only has access to published apps",
developer: "Access to the app builder",
admin: "Full access",
}
$: role = Constants.BudibaseRoleOptions.find(
x => x.value === users.getUserRole(row)
)
$: value = role?.label || "Not available"
$: tooltip = TooltipMap[role?.value] || ""
</script> </script>
<div on:click|stopPropagation> <div on:click|stopPropagation title={tooltip}>
{value} {value}
</div> </div>
<style>
</style>

View File

@ -8,11 +8,10 @@
Layout, Layout,
Modal, Modal,
ModalContent, ModalContent,
Icon, Search,
notifications, notifications,
Pagination, Pagination,
Search, Divider,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import AddUserModal from "./_components/AddUserModal.svelte" import AddUserModal from "./_components/AddUserModal.svelte"
import { users, groups, auth } from "stores/portal" import { users, groups, auth } from "stores/portal"
@ -20,69 +19,42 @@
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte" import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte" import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte" import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
import NameTableRenderer from "./_components/NameTableRenderer.svelte"
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte" import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte" import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
import PasswordModal from "./_components/PasswordModal.svelte" import PasswordModal from "./_components/PasswordModal.svelte"
import ImportUsersModal from "./_components/ImportUsersModal.svelte" import ImportUsersModal from "./_components/ImportUsersModal.svelte"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { Constants } from "@budibase/frontend-core"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core"
const accessTypes = [
{
icon: "User",
description: "App user - Only has access to published apps",
},
{
icon: "Hammer",
description: "Developer - Access to the app builder",
},
{
icon: "Draw",
description: "Admin - Full access",
},
]
//let email
let enrichedUsers = [] let enrichedUsers = []
let createUserModal, let createUserModal,
inviteConfirmationModal, inviteConfirmationModal,
onboardingTypeModal, onboardingTypeModal,
passwordModal, passwordModal,
importUsersModal importUsersModal
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let prevEmail = undefined, let prevEmail = undefined,
searchEmail = undefined searchEmail = undefined
let selectedRows = [] let selectedRows = []
let customRenderers = [ let customRenderers = [
{ column: "userGroups", component: GroupsTableRenderer }, { column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer }, { column: "apps", component: AppsTableRenderer },
{ column: "name", component: NameTableRenderer },
{ column: "role", component: RoleTableRenderer }, { column: "role", component: RoleTableRenderer },
] ]
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: schema = { $: schema = {
name: {},
email: {}, email: {},
role: { role: {
sortable: false, sortable: false,
}, },
...(hasGroupsLicense && { ...($auth.groupsEnabled && {
userGroups: { sortable: false, displayName: "User groups" }, userGroups: { sortable: false, displayName: "Groups" },
}), }),
apps: {}, apps: {},
} }
$: userData = [] $: userData = []
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, searchEmail) $: fetchUsers(page, searchEmail)
$: { $: {
@ -105,6 +77,7 @@
} }
}) })
} }
const showOnboardingTypeModal = async addUsersData => { const showOnboardingTypeModal = async addUsersData => {
userData = await removingDuplicities(addUsersData) userData = await removingDuplicities(addUsersData)
if (!userData?.users?.length) return if (!userData?.users?.length) return
@ -113,13 +86,13 @@
} }
async function createUserFlow() { async function createUserFlow() {
let emails = userData?.users?.map(x => x.email) || [] const payload = userData?.users?.map(user => ({
email: user.email,
builder: user.role === Constants.BudibaseRoles.Developer,
admin: user.role === Constants.BudibaseRoles.Admin,
}))
try { try {
const res = await users.invite({ const res = await users.invite(payload)
emails: emails,
builder: false,
admin: false,
})
notifications.success(res.message) notifications.success(res.message)
inviteConfirmationModal.show() inviteConfirmationModal.show()
} catch (error) { } catch (error) {
@ -232,23 +205,13 @@
} }
</script> </script>
<Layout noPadding> <Layout noPadding gap="M">
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading>Users</Heading> <Heading>Users</Heading>
<Body>Add users and control who gets access to your published apps</Body> <Body>Add users and control who gets access to your published apps</Body>
<div>
{#each accessTypes as type}
<div class="access-description">
<Icon name={type.icon} />
<div class="access-text">
<Body size="S">{type.description}</Body>
</div>
</div>
{/each}
</div>
</Layout> </Layout>
<Layout gap="S" noPadding> <Divider size="S" />
<div class="controls">
<ButtonGroup> <ButtonGroup>
<Button <Button
dataCy="add-user" dataCy="add-user"
@ -256,39 +219,47 @@
icon="UserAdd" icon="UserAdd"
cta>Add users</Button cta>Add users</Button
> >
<Button on:click={importUsersModal.show} icon="Import" primary <Button
>Import users</Button on:click={importUsersModal.show}
icon="Import"
secondary
newStyles
> >
Import users
<div class="field"> </Button>
<Label size="L">Search email</Label>
<Search bind:value={searchEmail} placeholder="" />
</div>
{#if selectedRows.length > 0}
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
{/if}
</ButtonGroup> </ButtonGroup>
<Table <div class="controls-right">
on:click={({ detail }) => $goto(`./${detail._id}`)} <Search bind:value={searchEmail} placeholder="Search email" />
{schema} {#if selectedRows.length > 0}
bind:selectedRows <DeleteRowsButton
data={enrichedUsers} item="user"
allowEditColumns={false} on:updaterows
allowEditRows={false} {selectedRows}
allowSelectRows={true} {deleteRows}
showHeaderBorder={false} />
{customRenderers} {/if}
/>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div> </div>
</Layout> </div>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
bind:selectedRows
data={enrichedUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={true}
showHeaderBorder={false}
{customRenderers}
/>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>
@ -325,28 +296,22 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
margin-top: var(--spacing-xl);
} }
.field { .controls {
display: flex; display: flex;
align-items: center;
flex-direction: row; flex-direction: row;
grid-gap: var(--spacing-m); justify-content: space-between;
margin-left: auto; align-items: center;
} }
.controls-right {
.field > :global(*) + :global(*) {
margin-left: var(--spacing-m);
}
.access-description {
display: flex; display: flex;
margin-top: var(--spacing-xl); flex-direction: row;
opacity: 0.8; justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
} }
.controls-right :global(.spectrum-Search) {
.access-text { width: 200px;
margin-left: var(--spacing-m);
} }
</style> </style>

View File

@ -17,10 +17,10 @@
import { users, groups, apps, auth } from "stores/portal" import { users, groups, apps, auth } from "stores/portal"
import AssignmentModal from "./AssignmentModal.svelte" import AssignmentModal from "./AssignmentModal.svelte"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { Constants } from "@budibase/frontend-core"
import { roles } from "stores/backend" import { roles } from "stores/backend"
export let app export let app
let assignmentModal let assignmentModal
let appGroups = [] let appGroups = []
let appUsers = [] let appUsers = []
@ -28,14 +28,9 @@
search = undefined search = undefined
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let fixedAppId let fixedAppId
$: page = $pageInfo.page $: page = $pageInfo.page
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: fixedAppId = apps.getProdAppID(app.devId) $: fixedAppId = apps.getProdAppID(app.devId)
$: appGroups = $groups.filter(x => { $: appGroups = $groups.filter(x => {
return x.apps.includes(app.appId) return x.apps.includes(app.appId)
}) })
@ -161,7 +156,7 @@
> >
</div> </div>
</div> </div>
{#if hasGroupsLicense && appGroups.length} {#if $auth.groupsEnabled && appGroups.length}
<List title="User Groups"> <List title="User Groups">
{#each appGroups as group} {#each appGroups as group}
<ListItem <ListItem

View File

@ -4,22 +4,65 @@
PickerDropdown, PickerDropdown,
ActionButton, ActionButton,
Layout, Layout,
Icon,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { groups, users } from "stores/portal" import { groups, users, auth } from "stores/portal"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
export let app export let app
export let addData export let addData
export let appUsers = [] export let appUsers = []
let prevSearch = undefined, let prevSearch = undefined,
search = undefined search = undefined
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let appData = [{ id: "", role: "" }]
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, search) $: fetchUsers(page, search)
$: availableUsers = getAvailableUsers($users, appUsers, appData)
$: filteredGroups = $groups.filter(group => {
return !group.apps.find(appId => {
return appId === app.appId
})
})
$: valid =
appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length)
$: optionSections = {
...($auth.groupsEnabled &&
filteredGroups.length && {
["User groups"]: {
data: filteredGroups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
}),
users: {
data: availableUsers,
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
}
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
return (allUsers.data || []).filter(user => {
// Filter out assigned users
if (appUsers.find(x => x._id === user._id)) {
return false
}
// Filter out new users which are going to be assigned
return !newUsers.find(x => x.id === user._id)
})
}
async function fetchUsers(page, search) { async function fetchUsers(page, search) {
if ($pageInfo.loading) { if ($pageInfo.loading) {
return return
@ -39,36 +82,13 @@
} }
} }
$: filteredGroups = $groups.filter(group => {
return !group.apps.find(appId => {
return appId === app.appId
})
})
$: optionSections = {
...(filteredGroups.length && {
groups: {
data: filteredGroups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
}),
users: {
data: $users.data.filter(u => !appUsers.find(x => x._id === u._id)),
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
}
$: appData = [{ id: "", role: "" }]
function addNewInput() { function addNewInput() {
appData = [...appData, { id: "", role: "" }] appData = [...appData, { id: "", role: "" }]
} }
const removeItem = index => {
appData = appData.filter((x, idx) => idx !== index)
}
</script> </script>
<ModalContent <ModalContent
@ -78,28 +98,61 @@
cancelText="Cancel" cancelText="Cancel"
onConfirm={() => addData(appData)} onConfirm={() => addData(appData)}
showCloseIcon={false} showCloseIcon={false}
disabled={!valid}
> >
<Layout noPadding gap="XS"> {#if appData?.length}
{#each appData as input, index} <Layout noPadding gap="XS">
<PickerDropdown {#each appData as input, index}
autocomplete <div class="item">
primaryOptions={optionSections} <div class="picker">
secondaryOptions={$roles} <PickerDropdown
secondaryPlaceholder="Access" autocomplete
bind:primaryValue={input.id} showClearIcon={false}
bind:secondaryValue={input.role} primaryOptions={optionSections}
bind:searchTerm={search} secondaryOptions={$roles}
getPrimaryOptionLabel={group => group.name} secondaryPlaceholder="Access"
getPrimaryOptionValue={group => group.name} bind:primaryValue={input.id}
getPrimaryOptionIcon={group => group.icon} bind:secondaryValue={input.role}
getPrimaryOptionColour={group => group.colour} bind:searchTerm={search}
getSecondaryOptionLabel={role => role.name} getPrimaryOptionLabel={group => group.name}
getSecondaryOptionValue={role => role._id} getPrimaryOptionValue={group => group.name}
getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)} getPrimaryOptionIcon={group => group.icon}
/> getPrimaryOptionColour={group => group.colour}
{/each} getSecondaryOptionLabel={role => role.name}
</Layout> getSecondaryOptionValue={role => role._id}
getSecondaryOptionColour={role =>
RoleUtils.getRoleColour(role._id)}
/>
</div>
<div class="icon">
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeItem(index)}
/>
</div>
</div>
{/each}
</Layout>
{/if}
<div> <div>
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton> <ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
</div> </div>
</ModalContent> </ModalContent>
<style>
.item {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.picker {
width: calc(100% - 30px);
}
.icon {
width: 20px;
}
</style>

View File

@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import analytics from "analytics" import analytics from "analytics"
import { FEATURE_FLAGS } from "helpers/featureFlags"
import { Constants } from "@budibase/frontend-core"
export function createAuthStore() { export function createAuthStore() {
const auth = writable({ const auth = writable({
@ -10,11 +12,13 @@ export function createAuthStore() {
tenantSet: false, tenantSet: false,
loaded: false, loaded: false,
postLogout: false, postLogout: false,
groupsEnabled: false,
}) })
const store = derived(auth, $store => { const store = derived(auth, $store => {
let initials = null let initials = null
let isAdmin = false let isAdmin = false
let isBuilder = false let isBuilder = false
let groupsEnabled = false
if ($store.user) { if ($store.user) {
const user = $store.user const user = $store.user
if (user.firstName) { if (user.firstName) {
@ -29,6 +33,9 @@ export function createAuthStore() {
} }
isAdmin = !!user.admin?.global isAdmin = !!user.admin?.global
isBuilder = !!user.builder?.global isBuilder = !!user.builder?.global
groupsEnabled =
user?.license.features.includes(Constants.Features.USER_GROUPS) &&
user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS)
} }
return { return {
user: $store.user, user: $store.user,
@ -39,6 +46,7 @@ export function createAuthStore() {
initials, initials,
isAdmin, isAdmin,
isBuilder, isBuilder,
groupsEnabled,
} }
}) })

View File

@ -26,12 +26,8 @@ export function createUsersStore() {
return await API.getUsers() return await API.getUsers()
} }
async function invite({ emails, builder, admin }) { async function invite(payload) {
return API.inviteUsers({ return API.inviteUsers(payload)
emails,
builder,
admin,
})
} }
async function acceptInvite(inviteCode, password) { async function acceptInvite(inviteCode, password) {
return API.acceptInvite({ return API.acceptInvite({

View File

@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({
/** /**
* Invites multiple users to the current tenant. * Invites multiple users to the current tenant.
* @param email An array of email addresses * @param users An array of users to invite
* @param builder whether the user should be a global builder
* @param admin whether the user should be a global admin
*/ */
inviteUsers: async ({ emails, builder, admin }) => { inviteUsers: async users => {
return await API.post({ return await API.post({
url: "/api/global/users/inviteMultiple", url: "/api/global/users/multi/invite",
body: { body: users.map(user => ({
emails, email: user.email,
userInfo: { userInfo: {
admin: admin ? { global: true } : undefined, admin: user.admin ? { global: true } : undefined,
builder: builder ? { global: true } : undefined, builder: user.admin || user.builder ? { global: true } : undefined,
}, },
}, })),
}) })
}, },

View File

@ -60,25 +60,31 @@ export const TableNames = {
USERS: "ta_users", USERS: "ta_users",
} }
export const BbRoles = [ export const BudibaseRoles = {
{ label: "App User", value: "appUser" }, AppUser: "appUser",
{ label: "Developer", value: "developer" }, Developer: "developer",
{ label: "Admin", value: "admin" }, Admin: "admin",
}
export const BudibaseRoleOptions = [
{ label: "App User", value: BudibaseRoles.AppUser },
{ label: "Developer", value: BudibaseRoles.Developer },
{ label: "Admin", value: BudibaseRoles.Admin },
] ]
export const BuilderRoleDescriptions = [ export const BuilderRoleDescriptions = [
{ {
value: "appUser", value: BudibaseRoles.AppUser,
icon: "User", icon: "User",
label: "App user - Only has access to published apps", label: "App user - Only has access to published apps",
}, },
{ {
value: "developer", value: BudibaseRoles.Developer,
icon: "Hammer", icon: "Hammer",
label: "Developer - Access to the app builder", label: "Developer - Access to the app builder",
}, },
{ {
value: "admin", value: BudibaseRoles.Admin,
icon: "Draw", icon: "Draw",
label: "Admin - Full access", label: "Admin - Full access",
}, },

View File

@ -214,13 +214,13 @@ export const invite = async (ctx: any) => {
} }
export const inviteMultiple = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => {
let { emails, userInfo } = ctx.request.body let users = ctx.request.body
let existing = false let existing = false
let existingEmail let existingEmail
for (let email of emails) { for (let user of users) {
if (await usersCore.getGlobalUserByEmail(email)) { if (await usersCore.getGlobalUserByEmail(user.email)) {
existing = true existing = true
existingEmail = email existingEmail = user.email
break break
} }
} }
@ -228,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => {
if (existing) { if (existing) {
ctx.throw(400, `${existingEmail} already exists`) ctx.throw(400, `${existingEmail} already exists`)
} }
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenancy.getTenantId()
const opts: any = {
subject: "{{ company }} platform invitation",
info: userInfo,
}
for (let i = 0; i < emails.length; i++) { for (let i = 0; i < users.length; i++) {
await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts) let userInfo = users[i].userInfo
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenancy.getTenantId()
const opts: any = {
subject: "{{ company }} platform invitation",
info: userInfo,
}
await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
} }
ctx.body = { ctx.body = {

View File

@ -32,10 +32,12 @@ function buildInviteValidation() {
function buildInviteMultipleValidation() { function buildInviteMultipleValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.array().required().items(
emails: Joi.array().required(), Joi.object({
userInfo: Joi.object().optional(), email: Joi.string(),
}).required()) userInfo: Joi.object().optional(),
})
))
} }
function buildInviteAcceptValidation() { function buildInviteAcceptValidation() {
@ -79,7 +81,7 @@ router
controller.invite controller.invite
) )
.post( .post(
"/api/global/users/inviteMultiple", "/api/global/users/multi/invite",
adminOnly, adminOnly,
buildInviteMultipleValidation(), buildInviteMultipleValidation(),
controller.inviteMultiple controller.inviteMultiple