add InputPicker component and finish onboarding flow
This commit is contained in:
parent
82ebc9526f
commit
4c4a6ccb14
|
@ -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>
|
|
@ -13,6 +13,7 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let autoWidth = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
$: selectedLookupMap = getSelectedLookupMap(value)
|
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||||
|
@ -85,4 +86,5 @@
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
onSelectOption={toggleOption}
|
onSelectOption={toggleOption}
|
||||||
{sort}
|
{sort}
|
||||||
|
{autoWidth}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
|
@ -14,7 +14,7 @@
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let autoWidth = false
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
{sort}
|
{sort}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
|
{autoWidth}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -445,7 +445,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
|
@ -513,7 +513,7 @@
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell .title {
|
.spectrum-Table-headCell .title {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
||||||
|
|
|
@ -23,6 +23,7 @@ export { default as Icon, directions } from "./Icon/Icon.svelte"
|
||||||
export { default as Toggle } from "./Form/Toggle.svelte"
|
export { default as Toggle } from "./Form/Toggle.svelte"
|
||||||
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.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 DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
|
@ -71,6 +72,7 @@ export { default as ListItem } from "./List/ListItem.svelte"
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
|
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
|
||||||
|
export { default as InternalRenderer } from "./Table/InternalRenderer.svelte"
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
export { default as Body } from "./Typography/Body.svelte"
|
export { default as Body } from "./Typography/Body.svelte"
|
||||||
|
|
|
@ -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>
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
$: wide =
|
$: wide =
|
||||||
$page.path.includes("email/:template") ||
|
$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"))
|
($page.path.includes("groups") && !$page.path.includes(":groupId"))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,13 @@
|
||||||
Body,
|
Body,
|
||||||
Icon,
|
Icon,
|
||||||
Popover,
|
Popover,
|
||||||
Search,
|
|
||||||
Divider,
|
|
||||||
Detail,
|
|
||||||
notifications,
|
notifications,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
|
|
||||||
import { users, apps, groups } from "stores/portal"
|
import { users, apps, groups } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
@ -47,8 +46,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectUser(id) {
|
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)
|
let enrichedUser = $users.find(user => user._id === id)
|
||||||
|
|
||||||
if (selectedUser) {
|
if (selectedUser) {
|
||||||
selectedUsers = selectedUsers.filter(id => id !== selectedUser)
|
selectedUsers = selectedUsers.filter(id => id !== selectedUser)
|
||||||
let newUsers = group.users.filter(user => user._id !== id)
|
let newUsers = group.users.filter(user => user._id !== id)
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
selectedUsers = [...selectedUsers, id]
|
selectedUsers = [...selectedUsers, id]
|
||||||
group.users.push(enrichedUser)
|
group.users.push(enrichedUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
await groups.actions.save(group)
|
await groups.actions.save(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,55 +98,24 @@
|
||||||
<Button on:click={popover.show()} icon="UserAdd" cta>Add User</Button>
|
<Button on:click={popover.show()} icon="UserAdd" cta>Add User</Button>
|
||||||
</div>
|
</div>
|
||||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||||
<div style="padding: var(--spacing-m)">
|
<UserGroupPicker
|
||||||
<Search placeholder="Search" bind:value={searchTerm} />
|
key={"email"}
|
||||||
<div class="users-header header">
|
title={"User"}
|
||||||
<div>
|
bind:searchTerm
|
||||||
<Detail
|
bind:selected={selectedUsers}
|
||||||
>{filteredUsers.length} User{filteredUsers.length === 1
|
bind:filtered={filteredUsers}
|
||||||
? ""
|
{addAll}
|
||||||
: "s"}</Detail
|
select={selectUser}
|
||||||
>
|
/>
|
||||||
</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>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{#if group?.users.length}
|
{#if group?.users.length}
|
||||||
{#each group.users as user}
|
{#each group.users as user}
|
||||||
<ListItem subtitle={user.access} title={user.email} avatar
|
<ListItem subtitle={user?.access} title={user?.email} avatar
|
||||||
><Icon
|
><Icon
|
||||||
on:click={() => removeUser(user._id)}
|
on:click={() => removeUser(user?._id)}
|
||||||
hoverable
|
hoverable
|
||||||
size="L"
|
size="L"
|
||||||
name="Close"
|
name="Close"
|
||||||
|
@ -180,7 +150,7 @@
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<ListItem icon="UserGroup" title="You have no users in this team" />
|
<ListItem icon="UserGroup" title="No apps" />
|
||||||
{/if}
|
{/if}
|
||||||
</List>
|
</List>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -190,24 +160,6 @@
|
||||||
margin-left: var(--spacing-l);
|
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 {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
@ -2,43 +2,44 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
ActionMenu,
|
||||||
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
Heading,
|
Heading,
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
|
||||||
Label,
|
Label,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
|
||||||
ModalContent,
|
ModalContent,
|
||||||
notifications,
|
notifications,
|
||||||
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
import { fetchData } from "helpers"
|
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 ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||||
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
|
|
||||||
export let userId
|
export let userId
|
||||||
let deleteUserModal
|
let deleteUserModal
|
||||||
let editRolesModal
|
|
||||||
let resetPasswordModal
|
let resetPasswordModal
|
||||||
|
let popoverAnchor
|
||||||
const roleSchema = {
|
let searchTerm = ""
|
||||||
name: { displayName: "App" },
|
let popover
|
||||||
role: {},
|
let selectedGroups = []
|
||||||
}
|
|
||||||
|
|
||||||
const noRoleSchema = {
|
|
||||||
name: { displayName: "App" },
|
|
||||||
}
|
|
||||||
|
|
||||||
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
|
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
|
||||||
|
|
||||||
// Merge the Apps list and the roles response to get something that makes sense for the table
|
// Merge the Apps list and the roles response to get something that makes sense for the table
|
||||||
$: allAppList = Object.keys($apps?.data).map(id => {
|
$: allAppList = Object.keys($apps?.data).map(id => {
|
||||||
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
|
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
|
||||||
|
@ -50,19 +51,23 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: appList = allAppList.filter(app => !!app.role[0])
|
// Used for searching through groups in the add group popover
|
||||||
$: noRoleAppList = allAppList
|
$: filteredGroups = $groups.filter(
|
||||||
.filter(app => !app.role[0])
|
group =>
|
||||||
.map(app => {
|
selectedGroups &&
|
||||||
delete app.role
|
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
return app
|
)
|
||||||
})
|
|
||||||
|
|
||||||
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 userFetch = fetchData(`/api/global/users/${userId}`)
|
||||||
const apps = fetchData(`/api/global/roles`)
|
const apps = fetchData(`/api/global/roles`)
|
||||||
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
try {
|
try {
|
||||||
await users.delete(userId)
|
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) {
|
async function updateUserFirstName(evt) {
|
||||||
try {
|
try {
|
||||||
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
|
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) {
|
async function updateUserLastName(evt) {
|
||||||
try {
|
try {
|
||||||
await users.save({ ...$userFetch?.data, lastName: evt.target.value })
|
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) {
|
async function toggleFlag(flagName, detail) {
|
||||||
toggleDisabled = true
|
toggleDisabled = true
|
||||||
try {
|
try {
|
||||||
|
@ -104,6 +149,7 @@
|
||||||
toggleDisabled = false
|
toggleDisabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function toggleBuilderAccess({ detail }) {
|
async function toggleBuilderAccess({ detail }) {
|
||||||
return toggleFlag("builder", detail)
|
return toggleFlag("builder", detail)
|
||||||
}
|
}
|
||||||
|
@ -116,38 +162,56 @@
|
||||||
selectedApp = detail
|
selectedApp = detail
|
||||||
editRolesModal.show()
|
editRolesModal.show()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await groups.actions.init()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting User groups")
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout gap="L" noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<div>
|
<div>
|
||||||
<ActionButton
|
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft">
|
||||||
on:click={() => $goto("./")}
|
Back
|
||||||
quiet
|
|
||||||
size="S"
|
|
||||||
icon="BackAndroid"
|
|
||||||
>
|
|
||||||
Back to users
|
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</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>
|
</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>
|
<Layout gap="S" noPadding>
|
||||||
<Heading size="S">General</Heading>
|
|
||||||
<div class="fields">
|
<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">
|
<div class="field">
|
||||||
<Label size="L">First name</Label>
|
<Label size="L">First name</Label>
|
||||||
<Input
|
<Input
|
||||||
|
@ -167,71 +231,91 @@
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
{#if userId !== $auth.user._id}
|
{#if userId !== $auth.user._id}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Development access</Label>
|
<Label size="L">Role</Label>
|
||||||
<Toggle
|
<Select options={Constants.BbRoles} on:blur={updateUserRole} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="regenerate">
|
|
||||||
<ActionButton
|
|
||||||
size="S"
|
|
||||||
icon="Refresh"
|
|
||||||
quiet
|
|
||||||
on:click={resetPasswordModal.show}>Force password reset</ActionButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider size="S" />
|
|
||||||
<Layout gap="S" noPadding>
|
<!-- User groups -->
|
||||||
<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" />
|
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Delete user</Heading>
|
<div class="tableTitle">
|
||||||
<Body>Deleting a user completely removes them from your account.</Body>
|
<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>
|
</Layout>
|
||||||
<div class="delete-button">
|
|
||||||
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={deleteUserModal}>
|
<Modal bind:this={deleteUserModal}>
|
||||||
|
@ -248,13 +332,6 @@
|
||||||
</Body>
|
</Body>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={editRolesModal}>
|
|
||||||
<UpdateRolesModal
|
|
||||||
app={selectedApp}
|
|
||||||
user={$userFetch.data}
|
|
||||||
on:update={userFetch.refresh}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<Modal bind:this={resetPasswordModal}>
|
<Modal bind:this={resetPasswordModal}>
|
||||||
<ForceResetPasswordModal
|
<ForceResetPasswordModal
|
||||||
user={$userFetch.data}
|
user={$userFetch.data}
|
||||||
|
@ -272,9 +349,26 @@
|
||||||
grid-template-columns: 32% 1fr;
|
grid-template-columns: 32% 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.regenerate {
|
|
||||||
position: absolute;
|
.title {
|
||||||
top: 0;
|
display: flex;
|
||||||
right: 0;
|
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>
|
</style>
|
||||||
|
|
|
@ -1,82 +1,78 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Body,
|
ActionButton,
|
||||||
Input,
|
Layout,
|
||||||
Select,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
|
||||||
Toggle,
|
|
||||||
Label,
|
Label,
|
||||||
|
ModalContent,
|
||||||
|
Multiselect,
|
||||||
|
notifications,
|
||||||
|
InputDropdown,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createValidationStore, emailValidator } from "helpers/validation"
|
import { users, groups } from "stores/portal"
|
||||||
import { users } from "stores/portal"
|
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let disabled
|
export let disabled
|
||||||
|
export let showOnboardingTypeModal
|
||||||
const options = ["Email onboarding", "Basic onboarding"]
|
const options = ["Email onboarding", "Basic onboarding"]
|
||||||
let selected = options[0]
|
let selected = options[0]
|
||||||
let builder, admin
|
let builder, admin
|
||||||
|
|
||||||
const [email, error, touched] = createValidationStore("", emailValidator)
|
$: userData = [{ email: "", role: "", error: null }]
|
||||||
|
|
||||||
|
/*
|
||||||
async function createUserFlow() {
|
async function createUserFlow() {
|
||||||
try {
|
try {
|
||||||
const res = await users.invite({ email: $email, builder, admin })
|
const res = await users.invite({ email: "", builder, admin })
|
||||||
notifications.success(res.message)
|
notifications.success(res.message)
|
||||||
analytics.captureEvent(Events.USER.INVITE, { type: selected })
|
analytics.captureEvent(Events.USER.INVITE, { type: selected })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error inviting user")
|
notifications.error("Error inviting user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
function addNewInput() {
|
||||||
|
userData = [...userData, { email: "", role: "" }]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={createUserFlow}
|
onConfirm={showOnboardingTypeModal}
|
||||||
size="M"
|
size="M"
|
||||||
title="Add new user"
|
title="Add new user"
|
||||||
confirmText="Add user"
|
confirmText="Add user"
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
disabled={$error}
|
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Layout noPadding gap="XS">
|
||||||
If you have SMTP configured and an email for the new user, you can use the
|
<Label>Email Address</Label>
|
||||||
automated email onboarding flow. Otherwise, use our basic onboarding process
|
|
||||||
with autogenerated passwords.
|
{#each userData as input, index}
|
||||||
</Body>
|
<InputDropdown
|
||||||
<Select
|
inputType="email"
|
||||||
placeholder={null}
|
bind:inputValue={input.email}
|
||||||
bind:value={selected}
|
bind:dropdownValue={input.role}
|
||||||
on:change
|
options={Constants.BbRoles}
|
||||||
{options}
|
error={input.error}
|
||||||
label="Add new user via:"
|
/>
|
||||||
/>
|
{/each}
|
||||||
<Input
|
<div>
|
||||||
type="email"
|
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||||
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} />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle">
|
</Layout>
|
||||||
<Label size="L">Administration access</Label>
|
|
||||||
<Toggle text="" bind:value={admin} />
|
<Multiselect
|
||||||
</div>
|
placeholder="Select User Groups"
|
||||||
</div>
|
label="User Groups"
|
||||||
|
options={$groups}
|
||||||
|
getOptionLabel={option => option.name}
|
||||||
|
getOptionValue={option => option.name}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toggle {
|
:global(.spectrum-Picker) {
|
||||||
display: grid;
|
border-top-left-radius: 0px;
|
||||||
grid-template-columns: 78% 1fr;
|
|
||||||
align-items: center;
|
|
||||||
width: 50%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
<style>
|
<style>
|
||||||
.align {
|
.align {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacing {
|
.spacing {
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
<style>
|
<style>
|
||||||
.align {
|
.align {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opacity {
|
.opacity {
|
||||||
|
|
|
@ -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>
|
|
@ -25,6 +25,7 @@
|
||||||
.align {
|
.align {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacing {
|
.spacing {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -8,19 +8,16 @@
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="align">
|
<div>
|
||||||
<Select
|
<Select value={"appUser"} {options} placeholder="Admin" autoWidth quiet />
|
||||||
on:click={e => e.stopPropagation()}
|
|
||||||
value={"Admin"}
|
|
||||||
quiet
|
|
||||||
{options}
|
|
||||||
placeholder="Admin"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.align {
|
div {
|
||||||
display: flex;
|
overflow: visible;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
import { Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
import { Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div
|
||||||
|
style=" overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<span slot="control">
|
<span slot="control">
|
||||||
<Icon hoverable name="More" />
|
<Icon hoverable name="More" />
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
Table,
|
Table,
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Icon,
|
Icon,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
@ -20,11 +21,17 @@
|
||||||
import SettingsTableRenderer from "./_components/SettingsTableRenderer.svelte"
|
import SettingsTableRenderer from "./_components/SettingsTableRenderer.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 PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
|
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
name: {},
|
name: {},
|
||||||
email: {},
|
email: {},
|
||||||
role: { noPropagation: true, sortable: false },
|
role: {
|
||||||
|
noPropagation: true,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
userGroups: { sortable: false, displayName: "User groups" },
|
userGroups: { sortable: false, displayName: "User groups" },
|
||||||
apps: {},
|
apps: {},
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -52,6 +59,12 @@
|
||||||
|
|
||||||
let search
|
let search
|
||||||
let email
|
let email
|
||||||
|
let createUserModal,
|
||||||
|
basicOnboardingModal,
|
||||||
|
inviteConfirmationModal,
|
||||||
|
onboardingTypeModal,
|
||||||
|
passwordModal,
|
||||||
|
importUsersModal
|
||||||
|
|
||||||
$: filteredUsers = $users
|
$: filteredUsers = $users
|
||||||
.filter(user => user.email.includes(search || ""))
|
.filter(user => user.email.includes(search || ""))
|
||||||
|
@ -80,10 +93,16 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let createUserModal
|
function showOnboardingTypeModal() {
|
||||||
let basicOnboardingModal = function openBasicOnboardingModal() {
|
onboardingTypeModal.show()
|
||||||
createUserModal.hide()
|
}
|
||||||
basicOnboardingModal.show()
|
|
||||||
|
function showConfirmationModal(onboardingType) {
|
||||||
|
if (onboardingType === "emailOnboarding") {
|
||||||
|
inviteConfirmationModal.show()
|
||||||
|
} else {
|
||||||
|
passwordModal.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -120,7 +139,9 @@
|
||||||
icon="UserAdd"
|
icon="UserAdd"
|
||||||
cta>Add Users</Button
|
cta>Add Users</Button
|
||||||
>
|
>
|
||||||
<Button icon="Import" primary>Import Users</Button>
|
<Button on:click={importUsersModal.show} icon="Import" primary
|
||||||
|
>Import Users</Button
|
||||||
|
>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Table
|
<Table
|
||||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||||
|
@ -142,8 +163,34 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
<AddUserModal />
|
<AddUserModal {showOnboardingTypeModal} />
|
||||||
</Modal>
|
</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>
|
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -56,6 +56,30 @@ export const TableNames = {
|
||||||
USERS: "ta_users",
|
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.
|
* API version header attached to all requests.
|
||||||
* Version changelog:
|
* Version changelog:
|
||||||
|
|
Loading…
Reference in New Issue