add InputPicker component and finish onboarding flow

This commit is contained in:
Peter Clement 2022-06-29 19:03:32 +01:00
parent d51efb2f70
commit 45fc55278e
23 changed files with 954 additions and 255 deletions

View File

@ -0,0 +1,215 @@
<script>
import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css"
import { fly } from "svelte/transition"
import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside"
export let inputValue
export let dropdownValue
export let id = null
export let inputType = "text"
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 options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
export let isOptionSelected = () => false
const dispatch = createEventDispatcher()
let open = false
let focus = false
$: fieldText = getFieldText(dropdownValue, options, placeholder)
const getFieldText = (dropdownValue, options, placeholder) => {
// Always use placeholder if no value
if (dropdownValue == null || dropdownValue === "") {
return placeholder || "Choose an option or type"
}
// Wait for options to load if there is a value but no options
if (!options?.length) {
return ""
}
// Render the label if the selected option is found, otherwise raw value
const selected = options.find(
option => getOptionValue(option) === dropdownValue
)
return selected ? getOptionLabel(selected) : dropdownValue
}
const updateValue = newValue => {
if (readonly) {
return
}
dispatch("change", newValue)
}
const onFocus = () => {
if (readonly) {
return
}
focus = true
}
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)
}
}
const onClick = () => {
dispatch("click")
if (readonly) {
return
}
open = true
}
const onPick = newValue => {
dispatch("pick", newValue)
open = false
}
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]
}
return value
}
</script>
<div
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
<input
{id}
on:click
on:blur
on:focus
on:input
on:keyup
on:blur={onBlur}
on:focus={onFocus}
on:input={onInput}
on:keyup={updateValueOnEnter}
value={inputValue || ""}
placeholder={placeholder || ""}
{disabled}
{readonly}
{inputType}
class="spectrum-Textfield-input spectrum-InputGroup-input"
/>
</div>
<div style="width: 30%">
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
{disabled}
class:is-open={open}
aria-haspopup="listbox"
on:mousedown={onClick}
>
<span class="spectrum-Picker-label">
{fieldText}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
{#if open}
<div
use:clickOutside={() => (open = false)}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#each options as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onPick(getOptionValue(option, idx))}
>
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
<style>
.spectrum-InputGroup {
min-width: 0;
width: 100%;
}
.spectrum-InputGroup-input {
border-right-width: 1px;
}
.spectrum-Textfield {
width: 100%;
}
.spectrum-Textfield-input {
width: 0;
}
.override-borders {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.spectrum-Popover {
max-height: 240px;
z-index: 999;
top: 100%;
}
</style>

View File

@ -13,6 +13,7 @@
export let readonly = false
export let autocomplete = false
export let sort = false
export let autoWidth = false
const dispatch = createEventDispatcher()
$: selectedLookupMap = getSelectedLookupMap(value)
@ -85,4 +86,5 @@
{getOptionValue}
onSelectOption={toggleOption}
{sort}
{autoWidth}
/>

View File

@ -0,0 +1,55 @@
<script>
import Field from "./Field.svelte"
import InputDropdown from "./Core/InputDropdown.svelte"
import { createEventDispatcher } from "svelte"
export let inputValue = null
export let dropdownValue = null
export let inputType = "text"
export let label = null
export let labelPosition = "above"
export let placeholder = null
export let disabled = false
export let readonly = false
export let error = null
export let updateOnChange = true
export let quiet = false
export let dataCy
export let autofocus
export let options = []
const dispatch = createEventDispatcher()
const onPick = e => {
dropdownValue = e.detail
dispatch("pick", e.detail)
}
const onChange = e => {
inputValue = e.detail
dispatch("change", e.detail)
}
</script>
<Field {label} {labelPosition} {error}>
<InputDropdown
{dataCy}
{updateOnChange}
{error}
{disabled}
{readonly}
{inputValue}
{dropdownValue}
{placeholder}
{inputType}
{quiet}
{autofocus}
{options}
on:change={onChange}
on:pick={onPick}
on:click
on:input
on:blur
on:focus
on:keyup
/>
</Field>

View File

@ -14,7 +14,7 @@
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let sort = false
export let autoWidth = false
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
@ -33,6 +33,7 @@
{sort}
{getOptionLabel}
{getOptionValue}
{autoWidth}
on:change={onChange}
on:click
/>

View File

@ -445,7 +445,7 @@
width: 100%;
border-radius: 0;
display: grid;
overflow: auto;
overflow: visible;
}
/* Header */
@ -513,7 +513,7 @@
z-index: 3;
}
.spectrum-Table-headCell .title {
overflow: hidden;
overflow: visible;
text-overflow: ellipsis;
}
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {

View File

@ -23,6 +23,7 @@ export { default as Icon, directions } from "./Icon/Icon.svelte"
export { default as Toggle } from "./Form/Toggle.svelte"
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
export { default as Checkbox } from "./Form/Checkbox.svelte"
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
export { default as Popover } from "./Popover/Popover.svelte"
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
@ -71,6 +72,7 @@ export { default as ListItem } from "./List/ListItem.svelte"
// Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
export { default as InternalRenderer } from "./Table/InternalRenderer.svelte"
// Typography
export { default as Body } from "./Typography/Body.svelte"

View File

@ -0,0 +1,73 @@
<script>
import { ActionButton, Icon, Search, Divider, Detail } from "@budibase/bbui"
export let searchTerm = ""
export let selected
export let filtered
export let addAll
export let select
export let title
export let key
</script>
<div style="padding: var(--spacing-m)">
<Search placeholder="Search" bind:value={searchTerm} />
<div class="header sub-header">
<div>
<Detail
>{filtered.length} {title}{filtered.length === 1 ? "" : "s"}</Detail
>
</div>
<div>
<ActionButton on:click={addAll} emphasized size="S">Add all</ActionButton>
</div>
</div>
<Divider noMargin />
<div>
{#each filtered as item}
<div
on:click={select(item._id)}
style="padding-bottom: var(--spacing-m)"
class="selection"
>
<div>
{item[key]}
</div>
{#if selected.includes(item._id)}
<div>
<Icon
color="var(--spectrum-global-color-blue-600);"
name="Checkmark"
/>
</div>
{/if}
</div>
{/each}
</div>
</div>
<style>
.header {
align-items: center;
padding: var(--spacing-m) 0 var(--spacing-m) 0;
display: flex;
justify-content: space-between;
}
.selection {
align-items: end;
display: flex;
justify-content: space-between;
cursor: pointer;
}
.selection > :first-child {
padding-top: var(--spacing-m);
}
.sub-header {
display: flex;
justify-content: space-between;
}
</style>

View File

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

View File

@ -8,14 +8,13 @@
Body,
Icon,
Popover,
Search,
Divider,
Detail,
notifications,
List,
ListItem,
StatusLight,
} from "@budibase/bbui"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
import { users, apps, groups } from "stores/portal"
import { onMount } from "svelte"
import { RoleUtils } from "@budibase/frontend-core"
@ -47,8 +46,9 @@
}
async function selectUser(id) {
let selectedUser = selectedUsers.find(user_id => user_id === id)
let selectedUser = selectedUsers.includes(id)
let enrichedUser = $users.find(user => user._id === id)
if (selectedUser) {
selectedUsers = selectedUsers.filter(id => id !== selectedUser)
let newUsers = group.users.filter(user => user._id !== id)
@ -57,6 +57,7 @@
selectedUsers = [...selectedUsers, id]
group.users.push(enrichedUser)
}
await groups.actions.save(group)
}
@ -97,55 +98,24 @@
<Button on:click={popover.show()} icon="UserAdd" cta>Add User</Button>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<div style="padding: var(--spacing-m)">
<Search placeholder="Search" bind:value={searchTerm} />
<div class="users-header header">
<div>
<Detail
>{filteredUsers.length} User{filteredUsers.length === 1
? ""
: "s"}</Detail
>
</div>
<div>
<ActionButton on:click={addAll} emphasized size="S"
>Add all</ActionButton
>
</div>
</div>
<Divider noMargin />
<div>
{#each filteredUsers as user}
<div
on:click={selectUser(user._id)}
style="padding-bottom: var(--spacing-m)"
class="user-selection"
>
<div>
{user.email}
</div>
{#if selectedUsers.includes(user._id)}
<div>
<Icon
color="var(--spectrum-global-color-blue-600);"
name="Checkmark"
/>
</div>
{/if}
</div>
{/each}
</div>
</div>
<UserGroupPicker
key={"email"}
title={"User"}
bind:searchTerm
bind:selected={selectedUsers}
bind:filtered={filteredUsers}
{addAll}
select={selectUser}
/>
</Popover>
</div>
<List>
{#if group?.users.length}
{#each group.users as user}
<ListItem subtitle={user.access} title={user.email} avatar
<ListItem subtitle={user?.access} title={user?.email} avatar
><Icon
on:click={() => removeUser(user._id)}
on:click={() => removeUser(user?._id)}
hoverable
size="L"
name="Close"
@ -180,7 +150,7 @@
</ListItem>
{/each}
{:else}
<ListItem icon="UserGroup" title="You have no users in this team" />
<ListItem icon="UserGroup" title="No apps" />
{/if}
</List>
</Layout>
@ -190,24 +160,6 @@
margin-left: var(--spacing-l);
}
.users-header {
align-items: center;
padding: var(--spacing-m) 0 var(--spacing-m) 0;
display: flex;
justify-content: space-between;
}
.user-selection {
align-items: end;
display: flex;
justify-content: space-between;
cursor: pointer;
}
.user-selection > :first-child {
padding-top: var(--spacing-m);
}
.header {
display: flex;
justify-content: space-between;

View File

@ -2,43 +2,44 @@
import { goto } from "@roxi/routify"
import {
ActionButton,
ActionMenu,
Avatar,
Button,
Layout,
Heading,
Body,
Divider,
Label,
List,
ListItem,
Icon,
Input,
MenuItem,
Popover,
Select,
Toggle,
Modal,
Table,
ModalContent,
notifications,
StatusLight,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { fetchData } from "helpers"
import { users, auth } from "stores/portal"
import { users, auth, groups } from "stores/portal"
import { Constants } from "@budibase/frontend-core"
import TagsRenderer from "./_components/RolesTagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
import { RoleUtils } from "@budibase/frontend-core"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
export let userId
let deleteUserModal
let editRolesModal
let resetPasswordModal
const roleSchema = {
name: { displayName: "App" },
role: {},
}
const noRoleSchema = {
name: { displayName: "App" },
}
let popoverAnchor
let searchTerm = ""
let popover
let selectedGroups = []
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
// Merge the Apps list and the roles response to get something that makes sense for the table
$: allAppList = Object.keys($apps?.data).map(id => {
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
@ -50,19 +51,23 @@
}
})
$: appList = allAppList.filter(app => !!app.role[0])
$: noRoleAppList = allAppList
.filter(app => !app.role[0])
.map(app => {
delete app.role
return app
})
// Used for searching through groups in the add group popover
$: filteredGroups = $groups.filter(
group =>
selectedGroups &&
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
)
let selectedApp
$: appList = allAppList.filter(app => !!app.role[0])
$: userGroups = $groups.filter(x => {
return x.users?.some(y => {
return y._id === userId
})
})
const userFetch = fetchData(`/api/global/users/${userId}`)
const apps = fetchData(`/api/global/roles`)
async function deleteUser() {
try {
await users.delete(userId)
@ -73,8 +78,17 @@
}
}
let toggleDisabled = false
function getHighestRole(roles) {
let highestRole
let highestRoleNumber = 0
roles.forEach(role => {
let roleNumber = RoleUtils.getRolePriority(role._id)
if (roleNumber > highestRoleNumber) {
highestRole = role
}
})
return highestRole
}
async function updateUserFirstName(evt) {
try {
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
@ -84,6 +98,13 @@
}
}
async function removeGroup(id) {
let updatedGroup = $groups.find(x => x._id === id)
let newUsers = updatedGroup.users.filter(user => user._id !== userId)
updatedGroup.users = newUsers
groups.actions.save(updatedGroup)
}
async function updateUserLastName(evt) {
try {
await users.save({ ...$userFetch?.data, lastName: evt.target.value })
@ -93,6 +114,30 @@
}
}
async function updateUserRole() {
return
}
async function addGroup(groupId) {
let selectedGroup = selectedGroups.includes(groupId)
let newUser = $users.find(user => user._id === userId)
let group = $groups.find(group => group._id === groupId)
if (selectedGroup) {
selectedGroups = selectedGroups.filter(id => id === selectedGroup)
let newUsers = group.users.filter(user => user._id !== newUser._id)
group.users = newUsers
} else {
selectedGroups = [...selectedGroups, groupId]
group.users.push(newUser)
}
await groups.actions.save(group)
}
function addAll() {}
/*
async function toggleFlag(flagName, detail) {
toggleDisabled = true
try {
@ -104,6 +149,7 @@
toggleDisabled = false
}
async function toggleBuilderAccess({ detail }) {
return toggleFlag("builder", detail)
}
@ -116,38 +162,56 @@
selectedApp = detail
editRolesModal.show()
}
*/
onMount(async () => {
try {
await groups.actions.init()
} catch (error) {
notifications.error("Error getting User groups")
}
})
</script>
<Layout noPadding>
<Layout gap="L" noPadding>
<Layout gap="XS" noPadding>
<div>
<ActionButton
on:click={() => $goto("./")}
quiet
size="S"
icon="BackAndroid"
>
Back to users
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft">
Back
</ActionButton>
</div>
<Heading>User: {$userFetch?.data?.email}</Heading>
<Body>
Change user settings and update their app roles. Also contains the ability
to delete the user as well as force reset their password.
</Body>
</Layout>
<Divider size="S" />
<Layout gap="XS" noPadding>
<div class="title">
<div>
<div style="display: flex;">
<Avatar size="XXL" initials="PC" />
<div class="subtitle">
<Heading size="S"
>{$userFetch?.data?.firstName +
" " +
$userFetch?.data?.lastName}</Heading
>
<Body size="XS">{$userFetch?.data?.email}</Body>
</div>
</div>
</div>
<div>
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem on:click={resetPasswordModal.show} icon="Refresh"
>Force Password Reset</MenuItem
>
<MenuItem on:click={deleteUserModal.show} icon="Delete"
>Delete</MenuItem
>
</ActionMenu>
</div>
</div>
</Layout>
<Layout gap="S" noPadding>
<Heading size="S">General</Heading>
<div class="fields">
<div class="field">
<Label size="L">Email</Label>
<Input disabled thin value={$userFetch?.data?.email} />
</div>
<div class="field">
<Label size="L">Group(s)</Label>
<Select disabled options={["All users"]} value="All users" />
</div>
<div class="field">
<Label size="L">First name</Label>
<Input
@ -167,71 +231,91 @@
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field">
<Label size="L">Development access</Label>
<Toggle
text=""
value={$userFetch?.data?.builder?.global}
on:change={toggleBuilderAccess}
disabled={toggleDisabled}
/>
</div>
<div class="field">
<Label size="L">Administration access</Label>
<Toggle
text=""
value={$userFetch?.data?.admin?.global}
on:change={toggleAdminAccess}
disabled={toggleDisabled}
/>
<Label size="L">Role</Label>
<Select options={Constants.BbRoles} on:blur={updateUserRole} />
</div>
{/if}
</div>
<div class="regenerate">
<ActionButton
size="S"
icon="Refresh"
quiet
on:click={resetPasswordModal.show}>Force password reset</ActionButton
>
</div>
</Layout>
<Divider size="S" />
<Layout gap="S" noPadding>
<Heading size="S">Configure roles</Heading>
<Body>Specify a role to grant access to an app.</Body>
<Table
on:click={openUpdateRolesModal}
schema={roleSchema}
data={appList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "role", component: TagsRenderer }]}
/>
</Layout>
<Layout gap="S" noPadding>
<Heading size="XS">No Access</Heading>
<Body
>Apps do not appear in the users portal. Public pages may still be viewed
if visited directly.</Body
>
<Table
on:click={openUpdateRolesModal}
schema={noRoleSchema}
data={noRoleAppList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
</Layout>
<Divider size="S" />
<!-- User groups -->
<Layout gap="XS" noPadding>
<Heading size="S">Delete user</Heading>
<Body>Deleting a user completely removes them from your account.</Body>
<div class="tableTitle">
<div>
<Heading size="XS">User groups</Heading>
<Body size="S"
>Manage apps that this User group has been assigned to</Body
>
</div>
<div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserGroup" cta
>Add User Group</Button
>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
key={"name"}
title={"Group"}
bind:searchTerm
bind:selected={selectedGroups}
bind:filtered={filteredGroups}
{addAll}
select={addGroup}
/>
</Popover>
</div>
<List>
{#if userGroups.length}
{#each userGroups as group}
<ListItem
title={group.name}
icon={group.icon}
iconBackground={group.color}
><Icon
on:click={removeGroup(group._id)}
hoverable
size="L"
name="Close"
/></ListItem
>
{/each}
{:else}
<ListItem icon="UserGroup" title="No groups" />
{/if}
</List>
</Layout>
<!-- 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 group has been assigned to</Body
>
</div>
</div>
<List>
{#if appList.length}
{#each appList as app}
<ListItem title={app.name} icon="Apps">
<div class="title ">
<StatusLight
color={RoleUtils.getRoleColour(getHighestRole(app.role)._id)}
/>
<div style="margin-left: var(--spacing-s);">
<Body size="XS">{getHighestRole(app.role).name}</Body>
</div>
</div>
</ListItem>
{/each}
{:else}
<ListItem icon="Apps" title="No apps" />
{/if}
</List>
</Layout>
<div class="delete-button">
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
</div>
</Layout>
<Modal bind:this={deleteUserModal}>
@ -248,13 +332,6 @@
</Body>
</ModalContent>
</Modal>
<Modal bind:this={editRolesModal}>
<UpdateRolesModal
app={selectedApp}
user={$userFetch.data}
on:update={userFetch.refresh}
/>
</Modal>
<Modal bind:this={resetPasswordModal}>
<ForceResetPasswordModal
user={$userFetch.data}
@ -272,9 +349,26 @@
grid-template-columns: 32% 1fr;
align-items: center;
}
.regenerate {
position: absolute;
top: 0;
right: 0;
.title {
display: flex;
align-items: center;
justify-content: space-between;
}
.tableTitle {
display: flex;
justify-content: space-between;
margin-bottom: var(--spacing-m);
}
.subtitle {
padding: 0 0 0 var(--spacing-m);
display: inline-block;
}
.appsTitle {
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,82 +1,78 @@
<script>
import {
Body,
Input,
Select,
ModalContent,
notifications,
Toggle,
ActionButton,
Layout,
Label,
ModalContent,
Multiselect,
notifications,
InputDropdown,
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
import { users, groups } from "stores/portal"
import analytics, { Events } from "analytics"
import { Constants } from "@budibase/frontend-core"
export let disabled
export let showOnboardingTypeModal
const options = ["Email onboarding", "Basic onboarding"]
let selected = options[0]
let builder, admin
const [email, error, touched] = createValidationStore("", emailValidator)
$: userData = [{ email: "", role: "", error: null }]
/*
async function createUserFlow() {
try {
const res = await users.invite({ email: $email, builder, admin })
const res = await users.invite({ email: "", builder, admin })
notifications.success(res.message)
analytics.captureEvent(Events.USER.INVITE, { type: selected })
} catch (error) {
notifications.error("Error inviting user")
}
}
*/
function addNewInput() {
userData = [...userData, { email: "", role: "" }]
}
</script>
<ModalContent
onConfirm={createUserFlow}
onConfirm={showOnboardingTypeModal}
size="M"
title="Add new user"
confirmText="Add user"
confirmDisabled={disabled}
cancelText="Cancel"
disabled={$error}
showCloseIcon={false}
>
<Body size="S">
If you have SMTP configured and an email for the new user, you can use the
automated email onboarding flow. Otherwise, use our basic onboarding process
with autogenerated passwords.
</Body>
<Select
placeholder={null}
bind:value={selected}
on:change
{options}
label="Add new user via:"
/>
<Input
type="email"
bind:value={$email}
error={$touched && $error}
placeholder="john@doe.com"
label="Email"
/>
<div>
<div class="toggle">
<Label size="L">Development access</Label>
<Toggle text="" bind:value={builder} />
<Layout noPadding gap="XS">
<Label>Email Address</Label>
{#each userData as input, index}
<InputDropdown
inputType="email"
bind:inputValue={input.email}
bind:dropdownValue={input.role}
options={Constants.BbRoles}
error={input.error}
/>
{/each}
<div>
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
</div>
<div class="toggle">
<Label size="L">Administration access</Label>
<Toggle text="" bind:value={admin} />
</div>
</div>
</Layout>
<Multiselect
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option.name}
/>
</ModalContent>
<style>
.toggle {
display: grid;
grid-template-columns: 78% 1fr;
align-items: center;
width: 50%;
:global(.spectrum-Picker) {
border-top-left-radius: 0px;
}
</style>

View File

@ -21,6 +21,7 @@
<style>
.align {
display: flex;
overflow: hidden;
}
.spacing {

View File

@ -21,6 +21,7 @@
<style>
.align {
display: flex;
overflow: hidden;
}
.opacity {

View File

@ -0,0 +1,52 @@
<script>
import { Body, ModalContent, RadioGroup, Multiselect } from "@budibase/bbui"
import { groups } from "stores/portal"
import { Constants } from "@budibase/frontend-core"
</script>
<ModalContent
size="M"
title="Import users"
confirmText="Done"
showCancelButton={false}
cancelText="Cancel"
showCloseIcon={false}
>
<Body size="S">Import your users email addrresses from a CSV</Body>
<div class="container">
<div class="inner">
<Body size="S">Upload</Body>
</div>
</div>
<RadioGroup options={Constants.BuilderRoleDescriptions} />
<Multiselect
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option.name}
/>
</ModalContent>
<style>
.inner {
display: flex;
}
:global(.spectrum-Picker) {
border-top-left-radius: 0px;
}
.container {
width: 100%;
height: var(--spectrum-alias-item-height-l);
background: var(--spectrum-global-color-gray-200);
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -25,6 +25,7 @@
.align {
display: flex;
align-items: center;
overflow: hidden;
}
.spacing {

View File

@ -0,0 +1,107 @@
<script>
import { ModalContent, Body, Layout, Icon } from "@budibase/bbui"
export let showConfirmationModal
let emailOnboardingKey = "emailOnboarding"
let basicOnboaridngKey = "basicOnboarding"
let selectedOnboardingType
</script>
<ModalContent
size="M"
title="Choose your onboarding"
confirmText="Done"
cancelText="Cancel"
showCloseIcon={false}
onConfirm={() => showConfirmationModal(selectedOnboardingType)}
disabled={!selectedOnboardingType}
>
<Layout noPadding gap="S">
<div
class="onboarding-type item"
class:selected={selectedOnboardingType == emailOnboardingKey}
on:click={() => {
selectedOnboardingType = emailOnboardingKey
}}
>
<div class="content onboarding-type-wrap">
<Icon name="WebPage" />
<div class="onboarding-type-text">
<Body size="S">Send email invites</Body>
</div>
</div>
<div style="color: var(--spectrum-global-color-green-600); float: right">
{#if selectedOnboardingType == emailOnboardingKey}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircle" />
</div>
{/if}
</div>
</div>
<div
class="onboarding-type item"
class:selected={selectedOnboardingType == basicOnboaridngKey}
on:click={() => {
selectedOnboardingType = basicOnboaridngKey
}}
>
<div class="content onboarding-type-wrap">
<Icon name="Key" />
<div class="onboarding-type-text">
<Body size="S">Generate passwords for each user</Body>
</div>
</div>
<div style="color: var(--spectrum-global-color-green-600); float: right">
{#if selectedOnboardingType == basicOnboaridngKey}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircle" />
</div>
{/if}
</div>
</div>
</Layout>
</ModalContent>
<style>
.onboarding-type.item {
padding: var(--spectrum-alias-item-padding-xl);
}
.onboarding-type-wrap {
display: flex;
flex-direction: row;
align-items: center;
}
.checkmark-spacing {
margin-right: var(--spacing-m);
}
.content {
letter-spacing: 0px;
}
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-primary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
border-width: 1px;
display: flex;
justify-content: space-between;
align-items: center;
}
.item:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.onboarding-type-wrap .onboarding-type-text {
padding-left: var(--spectrum-alias-item-padding-xl);
}
.onboarding-type-wrap :global(.spectrum-Icon) {
min-width: var(--spectrum-icon-size-m);
}
.onboarding-type-wrap :global(.spectrum-Heading) {
padding-bottom: var(--spectrum-alias-item-padding-s);
}
</style>

View File

@ -0,0 +1,15 @@
<script>
import { InternalRenderer } from "@budibase/bbui"
export let value
</script>
<div style="display: flex; ">
{value}
<div style="margin-left: 1.5rem;">
<InternalRenderer {value} />
</div>
</div>
<style>
</style>

View File

@ -0,0 +1,61 @@
<script>
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
const schema = {
email: {},
password: {},
}
</script>
<ModalContent
size="S"
title="Accounts created!"
confirmText="Done"
showCancelButton={false}
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
>
<div class="container">
<div class="inner">
<Icon name="Download" />
<div style="margin-left: var(--spacing-m)">
<Body size="XS">Passwords CSV</Body>
</div>
</div>
</div>
<Table
{schema}
data={[{ email: "test", password: "§xz§§zvzxvxzv" }]}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]}
/>
</ModalContent>
<style>
.inner {
display: flex;
}
:global(.spectrum-Picker) {
border-top-left-radius: 0px;
}
.container {
width: 100%;
height: var(--spectrum-alias-item-height-l);
background: #009562;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -8,19 +8,16 @@
]
</script>
<div class="align">
<Select
on:click={e => e.stopPropagation()}
value={"Admin"}
quiet
{options}
placeholder="Admin"
/>
<div>
<Select value={"appUser"} {options} placeholder="Admin" autoWidth quiet />
</div>
<style>
.align {
display: flex;
div {
overflow: visible;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 1000;
position: relative;
}
</style>

View File

@ -2,7 +2,10 @@
import { Icon, ActionMenu, MenuItem } from "@budibase/bbui"
</script>
<div>
<div
style=" overflow: hidden;
"
>
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />

View File

@ -7,6 +7,7 @@
Table,
Layout,
Modal,
ModalContent,
Icon,
notifications,
} from "@budibase/bbui"
@ -20,11 +21,17 @@
import SettingsTableRenderer from "./_components/SettingsTableRenderer.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"
const schema = {
name: {},
email: {},
role: { noPropagation: true, sortable: false },
role: {
noPropagation: true,
sortable: false,
},
userGroups: { sortable: false, displayName: "User groups" },
apps: {},
settings: {
@ -52,6 +59,12 @@
let search
let email
let createUserModal,
basicOnboardingModal,
inviteConfirmationModal,
onboardingTypeModal,
passwordModal,
importUsersModal
$: filteredUsers = $users
.filter(user => user.email.includes(search || ""))
@ -80,10 +93,16 @@
}
})
let createUserModal
let basicOnboardingModal = function openBasicOnboardingModal() {
createUserModal.hide()
basicOnboardingModal.show()
function showOnboardingTypeModal() {
onboardingTypeModal.show()
}
function showConfirmationModal(onboardingType) {
if (onboardingType === "emailOnboarding") {
inviteConfirmationModal.show()
} else {
passwordModal.show()
}
}
onMount(async () => {
@ -120,7 +139,9 @@
icon="UserAdd"
cta>Add Users</Button
>
<Button icon="Import" primary>Import Users</Button>
<Button on:click={importUsersModal.show} icon="Import" primary
>Import Users</Button
>
</ButtonGroup>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
@ -142,8 +163,34 @@
</Layout>
<Modal bind:this={createUserModal}>
<AddUserModal />
<AddUserModal {showOnboardingTypeModal} />
</Modal>
<Modal bind:this={inviteConfirmationModal}>
<ModalContent
showCancelButton={false}
title="Invites sent!"
confirmText="Done"
>
<Body size="S"
>Your users should now recieve an email invite to get access to their
Budibase account</Body
></ModalContent
>
</Modal>
<Modal bind:this={onboardingTypeModal}>
<OnboardingTypeModal {showConfirmationModal} />
</Modal>
<Modal bind:this={passwordModal}>
<PasswordModal />
</Modal>
<Modal bind:this={importUsersModal}>
<ImportUsersModal />
</Modal>
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
<style>

View File

@ -56,6 +56,30 @@ export const TableNames = {
USERS: "ta_users",
}
export const BbRoles = [
{ label: "App User", value: "appUser" },
{ label: "Developer", value: "developer" },
{ label: "Admin", value: "admin" },
]
export const BuilderRoleDescriptions = [
{
value: "appUser",
icon: "User",
label: "App user - Only has access to published apps",
},
{
value: "developer",
icon: "Hammer",
label: "Developer - Access to the app builder",
},
{
value: "admin",
icon: "Draw",
label: "Admin - Full access",
},
]
/**
* API version header attached to all requests.
* Version changelog: