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 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}
|
||||
/>
|
||||
|
|
|
@ -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 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
|
||||
/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 =
|
||||
$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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
<UserGroupPicker
|
||||
key={"email"}
|
||||
title={"User"}
|
||||
bind:searchTerm
|
||||
bind:selected={selectedUsers}
|
||||
bind:filtered={filteredUsers}
|
||||
{addAll}
|
||||
select={selectUser}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
||||
|
|
|
@ -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>
|
||||
</Layout>
|
||||
<div class="delete-button">
|
||||
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
@ -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"
|
||||
<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>
|
||||
<div class="toggle">
|
||||
<Label size="L">Development access</Label>
|
||||
<Toggle text="" bind:value={builder} />
|
||||
</div>
|
||||
<div class="toggle">
|
||||
<Label size="L">Administration access</Label>
|
||||
<Toggle text="" bind:value={admin} />
|
||||
</div>
|
||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||
</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>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<style>
|
||||
.align {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<style>
|
||||
.align {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue