commit
feee950c3d
|
@ -203,15 +203,24 @@ exports.getAllRoles = async appId => {
|
|||
if (appId) {
|
||||
return doWithDB(appId, internal)
|
||||
} 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) {
|
||||
const body = await db.allDocs(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
let roles = body.rows.map(row => row.doc)
|
||||
let roles = []
|
||||
if (db) {
|
||||
const body = await db.allDocs(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
roles = body.rows.map(row => row.doc)
|
||||
}
|
||||
const builtinRoles = exports.getBuiltinRoles()
|
||||
|
||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
export let id = null
|
||||
export let placeholder = "Choose an option or type"
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let error = null
|
||||
export let secondaryOptions = []
|
||||
|
@ -35,6 +34,7 @@
|
|||
export let isOptionSelected = () => false
|
||||
export let isPlaceholder = false
|
||||
export let placeholderOption = null
|
||||
export let showClearIcon = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let primaryOpen = false
|
||||
|
@ -50,17 +50,11 @@
|
|||
}
|
||||
|
||||
const updateValue = newValue => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
||||
const onClickSecondary = () => {
|
||||
dispatch("click")
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
secondaryOpen = true
|
||||
}
|
||||
|
||||
|
@ -80,24 +74,15 @@
|
|||
}
|
||||
|
||||
const onBlur = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
@ -140,11 +125,12 @@
|
|||
value={primaryLabel || ""}
|
||||
placeholder={placeholder || ""}
|
||||
{disabled}
|
||||
{readonly}
|
||||
readonly
|
||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||
class:labelPadding={iconData}
|
||||
class:open={primaryOpen}
|
||||
/>
|
||||
{#if primaryValue}
|
||||
{#if primaryValue && showClearIcon}
|
||||
<button
|
||||
on:click={() => onClearPrimary()}
|
||||
type="reset"
|
||||
|
@ -198,7 +184,7 @@
|
|||
</li>
|
||||
{/if}
|
||||
{#each groupTitles as title}
|
||||
<div class="spectrum-Menu-item">
|
||||
<div class="spectrum-Menu-item title">
|
||||
<Detail>{title}</Detail>
|
||||
</div>
|
||||
{#if primaryOptions}
|
||||
|
@ -433,4 +419,18 @@
|
|||
.spectrum-Search-clearButton {
|
||||
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>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
export let primaryOptions = []
|
||||
export let secondaryOptions = []
|
||||
export let searchTerm
|
||||
export let showClearIcon = true
|
||||
|
||||
let primaryLabel
|
||||
let secondaryLabel
|
||||
|
@ -120,6 +121,7 @@
|
|||
{secondaryValue}
|
||||
{primaryLabel}
|
||||
{secondaryLabel}
|
||||
{showClearIcon}
|
||||
on:pickprimary={onPickPrimary}
|
||||
on:picksecondary={onPickSecondary}
|
||||
on:search={updateSearchTerm}
|
||||
|
|
|
@ -9,11 +9,12 @@
|
|||
export let avatar = false
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
export let hoverable = false
|
||||
|
||||
$: initials = avatar ? title?.[0] : null
|
||||
</script>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item" class:hoverable on:click>
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||
|
@ -39,11 +40,12 @@
|
|||
.list-item {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.list-item:not(:first-child) {
|
||||
border-top: none;
|
||||
|
@ -56,6 +58,10 @@
|
|||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
|
|
|
@ -106,7 +106,9 @@
|
|||
{/if}
|
||||
|
||||
{#if showCancelButton}
|
||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
<Button group secondary newStyles on:click={close}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if showConfirmButton}
|
||||
<span class="confirm-wrap">
|
||||
|
|
|
@ -503,12 +503,6 @@
|
|||
.spectrum-Table-headCell--alignRight {
|
||||
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 {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
@ -580,13 +574,6 @@
|
|||
background-color: var(--table-bg);
|
||||
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 {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export let selectedRows
|
||||
export let deleteRows
|
||||
export let item = "row"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let modal
|
||||
|
@ -14,12 +15,14 @@
|
|||
modal?.hide()
|
||||
dispatch("updaterows")
|
||||
}
|
||||
|
||||
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||
</script>
|
||||
|
||||
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
||||
Delete
|
||||
{selectedRows.length}
|
||||
row(s)
|
||||
{text}
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
bind:this={modal}
|
||||
|
@ -29,5 +32,5 @@
|
|||
>
|
||||
Are you sure you want to delete
|
||||
{selectedRows.length}
|
||||
row{selectedRows.length > 1 ? "s" : ""}?
|
||||
{text}?
|
||||
</ConfirmDialog>
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
import { AppStatus } from "constants"
|
||||
import Logo from "assets/bb-space-man.svg"
|
||||
import AccessFilter from "./_components/AcessFilter.svelte"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
let sortBy = "name"
|
||||
let template
|
||||
|
@ -69,10 +68,6 @@
|
|||
$: unlocked = lockedApps?.length === 0
|
||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
...app,
|
||||
|
@ -360,7 +355,7 @@
|
|||
</Button>
|
||||
{/if}
|
||||
<div class="filter">
|
||||
{#if hasGroupsLicense && $groups.length}
|
||||
{#if $auth.groupsEnabled && $groups.length}
|
||||
<AccessFilter on:change={accessFilterAction} />
|
||||
{/if}
|
||||
<Select
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
$: wide =
|
||||
$page.path.includes("email/:template") ||
|
||||
($page.path.includes("users") && !$page.path.includes(":userId")) ||
|
||||
($page.path.includes("groups") && !$page.path.includes(":groupId"))
|
||||
</script>
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
} from "@budibase/bbui"
|
||||
import { groups, auth } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -27,10 +26,6 @@
|
|||
let modal
|
||||
let group = cloneDeep(DefaultGroup)
|
||||
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
async function deleteGroup(group) {
|
||||
try {
|
||||
groups.actions.delete(group)
|
||||
|
@ -54,7 +49,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
if (hasGroupsLicense) {
|
||||
if ($auth.groupsEnabled) {
|
||||
await groups.actions.init()
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -67,7 +62,7 @@
|
|||
<Layout gap="XS" noPadding>
|
||||
<div style="display: flex;">
|
||||
<Heading size="M">User groups</Heading>
|
||||
{#if !hasGroupsLicense}
|
||||
{#if !$auth.groupsEnabled}
|
||||
<Tags>
|
||||
<div class="tags">
|
||||
<div class="tag">
|
||||
|
@ -82,15 +77,15 @@
|
|||
<div class="align-buttons">
|
||||
<Button
|
||||
newStyles
|
||||
icon={hasGroupsLicense ? "UserGroup" : ""}
|
||||
cta={hasGroupsLicense}
|
||||
on:click={hasGroupsLicense
|
||||
icon={$auth.groupsEnabled ? "UserGroup" : ""}
|
||||
cta={$auth.groupsEnabled}
|
||||
on:click={$auth.groupsEnabled
|
||||
? showCreateGroupModal
|
||||
: window.open("https://budibase.com/pricing/", "_blank")}
|
||||
>
|
||||
{hasGroupsLicense ? "Create user group" : "Upgrade Account"}
|
||||
{$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
|
||||
</Button>
|
||||
{#if !hasGroupsLicense}
|
||||
{#if !$auth.groupsEnabled}
|
||||
<Button
|
||||
newStyles
|
||||
secondary
|
||||
|
@ -101,7 +96,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasGroupsLicense && $groups.length}
|
||||
{#if $auth.groupsEnabled && $groups.length}
|
||||
<div class="groupTable">
|
||||
{#each $groups as group}
|
||||
<div>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
Select,
|
||||
Modal,
|
||||
notifications,
|
||||
Divider,
|
||||
StatusLight,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
|
@ -41,18 +42,13 @@
|
|||
let allAppList = []
|
||||
let user
|
||||
let loaded = false
|
||||
$: fetchUser(userId)
|
||||
|
||||
$: fetchUser(userId)
|
||||
$: fullName = $userFetch?.data?.firstName
|
||||
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
|
||||
: ""
|
||||
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
$: nameLabel = getNameLabel($userFetch)
|
||||
$: initials = getInitials(nameLabel)
|
||||
|
||||
$: allAppList = $apps
|
||||
.filter(x => {
|
||||
if ($userFetch.data?.roles) {
|
||||
|
@ -85,7 +81,6 @@
|
|||
return y._id === userId
|
||||
})
|
||||
})
|
||||
|
||||
$: globalRole = $userFetch?.data?.admin?.global
|
||||
? "admin"
|
||||
: $userFetch?.data?.builder?.global
|
||||
|
@ -216,15 +211,14 @@
|
|||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Layout gap="L" noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<div>
|
||||
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft">
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Layout gap="XL" noPadding>
|
||||
<div>
|
||||
<ActionButton on:click={() => $goto("./")} icon="ArrowLeft">
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Layout noPadding gap="M">
|
||||
<div class="title">
|
||||
<div>
|
||||
<div style="display: flex;">
|
||||
|
@ -232,7 +226,7 @@
|
|||
<div class="subtitle">
|
||||
<Heading size="S">{nameLabel}</Heading>
|
||||
{#if nameLabel !== $userFetch?.data?.email}
|
||||
<Body size="XS">{$userFetch?.data?.email}</Body>
|
||||
<Body size="S">{$userFetch?.data?.email}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -253,56 +247,62 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<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}
|
||||
<Divider size="S" />
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Details</Heading>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Role</Label>
|
||||
<Select
|
||||
value={globalRole}
|
||||
options={Constants.BbRoles}
|
||||
on:change={updateUserRole}
|
||||
<Label size="L">Email</Label>
|
||||
<Input disabled value={$userFetch?.data?.email} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">First name</Label>
|
||||
<Input
|
||||
value={$userFetch?.data?.firstName}
|
||||
on:blur={updateUserFirstName}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="field">
|
||||
<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>
|
||||
|
||||
{#if hasGroupsLicense}
|
||||
{#if $auth.groupsEnabled}
|
||||
<!-- User groups -->
|
||||
<Layout gap="XS" noPadding>
|
||||
<Layout gap="S" noPadding>
|
||||
<div class="tableTitle">
|
||||
<div>
|
||||
<Heading size="XS">User groups</Heading>
|
||||
<Body size="S">Add or remove this user from user groups</Body>
|
||||
</div>
|
||||
<Heading size="S">User groups</Heading>
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button on:click={popover.show()} icon="UserGroup" cta>
|
||||
Add user group
|
||||
<Button
|
||||
on:click={popover.show()}
|
||||
icon="UserGroup"
|
||||
secondary
|
||||
newStyles
|
||||
>
|
||||
Add to user group
|
||||
</Button>
|
||||
</div>
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
key={"name"}
|
||||
title={"Group"}
|
||||
title={"User group"}
|
||||
bind:searchTerm
|
||||
bind:selected={selectedGroups}
|
||||
bind:filtered={filteredGroups}
|
||||
|
@ -311,7 +311,6 @@
|
|||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<List>
|
||||
{#if userGroups.length}
|
||||
{#each userGroups as group}
|
||||
|
@ -319,13 +318,16 @@
|
|||
title={group.name}
|
||||
icon={group.icon}
|
||||
iconBackground={group.color}
|
||||
><Icon
|
||||
hoverable
|
||||
on:click={() => $goto(`../groups/${group._id}`)}
|
||||
>
|
||||
<Icon
|
||||
on:click={removeGroup(group._id)}
|
||||
hoverable
|
||||
size="L"
|
||||
size="S"
|
||||
name="Close"
|
||||
/></ListItem
|
||||
>
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="No groups" />
|
||||
|
@ -333,37 +335,28 @@
|
|||
</List>
|
||||
</Layout>
|
||||
{/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>
|
||||
{#if allAppList.length}
|
||||
{#each allAppList as app}
|
||||
<div
|
||||
class="pointer"
|
||||
on:click={$goto(`../../overview/${app.devId}`)}
|
||||
<ListItem
|
||||
title={app.name}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
hoverable
|
||||
on:click={() => $goto(`../../overview/${app.devId}`)}
|
||||
>
|
||||
<ListItem
|
||||
title={app.name}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
|
||||
>
|
||||
{getRoleLabel(getHighestRole(app.roles))}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
|
||||
>
|
||||
{getRoleLabel(getHighestRole(app.roles))}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="Apps" title="No apps" />
|
||||
|
@ -384,16 +377,13 @@
|
|||
</Modal>
|
||||
|
||||
<style>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 32% 1fr;
|
||||
grid-template-columns: 120px 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
@ -406,7 +396,7 @@
|
|||
.tableTitle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-m);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
|
@ -416,9 +406,4 @@
|
|||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.appsTitle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,13 +13,10 @@
|
|||
import { emailValidator } from "helpers/validation"
|
||||
|
||||
export let showOnboardingTypeModal
|
||||
|
||||
const password = Math.random().toString(36).substring(2, 22)
|
||||
let disabled
|
||||
let userGroups = []
|
||||
$: errors = []
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
$: userData = [
|
||||
{
|
||||
|
@ -29,6 +26,7 @@
|
|||
forceResetPassword: true,
|
||||
},
|
||||
]
|
||||
$: hasError = userData.find(x => x.error != null)
|
||||
|
||||
function removeInput(idx) {
|
||||
userData = userData.filter((e, i) => i !== idx)
|
||||
|
@ -41,38 +39,49 @@
|
|||
role: "appUser",
|
||||
password: Math.random().toString(36).substring(2, 22),
|
||||
forceResetPassword: true,
|
||||
error: null,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function validateInput(email, index) {
|
||||
if (email) {
|
||||
if (emailValidator(email) === true) {
|
||||
errors[index] = true
|
||||
return null
|
||||
const res = emailValidator(email)
|
||||
if (res === true) {
|
||||
delete userData[index].error
|
||||
} else {
|
||||
errors[index] = false
|
||||
return emailValidator(email)
|
||||
userData[index].error = res
|
||||
}
|
||||
} 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>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={async () =>
|
||||
showOnboardingTypeModal({ users: userData, groups: userGroups })}
|
||||
{onConfirm}
|
||||
size="M"
|
||||
title="Add new user"
|
||||
confirmText="Add user"
|
||||
title="Add new users"
|
||||
confirmText="Add users"
|
||||
confirmDisabled={disabled}
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
disabled={errors.some(x => x === false) ||
|
||||
userData.some(x => x.email === "" || x.email === null)}
|
||||
disabled={hasError || !userData.length}
|
||||
>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Email Address</Label>
|
||||
|
||||
<Label>Email address</Label>
|
||||
{#each userData as input, index}
|
||||
<div
|
||||
style="display: flex;
|
||||
|
@ -84,15 +93,12 @@
|
|||
inputType="email"
|
||||
bind:inputValue={input.email}
|
||||
bind:dropdownValue={input.role}
|
||||
options={Constants.BbRoles}
|
||||
error={validateInput(input.email, index)}
|
||||
options={Constants.BudibaseRoleOptions}
|
||||
error={input.error}
|
||||
on:blur={() => validateInput(input.email, index)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class:fix-height={errors.length && !errors[index]}
|
||||
class:normal-height={errors.length && !!errors[index]}
|
||||
style="width: 10% "
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
|
@ -107,11 +113,11 @@
|
|||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if hasGroupsLicense}
|
||||
{#if $auth.groupsEnabled}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="Select User Groups"
|
||||
label="User Groups"
|
||||
placeholder="No groups"
|
||||
label="Groups"
|
||||
options={$groups}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
|
@ -120,10 +126,9 @@
|
|||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.fix-height {
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
.normal-height {
|
||||
margin-bottom: 0%;
|
||||
.icon {
|
||||
width: 10%;
|
||||
align-self: flex-start;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { Icon, Body } from "@budibase/bbui"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
</script>
|
||||
|
||||
|
@ -7,17 +8,9 @@
|
|||
<div class="spacing">
|
||||
<Icon name="UserGroup" />
|
||||
</div>
|
||||
{#if value?.length === 0}
|
||||
<div class="opacity">0</div>
|
||||
{:else if value?.length === 1}
|
||||
<div class="opacity">
|
||||
<Body size="S">{value[0]?.name}</Body>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="opacity">
|
||||
{parseInt(value?.length) || 0} groups
|
||||
</div>
|
||||
{/if}
|
||||
<div class="opacity">
|
||||
{value?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { groups, auth, admin } from "stores/portal"
|
||||
import { emailValidator } from "../../../../../../helpers/validation"
|
||||
import { emailValidator } from "helpers/validation"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
const BYTES_IN_MB = 1000000
|
||||
|
@ -22,9 +22,6 @@
|
|||
let usersRole = null
|
||||
|
||||
$: invalidEmails = []
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
const validEmails = userEmails => {
|
||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||
|
@ -81,7 +78,7 @@
|
|||
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
|
||||
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">
|
||||
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||
|
@ -95,11 +92,11 @@
|
|||
options={Constants.BuilderRoleDescriptions}
|
||||
/>
|
||||
|
||||
{#if hasGroupsLicense}
|
||||
{#if $auth.groupsEnabled}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="Select User Groups"
|
||||
label="User Groups"
|
||||
placeholder="No groups"
|
||||
label="Groups"
|
||||
options={$groups}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
|
@ -122,14 +119,12 @@
|
|||
|
||||
label {
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-s);
|
||||
color: var(--ink);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
transition: all 0.2s ease 0s;
|
||||
display: inline-flex;
|
||||
text-rendering: optimizeLegibility;
|
||||
min-width: auto;
|
||||
|
@ -141,10 +136,15 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: var(--grey-2);
|
||||
font-size: var(--font-size-xs);
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
font-size: 12px;
|
||||
line-height: normal;
|
||||
border: var(--border-transparent);
|
||||
transition: background-color 130ms ease-out;
|
||||
}
|
||||
label:hover {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
|
|
@ -49,10 +49,10 @@
|
|||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body size="XS"
|
||||
>All your new users can be accessed through the autogenerated passwords.
|
||||
Make not of these passwords or download the csv</Body
|
||||
>
|
||||
<Body size="XS">
|
||||
All your new users can be accessed through the autogenerated passwords. Take
|
||||
note of these passwords or download the CSV file.
|
||||
</Body>
|
||||
|
||||
<div class="container" on:click={downloadCsvFile}>
|
||||
<div class="inner">
|
||||
|
|
|
@ -3,14 +3,20 @@
|
|||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export let row
|
||||
$: value =
|
||||
Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label ||
|
||||
"Not Available"
|
||||
|
||||
const TooltipMap = {
|
||||
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>
|
||||
|
||||
<div on:click|stopPropagation>
|
||||
<div on:click|stopPropagation title={tooltip}>
|
||||
{value}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -8,11 +8,10 @@
|
|||
Layout,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Icon,
|
||||
Search,
|
||||
notifications,
|
||||
Pagination,
|
||||
Search,
|
||||
Label,
|
||||
Divider,
|
||||
} from "@budibase/bbui"
|
||||
import AddUserModal from "./_components/AddUserModal.svelte"
|
||||
import { users, groups, auth } from "stores/portal"
|
||||
|
@ -20,69 +19,42 @@
|
|||
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
|
||||
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
|
||||
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
|
||||
import NameTableRenderer from "./_components/NameTableRenderer.svelte"
|
||||
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
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 createUserModal,
|
||||
inviteConfirmationModal,
|
||||
onboardingTypeModal,
|
||||
passwordModal,
|
||||
importUsersModal
|
||||
|
||||
let pageInfo = createPaginationStore()
|
||||
let prevEmail = undefined,
|
||||
searchEmail = undefined
|
||||
|
||||
let selectedRows = []
|
||||
let customRenderers = [
|
||||
{ column: "userGroups", component: GroupsTableRenderer },
|
||||
{ column: "apps", component: AppsTableRenderer },
|
||||
{ column: "name", component: NameTableRenderer },
|
||||
{ column: "role", component: RoleTableRenderer },
|
||||
]
|
||||
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
$: schema = {
|
||||
name: {},
|
||||
email: {},
|
||||
role: {
|
||||
sortable: false,
|
||||
},
|
||||
...(hasGroupsLicense && {
|
||||
userGroups: { sortable: false, displayName: "User groups" },
|
||||
...($auth.groupsEnabled && {
|
||||
userGroups: { sortable: false, displayName: "Groups" },
|
||||
}),
|
||||
apps: {},
|
||||
}
|
||||
|
||||
$: userData = []
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, searchEmail)
|
||||
$: {
|
||||
|
@ -105,6 +77,7 @@
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showOnboardingTypeModal = async addUsersData => {
|
||||
userData = await removingDuplicities(addUsersData)
|
||||
if (!userData?.users?.length) return
|
||||
|
@ -113,13 +86,13 @@
|
|||
}
|
||||
|
||||
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 {
|
||||
const res = await users.invite({
|
||||
emails: emails,
|
||||
builder: false,
|
||||
admin: false,
|
||||
})
|
||||
const res = await users.invite(payload)
|
||||
notifications.success(res.message)
|
||||
inviteConfirmationModal.show()
|
||||
} catch (error) {
|
||||
|
@ -232,23 +205,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading>Users</Heading>
|
||||
<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 gap="S" noPadding>
|
||||
<Divider size="S" />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
dataCy="add-user"
|
||||
|
@ -256,39 +219,47 @@
|
|||
icon="UserAdd"
|
||||
cta>Add users</Button
|
||||
>
|
||||
<Button on:click={importUsersModal.show} icon="Import" primary
|
||||
>Import users</Button
|
||||
<Button
|
||||
on:click={importUsersModal.show}
|
||||
icon="Import"
|
||||
secondary
|
||||
newStyles
|
||||
>
|
||||
|
||||
<div class="field">
|
||||
<Label size="L">Search email</Label>
|
||||
<Search bind:value={searchEmail} placeholder="" />
|
||||
</div>
|
||||
{#if selectedRows.length > 0}
|
||||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||
{/if}
|
||||
Import users
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<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 class="controls-right">
|
||||
<Search bind:value={searchEmail} placeholder="Search email" />
|
||||
{#if selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
item="user"
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
{deleteRows}
|
||||
/>
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
<Modal bind:this={createUserModal}>
|
||||
|
@ -325,28 +296,22 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.field {
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
grid-gap: var(--spacing-m);
|
||||
margin-left: auto;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field > :global(*) + :global(*) {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.access-description {
|
||||
.controls-right {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-xl);
|
||||
opacity: 0.8;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.access-text {
|
||||
margin-left: var(--spacing-m);
|
||||
.controls-right :global(.spectrum-Search) {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
import { users, groups, apps, auth } from "stores/portal"
|
||||
import AssignmentModal from "./AssignmentModal.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { roles } from "stores/backend"
|
||||
|
||||
export let app
|
||||
|
||||
let assignmentModal
|
||||
let appGroups = []
|
||||
let appUsers = []
|
||||
|
@ -28,14 +28,9 @@
|
|||
search = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let fixedAppId
|
||||
|
||||
$: page = $pageInfo.page
|
||||
|
||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
|
||||
$: appGroups = $groups.filter(x => {
|
||||
return x.apps.includes(app.appId)
|
||||
})
|
||||
|
@ -161,7 +156,7 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if hasGroupsLicense && appGroups.length}
|
||||
{#if $auth.groupsEnabled && appGroups.length}
|
||||
<List title="User Groups">
|
||||
{#each appGroups as group}
|
||||
<ListItem
|
||||
|
|
|
@ -4,22 +4,65 @@
|
|||
PickerDropdown,
|
||||
ActionButton,
|
||||
Layout,
|
||||
Icon,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
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 { createPaginationStore } from "helpers/pagination"
|
||||
|
||||
export let app
|
||||
export let addData
|
||||
export let appUsers = []
|
||||
|
||||
let prevSearch = undefined,
|
||||
search = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let appData = [{ id: "", role: "" }]
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: 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) {
|
||||
if ($pageInfo.loading) {
|
||||
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() {
|
||||
appData = [...appData, { id: "", role: "" }]
|
||||
}
|
||||
|
||||
const removeItem = index => {
|
||||
appData = appData.filter((x, idx) => idx !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -78,28 +98,61 @@
|
|||
cancelText="Cancel"
|
||||
onConfirm={() => addData(appData)}
|
||||
showCloseIcon={false}
|
||||
disabled={!valid}
|
||||
>
|
||||
<Layout noPadding gap="XS">
|
||||
{#each appData as input, index}
|
||||
<PickerDropdown
|
||||
autocomplete
|
||||
primaryOptions={optionSections}
|
||||
secondaryOptions={$roles}
|
||||
secondaryPlaceholder="Access"
|
||||
bind:primaryValue={input.id}
|
||||
bind:secondaryValue={input.role}
|
||||
bind:searchTerm={search}
|
||||
getPrimaryOptionLabel={group => 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}
|
||||
</Layout>
|
||||
{#if appData?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
{#each appData as input, index}
|
||||
<div class="item">
|
||||
<div class="picker">
|
||||
<PickerDropdown
|
||||
autocomplete
|
||||
showClearIcon={false}
|
||||
primaryOptions={optionSections}
|
||||
secondaryOptions={$roles}
|
||||
secondaryPlaceholder="Access"
|
||||
bind:primaryValue={input.id}
|
||||
bind:secondaryValue={input.role}
|
||||
bind:searchTerm={search}
|
||||
getPrimaryOptionLabel={group => 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)}
|
||||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeItem(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
<div>
|
||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store"
|
|||
import { API } from "api"
|
||||
import { admin } from "stores/portal"
|
||||
import analytics from "analytics"
|
||||
import { FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export function createAuthStore() {
|
||||
const auth = writable({
|
||||
|
@ -10,11 +12,13 @@ export function createAuthStore() {
|
|||
tenantSet: false,
|
||||
loaded: false,
|
||||
postLogout: false,
|
||||
groupsEnabled: false,
|
||||
})
|
||||
const store = derived(auth, $store => {
|
||||
let initials = null
|
||||
let isAdmin = false
|
||||
let isBuilder = false
|
||||
let groupsEnabled = false
|
||||
if ($store.user) {
|
||||
const user = $store.user
|
||||
if (user.firstName) {
|
||||
|
@ -29,6 +33,9 @@ export function createAuthStore() {
|
|||
}
|
||||
isAdmin = !!user.admin?.global
|
||||
isBuilder = !!user.builder?.global
|
||||
groupsEnabled =
|
||||
user?.license.features.includes(Constants.Features.USER_GROUPS) &&
|
||||
user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS)
|
||||
}
|
||||
return {
|
||||
user: $store.user,
|
||||
|
@ -39,6 +46,7 @@ export function createAuthStore() {
|
|||
initials,
|
||||
isAdmin,
|
||||
isBuilder,
|
||||
groupsEnabled,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -26,12 +26,8 @@ export function createUsersStore() {
|
|||
return await API.getUsers()
|
||||
}
|
||||
|
||||
async function invite({ emails, builder, admin }) {
|
||||
return API.inviteUsers({
|
||||
emails,
|
||||
builder,
|
||||
admin,
|
||||
})
|
||||
async function invite(payload) {
|
||||
return API.inviteUsers(payload)
|
||||
}
|
||||
async function acceptInvite(inviteCode, password) {
|
||||
return API.acceptInvite({
|
||||
|
|
|
@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({
|
|||
|
||||
/**
|
||||
* Invites multiple users to the current tenant.
|
||||
* @param email An array of email addresses
|
||||
* @param builder whether the user should be a global builder
|
||||
* @param admin whether the user should be a global admin
|
||||
* @param users An array of users to invite
|
||||
*/
|
||||
inviteUsers: async ({ emails, builder, admin }) => {
|
||||
inviteUsers: async users => {
|
||||
return await API.post({
|
||||
url: "/api/global/users/inviteMultiple",
|
||||
body: {
|
||||
emails,
|
||||
url: "/api/global/users/multi/invite",
|
||||
body: users.map(user => ({
|
||||
email: user.email,
|
||||
userInfo: {
|
||||
admin: admin ? { global: true } : undefined,
|
||||
builder: builder ? { global: true } : undefined,
|
||||
admin: user.admin ? { global: true } : undefined,
|
||||
builder: user.admin || user.builder ? { global: true } : undefined,
|
||||
},
|
||||
},
|
||||
})),
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -60,25 +60,31 @@ export const TableNames = {
|
|||
USERS: "ta_users",
|
||||
}
|
||||
|
||||
export const BbRoles = [
|
||||
{ label: "App User", value: "appUser" },
|
||||
{ label: "Developer", value: "developer" },
|
||||
{ label: "Admin", value: "admin" },
|
||||
export const BudibaseRoles = {
|
||||
AppUser: "appUser",
|
||||
Developer: "developer",
|
||||
Admin: "admin",
|
||||
}
|
||||
|
||||
export const BudibaseRoleOptions = [
|
||||
{ label: "App User", value: BudibaseRoles.AppUser },
|
||||
{ label: "Developer", value: BudibaseRoles.Developer },
|
||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
||||
]
|
||||
|
||||
export const BuilderRoleDescriptions = [
|
||||
{
|
||||
value: "appUser",
|
||||
value: BudibaseRoles.AppUser,
|
||||
icon: "User",
|
||||
label: "App user - Only has access to published apps",
|
||||
},
|
||||
{
|
||||
value: "developer",
|
||||
value: BudibaseRoles.Developer,
|
||||
icon: "Hammer",
|
||||
label: "Developer - Access to the app builder",
|
||||
},
|
||||
{
|
||||
value: "admin",
|
||||
value: BudibaseRoles.Admin,
|
||||
icon: "Draw",
|
||||
label: "Admin - Full access",
|
||||
},
|
||||
|
|
|
@ -214,13 +214,13 @@ export const invite = async (ctx: any) => {
|
|||
}
|
||||
|
||||
export const inviteMultiple = async (ctx: any) => {
|
||||
let { emails, userInfo } = ctx.request.body
|
||||
let users = ctx.request.body
|
||||
let existing = false
|
||||
let existingEmail
|
||||
for (let email of emails) {
|
||||
if (await usersCore.getGlobalUserByEmail(email)) {
|
||||
for (let user of users) {
|
||||
if (await usersCore.getGlobalUserByEmail(user.email)) {
|
||||
existing = true
|
||||
existingEmail = email
|
||||
existingEmail = user.email
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -228,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => {
|
|||
if (existing) {
|
||||
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++) {
|
||||
await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts)
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
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 = {
|
||||
|
|
|
@ -32,10 +32,12 @@ function buildInviteValidation() {
|
|||
|
||||
function buildInviteMultipleValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
emails: Joi.array().required(),
|
||||
userInfo: Joi.object().optional(),
|
||||
}).required())
|
||||
return joiValidator.body(Joi.array().required().items(
|
||||
Joi.object({
|
||||
email: Joi.string(),
|
||||
userInfo: Joi.object().optional(),
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
function buildInviteAcceptValidation() {
|
||||
|
@ -79,7 +81,7 @@ router
|
|||
controller.invite
|
||||
)
|
||||
.post(
|
||||
"/api/global/users/inviteMultiple",
|
||||
"/api/global/users/multi/invite",
|
||||
adminOnly,
|
||||
buildInviteMultipleValidation(),
|
||||
controller.inviteMultiple
|
||||
|
|
Loading…
Reference in New Issue