builder side panel changes to support inviting creators
This commit is contained in:
parent
e88efe2d1a
commit
8b8bce186c
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select, FancySelect } from "@budibase/bbui"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { licensing } from "stores/portal"
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let allowedRoles = null
|
export let allowedRoles = null
|
||||||
export let allowCreator = false
|
export let allowCreator = false
|
||||||
|
export let fancySelect = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const RemoveID = "remove"
|
const RemoveID = "remove"
|
||||||
|
@ -47,7 +48,7 @@
|
||||||
{
|
{
|
||||||
_id: CreatorID,
|
_id: CreatorID,
|
||||||
name: "Creator",
|
name: "Creator",
|
||||||
tag: $licensing.perAppBuildersEnabled && null,
|
tag: !$licensing.perAppBuildersEnabled && "Business",
|
||||||
},
|
},
|
||||||
...newRoles,
|
...newRoles,
|
||||||
]
|
]
|
||||||
|
@ -82,7 +83,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
console.log(e.detail)
|
|
||||||
if (allowRemove && e.detail === RemoveID) {
|
if (allowRemove && e.detail === RemoveID) {
|
||||||
dispatch("remove")
|
dispatch("remove")
|
||||||
} else if (e.detail === CreatorID) {
|
} else if (e.detail === CreatorID) {
|
||||||
|
@ -93,26 +93,53 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
{#if fancySelect}
|
||||||
{autoWidth}
|
<FancySelect
|
||||||
{quiet}
|
{autoWidth}
|
||||||
{disabled}
|
{quiet}
|
||||||
{align}
|
{disabled}
|
||||||
{footer}
|
{align}
|
||||||
bind:value
|
{footer}
|
||||||
on:change={onChange}
|
bind:value
|
||||||
{options}
|
on:change={onChange}
|
||||||
getOptionLabel={role => role.name}
|
{options}
|
||||||
getOptionValue={role => role._id}
|
label="Access on this app"
|
||||||
getOptionColour={getColor}
|
getOptionLabel={role => role.name}
|
||||||
getOptionIcon={getIcon}
|
getOptionValue={role => role._id}
|
||||||
isOptionEnabled={option => {
|
getOptionColour={getColor}
|
||||||
if (option._id == CreatorID && !$licensing.perAppBuildersEnabled) {
|
getOptionIcon={getIcon}
|
||||||
return false
|
isOptionEnabled={option => {
|
||||||
} else {
|
if (option._id == CreatorID && !$licensing.perAppBuildersEnabled) {
|
||||||
return true
|
return false
|
||||||
}
|
} else {
|
||||||
}}
|
return true
|
||||||
{placeholder}
|
}
|
||||||
{error}
|
}}
|
||||||
/>
|
{placeholder}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Select
|
||||||
|
{autoWidth}
|
||||||
|
{quiet}
|
||||||
|
{disabled}
|
||||||
|
{align}
|
||||||
|
{footer}
|
||||||
|
bind:value
|
||||||
|
on:change={onChange}
|
||||||
|
{options}
|
||||||
|
getOptionLabel={role => role.name}
|
||||||
|
getOptionValue={role => role._id}
|
||||||
|
getOptionColour={getColor}
|
||||||
|
getOptionIcon={getIcon}
|
||||||
|
isOptionEnabled={option => {
|
||||||
|
if (option._id == CreatorID && !$licensing.perAppBuildersEnabled) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{placeholder}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
Divider,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
|
@ -9,6 +10,10 @@
|
||||||
ActionButton,
|
ActionButton,
|
||||||
CopyInput,
|
CopyInput,
|
||||||
Modal,
|
Modal,
|
||||||
|
FancyForm,
|
||||||
|
FancyInput,
|
||||||
|
Button,
|
||||||
|
FancySelect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
||||||
|
@ -31,11 +36,17 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let inviting = false
|
let inviting = false
|
||||||
let searchFocus = false
|
let searchFocus = false
|
||||||
|
let invitingFlow = false
|
||||||
// Initially filter entities without app access
|
// Initially filter entities without app access
|
||||||
// Show all when false
|
// Show all when false
|
||||||
let filterByAppAccess = false
|
let filterByAppAccess = false
|
||||||
|
|
||||||
|
let email
|
||||||
|
let error
|
||||||
|
let form
|
||||||
|
let creationRoleType = "appUser"
|
||||||
|
let creationAccessType = "BASIC"
|
||||||
|
|
||||||
let appInvites = []
|
let appInvites = []
|
||||||
let filteredInvites = []
|
let filteredInvites = []
|
||||||
let filteredUsers = []
|
let filteredUsers = []
|
||||||
|
@ -54,7 +65,6 @@
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const showInvite = (invites, users, groups, query) => {
|
const showInvite = (invites, users, groups, query) => {
|
||||||
return !invites?.length && !users?.length && !groups?.length && query
|
return !invites?.length && !users?.length && !groups?.length && query
|
||||||
}
|
}
|
||||||
|
@ -292,7 +302,6 @@
|
||||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||||
$: console.log(filteredGroups)
|
|
||||||
/*
|
/*
|
||||||
Create pseudo users from the "users" attribute on app groups.
|
Create pseudo users from the "users" attribute on app groups.
|
||||||
These users will appear muted in the UI and show the ROLE
|
These users will appear muted in the UI and show the ROLE
|
||||||
|
@ -348,11 +357,20 @@
|
||||||
const payload = [
|
const payload = [
|
||||||
{
|
{
|
||||||
email: newUserEmail,
|
email: newUserEmail,
|
||||||
builder: false,
|
builder:
|
||||||
admin: false,
|
creationRoleType === Constants.BudibaseRoles.Admin ? true : false,
|
||||||
apps: { [prodAppId]: Constants.Roles.BASIC },
|
admin:
|
||||||
|
creationRoleType === Constants.BudibaseRoles.Admin ? true : false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (creationAccessType === Constants.Roles.CREATOR) {
|
||||||
|
payload[0].appBuilder = prodAppId
|
||||||
|
} else {
|
||||||
|
payload[0].apps = {
|
||||||
|
[prodAppId]: creationAccessType,
|
||||||
|
}
|
||||||
|
}
|
||||||
let userInviteResponse
|
let userInviteResponse
|
||||||
try {
|
try {
|
||||||
userInviteResponse = await users.onboard(payload)
|
userInviteResponse = await users.onboard(payload)
|
||||||
|
@ -364,6 +382,10 @@
|
||||||
return userInviteResponse
|
return userInviteResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openInviteFlow = () => {
|
||||||
|
invitingFlow = true
|
||||||
|
}
|
||||||
|
|
||||||
const onInviteUser = async () => {
|
const onInviteUser = async () => {
|
||||||
userOnboardResponse = await inviteUser()
|
userOnboardResponse = await inviteUser()
|
||||||
const originalQuery = query + ""
|
const originalQuery = query + ""
|
||||||
|
@ -391,6 +413,7 @@
|
||||||
notifications.error(inviteFailureResponse)
|
notifications.error(inviteFailureResponse)
|
||||||
}
|
}
|
||||||
userOnboardResponse = null
|
userOnboardResponse = null
|
||||||
|
invitingFlow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateUserInvite = async (invite, role) => {
|
const onUpdateUserInvite = async (invite, role) => {
|
||||||
|
@ -476,7 +499,17 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="builder-side-panel-header">
|
<div class="builder-side-panel-header">
|
||||||
<Heading size="S">Users</Heading>
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
invitingFlow = false
|
||||||
|
}}
|
||||||
|
class="header"
|
||||||
|
>
|
||||||
|
{#if invitingFlow}
|
||||||
|
<Icon name="BackAndroid" />
|
||||||
|
{/if}
|
||||||
|
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
|
||||||
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
color="var(--spectrum-global-color-gray-600)"
|
color="var(--spectrum-global-color-gray-600)"
|
||||||
name="RailRightClose"
|
name="RailRightClose"
|
||||||
|
@ -489,219 +522,280 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="search" class:focused={searchFocus}>
|
{#if !invitingFlow}
|
||||||
<span class="search-input">
|
<div class="search" class:focused={searchFocus}>
|
||||||
<Input
|
<span class="search-input">
|
||||||
placeholder={"Add users and groups to your app"}
|
<Input
|
||||||
autocomplete="off"
|
placeholder={"Add users and groups to your app"}
|
||||||
disabled={inviting}
|
autocomplete="off"
|
||||||
value={query}
|
disabled={inviting}
|
||||||
on:input={e => {
|
value={query}
|
||||||
query = e.target.value.trim()
|
on:input={e => {
|
||||||
}}
|
query = e.target.value.trim()
|
||||||
on:focus={() => (searchFocus = true)}
|
}}
|
||||||
on:blur={() => (searchFocus = false)}
|
on:focus={() => (searchFocus = true)}
|
||||||
/>
|
on:blur={() => (searchFocus = false)}
|
||||||
</span>
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
class:searching={query || !filterByAppAccess}
|
class:searching={query || !filterByAppAccess}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!filterByAppAccess) {
|
if (!filterByAppAccess) {
|
||||||
|
filterByAppAccess = true
|
||||||
|
}
|
||||||
|
if (!query) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query = null
|
||||||
|
userOnboardResponse = null
|
||||||
filterByAppAccess = true
|
filterByAppAccess = true
|
||||||
}
|
}}
|
||||||
if (!query) {
|
>
|
||||||
return
|
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||||
}
|
</span>
|
||||||
query = null
|
</div>
|
||||||
userOnboardResponse = null
|
|
||||||
filterByAppAccess = true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{#if promptInvite && !userOnboardResponse}
|
{#if promptInvite && !userOnboardResponse}
|
||||||
<Layout gap="S" paddingX="XL">
|
<Layout gap="S" paddingX="XL">
|
||||||
<div class="invite-header">
|
<div class="invite-header">
|
||||||
<Heading size="XS">No user found</Heading>
|
<Heading size="XS">No user found</Heading>
|
||||||
<div class="invite-directions">
|
<div class="invite-directions">
|
||||||
Add a valid email to invite a new user
|
Add a valid email to invite a new user
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="invite-form">
|
||||||
<div class="invite-form">
|
<span>{query || ""}</span>
|
||||||
<span>{query || ""}</span>
|
<ActionButton
|
||||||
<ActionButton
|
icon="UserAdd"
|
||||||
icon="UserAdd"
|
disabled={!queryIsEmail || inviting}
|
||||||
disabled={!queryIsEmail || inviting}
|
on:click={$licensing.userLimitReached
|
||||||
on:click={$licensing.userLimitReached
|
? userLimitReachedModal.show
|
||||||
? userLimitReachedModal.show
|
: openInviteFlow}
|
||||||
: onInviteUser}
|
>
|
||||||
>
|
Add user
|
||||||
Add user
|
</ActionButton>
|
||||||
</ActionButton>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
</Layout>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !promptInvite}
|
{#if !promptInvite}
|
||||||
|
<Layout gap="L" noPadding>
|
||||||
|
{#if filteredInvites?.length}
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<div class="auth-entity-header">
|
||||||
|
<div class="auth-entity-title">Pending invites</div>
|
||||||
|
<div class="auth-entity-access-title">Access</div>
|
||||||
|
</div>
|
||||||
|
{#each filteredInvites as invite}
|
||||||
|
<div class="auth-entity">
|
||||||
|
<div class="details">
|
||||||
|
<div class="user-email" title={invite.email}>
|
||||||
|
{invite.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-entity-access">
|
||||||
|
<RoleSelect
|
||||||
|
placeholder={false}
|
||||||
|
value={invite.info.apps?.[prodAppId]}
|
||||||
|
allowRemove={invite.info.apps?.[prodAppId]}
|
||||||
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
|
quiet={true}
|
||||||
|
on:addcreator={() => {
|
||||||
|
onUpdateUserInvite(invite, Constants.Roles.CREATOR)
|
||||||
|
}}
|
||||||
|
on:change={e => {
|
||||||
|
onUpdateUserInvite(invite, e.detail)
|
||||||
|
}}
|
||||||
|
on:remove={() => {
|
||||||
|
onUninviteAppUser(invite)
|
||||||
|
}}
|
||||||
|
autoWidth
|
||||||
|
align="right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<div class="auth-entity-header">
|
||||||
|
<div class="auth-entity-title">Groups</div>
|
||||||
|
<div class="auth-entity-access-title">Access</div>
|
||||||
|
</div>
|
||||||
|
{#each filteredGroups as group}
|
||||||
|
<div
|
||||||
|
class="auth-entity group"
|
||||||
|
on:click={() => {
|
||||||
|
if (selectedGroup != group._id) {
|
||||||
|
selectedGroup = group._id
|
||||||
|
} else {
|
||||||
|
selectedGroup = null
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:keydown={() => {}}
|
||||||
|
>
|
||||||
|
<div class="details">
|
||||||
|
<GroupIcon {group} size="S" />
|
||||||
|
<div>
|
||||||
|
{group.name}
|
||||||
|
</div>
|
||||||
|
<div class="auth-entity-meta">
|
||||||
|
{`${group.users?.length} user${
|
||||||
|
group.users?.length != 1 ? "s" : ""
|
||||||
|
}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-entity-access">
|
||||||
|
<RoleSelect
|
||||||
|
placeholder={false}
|
||||||
|
value={group.role}
|
||||||
|
allowRemove={group.role}
|
||||||
|
allowPublic={false}
|
||||||
|
quiet={true}
|
||||||
|
allowCreator={true}
|
||||||
|
on:change={e => {
|
||||||
|
onUpdateGroup(group, e.detail)
|
||||||
|
}}
|
||||||
|
on:addcreator={() => {
|
||||||
|
addGroupAppBuilder(group._id)
|
||||||
|
}}
|
||||||
|
on:remove={() => {
|
||||||
|
onUpdateGroup(group)
|
||||||
|
}}
|
||||||
|
autoWidth
|
||||||
|
align="right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if filteredUsers?.length}
|
||||||
|
<div class="auth-entity-section">
|
||||||
|
<div class="auth-entity-header">
|
||||||
|
<div class="auth-entity-title">Users</div>
|
||||||
|
<div class="auth-entity-access-title">Access</div>
|
||||||
|
</div>
|
||||||
|
{#each allUsers as user}
|
||||||
|
<div class="auth-entity">
|
||||||
|
<div class="details">
|
||||||
|
<div class="user-email" title={user.email}>
|
||||||
|
{user.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-entity-access" class:muted={user.group}>
|
||||||
|
<RoleSelect
|
||||||
|
footer={getRoleFooter(user)}
|
||||||
|
placeholder={false}
|
||||||
|
value={user.role}
|
||||||
|
allowRemove={user.role && !user.group}
|
||||||
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
|
quiet={true}
|
||||||
|
on:addcreator={() => {
|
||||||
|
addAppBuilder(user._id)
|
||||||
|
}}
|
||||||
|
on:change={e => {
|
||||||
|
onUpdateUser(user, e.detail)
|
||||||
|
}}
|
||||||
|
on:remove={() => {
|
||||||
|
onUpdateUser(user)
|
||||||
|
}}
|
||||||
|
autoWidth
|
||||||
|
align="right"
|
||||||
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
|
? [Constants.Roles.ADMIN]
|
||||||
|
: null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if userOnboardResponse?.created}
|
||||||
|
<Layout gap="S" paddingX="XL">
|
||||||
|
<div class="invite-header">
|
||||||
|
<Heading size="XS">User added!</Heading>
|
||||||
|
<div class="invite-directions">
|
||||||
|
Email invites are not available without SMTP configuration. Here
|
||||||
|
is the password that has been generated for this user.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CopyInput
|
||||||
|
value={userOnboardResponse.successful[0]?.password}
|
||||||
|
label="Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Divider />
|
||||||
|
<div class="body">
|
||||||
<Layout gap="L" noPadding>
|
<Layout gap="L" noPadding>
|
||||||
{#if filteredInvites?.length}
|
<div class="user-form">
|
||||||
<Layout noPadding gap="XS">
|
<FancyForm bind:this={form}>
|
||||||
<div class="auth-entity-header">
|
<FancyInput
|
||||||
<div class="auth-entity-title">Pending invites</div>
|
disabled={false}
|
||||||
<div class="auth-entity-access-title">Access</div>
|
label="Email"
|
||||||
|
value={query}
|
||||||
|
on:change={e => {
|
||||||
|
email = e.detail
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
if (!email) {
|
||||||
|
return "Please enter an email"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
<FancySelect
|
||||||
|
bind:value={creationRoleType}
|
||||||
|
options={sdk.users.isAdmin($auth.user)
|
||||||
|
? Constants.BudibaseRoleOptionsNew
|
||||||
|
: Constants.BudibaseRoleOptionsNew.filter(
|
||||||
|
option => option.value !== Constants.BudibaseRoles.Admin
|
||||||
|
)}
|
||||||
|
label="Role"
|
||||||
|
/>
|
||||||
|
{#if creationRoleType !== "ADMIN"}
|
||||||
|
<RoleSelect
|
||||||
|
placeholder={false}
|
||||||
|
bind:value={creationAccessType}
|
||||||
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
|
quiet={true}
|
||||||
|
autoWidth
|
||||||
|
align="right"
|
||||||
|
fancySelect
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</FancyForm>
|
||||||
|
{#if creationRoleType === Constants.Roles.ADMIN}
|
||||||
|
<div class="admin-info">
|
||||||
|
<Icon name="Info" />
|
||||||
|
Admins will get full access to all apps and settings
|
||||||
</div>
|
</div>
|
||||||
{#each filteredInvites as invite}
|
{/if}
|
||||||
<div class="auth-entity">
|
<span class="add-user">
|
||||||
<div class="details">
|
<Button newStyles cta on:click={onInviteUser}>Add user</Button>
|
||||||
<div class="user-email" title={invite.email}>
|
</span>
|
||||||
{invite.email}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="auth-entity-access">
|
|
||||||
<RoleSelect
|
|
||||||
placeholder={false}
|
|
||||||
value={invite.info.apps?.[prodAppId]}
|
|
||||||
allowRemove={invite.info.apps?.[prodAppId]}
|
|
||||||
allowPublic={false}
|
|
||||||
quiet={true}
|
|
||||||
on:change={e => {
|
|
||||||
onUpdateUserInvite(invite, e.detail)
|
|
||||||
}}
|
|
||||||
on:remove={() => {
|
|
||||||
onUninviteAppUser(invite)
|
|
||||||
}}
|
|
||||||
autoWidth
|
|
||||||
align="right"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<div class="auth-entity-header">
|
|
||||||
<div class="auth-entity-title">Groups</div>
|
|
||||||
<div class="auth-entity-access-title">Access</div>
|
|
||||||
</div>
|
|
||||||
{#each filteredGroups as group}
|
|
||||||
<div
|
|
||||||
class="auth-entity group"
|
|
||||||
on:click={() => {
|
|
||||||
if (selectedGroup != group._id) {
|
|
||||||
selectedGroup = group._id
|
|
||||||
} else {
|
|
||||||
selectedGroup = null
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:keydown={() => {}}
|
|
||||||
>
|
|
||||||
<div class="details">
|
|
||||||
<GroupIcon {group} size="S" />
|
|
||||||
<div>
|
|
||||||
{group.name}
|
|
||||||
</div>
|
|
||||||
<div class="auth-entity-meta">
|
|
||||||
{`${group.users?.length} user${
|
|
||||||
group.users?.length != 1 ? "s" : ""
|
|
||||||
}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="auth-entity-access">
|
|
||||||
<RoleSelect
|
|
||||||
placeholder={false}
|
|
||||||
value={group.role}
|
|
||||||
allowRemove={group.role}
|
|
||||||
allowPublic={false}
|
|
||||||
quiet={true}
|
|
||||||
allowCreator={true}
|
|
||||||
on:change={e => {
|
|
||||||
onUpdateGroup(group, e.detail)
|
|
||||||
}}
|
|
||||||
on:addcreator={() => {
|
|
||||||
addGroupAppBuilder(group._id)
|
|
||||||
}}
|
|
||||||
on:remove={() => {
|
|
||||||
onUpdateGroup(group)
|
|
||||||
}}
|
|
||||||
autoWidth
|
|
||||||
align="right"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if filteredUsers?.length}
|
|
||||||
<div class="auth-entity-section">
|
|
||||||
<div class="auth-entity-header">
|
|
||||||
<div class="auth-entity-title">Users</div>
|
|
||||||
<div class="auth-entity-access-title">Access</div>
|
|
||||||
</div>
|
|
||||||
{#each allUsers as user}
|
|
||||||
<div class="auth-entity">
|
|
||||||
<div class="details">
|
|
||||||
<div class="user-email" title={user.email}>
|
|
||||||
{user.email}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="auth-entity-access" class:muted={user.group}>
|
|
||||||
<RoleSelect
|
|
||||||
footer={getRoleFooter(user)}
|
|
||||||
placeholder={false}
|
|
||||||
value={user.role}
|
|
||||||
allowRemove={user.role && !user.group}
|
|
||||||
allowPublic={false}
|
|
||||||
allowCreator={true}
|
|
||||||
quiet={true}
|
|
||||||
on:addcreator={() => {
|
|
||||||
addAppBuilder(user._id)
|
|
||||||
}}
|
|
||||||
on:change={e => {
|
|
||||||
onUpdateUser(user, e.detail)
|
|
||||||
}}
|
|
||||||
on:remove={() => {
|
|
||||||
onUpdateUser(user)
|
|
||||||
}}
|
|
||||||
autoWidth
|
|
||||||
align="right"
|
|
||||||
allowedRoles={user.isAdminOrGlobalBuilder
|
|
||||||
? [Constants.Roles.ADMIN]
|
|
||||||
: null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if userOnboardResponse?.created}
|
|
||||||
<Layout gap="S" paddingX="XL">
|
|
||||||
<div class="invite-header">
|
|
||||||
<Heading size="XS">User added!</Heading>
|
|
||||||
<div class="invite-directions">
|
|
||||||
Email invites are not available without SMTP configuration. Here is
|
|
||||||
the password that has been generated for this user.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CopyInput
|
|
||||||
value={userOnboardResponse.successful[0]?.password}
|
|
||||||
label="Password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
<Modal bind:this={userLimitReachedModal}>
|
<Modal bind:this={userLimitReachedModal}>
|
||||||
<UpgradeModal {isOwner} />
|
<UpgradeModal {isOwner} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -717,6 +811,22 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-user {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-info {
|
||||||
|
padding: var(--spacing-l) var(--spacing-l) var(--spacing-xs)
|
||||||
|
var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
height: 50px;
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -856,6 +966,16 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-lf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -266,14 +266,17 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||||
|
|
||||||
// Temp password to be passed to the user.
|
// Temp password to be passed to the user.
|
||||||
createdPasswords[invite.email] = password
|
createdPasswords[invite.email] = password
|
||||||
|
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||||
|
if (invite.userInfo.appBuilder) {
|
||||||
|
builder.apps = [invite.userInfo.appBuilder]
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
password,
|
password,
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
roles: invite.userInfo.apps,
|
roles: invite.userInfo.apps,
|
||||||
admin: { global: false },
|
admin: { global: false },
|
||||||
builder: { global: false },
|
builder,
|
||||||
tenantId: tenancy.getTenantId(),
|
tenantId: tenancy.getTenantId(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -392,7 +395,7 @@ export const inviteAccept = async (
|
||||||
// info is an extension of the user object that was stored by global
|
// info is an extension of the user object that was stored by global
|
||||||
const { email, info }: any = await checkInviteCode(inviteCode)
|
const { email, info }: any = await checkInviteCode(inviteCode)
|
||||||
const user = await tenancy.doInTenant(info.tenantId, async () => {
|
const user = await tenancy.doInTenant(info.tenantId, async () => {
|
||||||
let request = {
|
let request: any = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
password,
|
password,
|
||||||
|
@ -400,9 +403,13 @@ export const inviteAccept = async (
|
||||||
roles: info.apps,
|
roles: info.apps,
|
||||||
tenantId: info.tenantId,
|
tenantId: info.tenantId,
|
||||||
}
|
}
|
||||||
|
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||||
|
|
||||||
|
if (info.appBuilder) {
|
||||||
|
builder.apps = [info.appBuilder]
|
||||||
|
request.builder = builder
|
||||||
|
}
|
||||||
delete info.apps
|
delete info.apps
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
...request,
|
...request,
|
||||||
...info,
|
...info,
|
||||||
|
|
Loading…
Reference in New Issue