Merge pull request #6989 from Budibase/pc/fixes
User Management UI fixes
This commit is contained in:
commit
c3a4941119
|
@ -50,4 +50,5 @@ exports.getTenantFeatureFlags = tenantId => {
|
||||||
exports.FeatureFlag = {
|
exports.FeatureFlag = {
|
||||||
LICENSING: "LICENSING",
|
LICENSING: "LICENSING",
|
||||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||||
|
USER_GROUPS: "USER_GROUPS",
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,16 @@
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
class:is-focused={focus}
|
class:is-focused={focus}
|
||||||
>
|
>
|
||||||
|
{#if error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
{id}
|
{id}
|
||||||
on:click
|
on:click
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Icon from "../../Icon/Icon.svelte"
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
import Detail from "../../Typography/Detail.svelte"
|
import Detail from "../../Typography/Detail.svelte"
|
||||||
|
import Search from "./Search.svelte"
|
||||||
|
|
||||||
export let primaryLabel = ""
|
export let primaryLabel = ""
|
||||||
export let primaryValue = null
|
export let primaryValue = null
|
||||||
|
@ -22,7 +23,6 @@
|
||||||
export let secondaryFieldText = ""
|
export let secondaryFieldText = ""
|
||||||
export let secondaryFieldIcon = ""
|
export let secondaryFieldIcon = ""
|
||||||
export let secondaryFieldColour = ""
|
export let secondaryFieldColour = ""
|
||||||
export let getPrimaryOptionLabel = option => option
|
|
||||||
export let getPrimaryOptionValue = option => option
|
export let getPrimaryOptionValue = option => option
|
||||||
export let getPrimaryOptionColour = () => null
|
export let getPrimaryOptionColour = () => null
|
||||||
export let getPrimaryOptionIcon = () => null
|
export let getPrimaryOptionIcon = () => null
|
||||||
|
@ -43,17 +43,12 @@
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
|
||||||
$: groupTitles = Object.keys(primaryOptions)
|
$: groupTitles = Object.keys(primaryOptions)
|
||||||
$: filteredOptions = getFilteredOptions(
|
|
||||||
primaryOptions,
|
|
||||||
searchTerm,
|
|
||||||
getPrimaryOptionLabel
|
|
||||||
)
|
|
||||||
let iconData
|
let iconData
|
||||||
/*
|
|
||||||
$: iconData = primaryOptions?.find(x => {
|
const updateSearch = e => {
|
||||||
return x.name === primaryFieldText
|
dispatch("search", e.detail)
|
||||||
})
|
}
|
||||||
*/
|
|
||||||
const updateValue = newValue => {
|
const updateValue = newValue => {
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
return
|
return
|
||||||
|
@ -107,16 +102,6 @@
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredOptions = (options, term, getLabel) => {
|
|
||||||
if (autocomplete && term) {
|
|
||||||
const lowerCaseTerm = term.toLowerCase()
|
|
||||||
return options.filter(option => {
|
|
||||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -183,6 +168,15 @@
|
||||||
class:auto-width={autoWidth}
|
class:auto-width={autoWidth}
|
||||||
class:is-full-width={!secondaryOptions.length}
|
class:is-full-width={!secondaryOptions.length}
|
||||||
>
|
>
|
||||||
|
{#if autocomplete}
|
||||||
|
<Search
|
||||||
|
value={searchTerm}
|
||||||
|
on:change={event => updateSearch(event)}
|
||||||
|
{disabled}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
{#if placeholderOption}
|
{#if placeholderOption}
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
export let autofocus
|
export let autofocus
|
||||||
export let primaryOptions = []
|
export let primaryOptions = []
|
||||||
export let secondaryOptions = []
|
export let secondaryOptions = []
|
||||||
|
export let searchTerm
|
||||||
|
|
||||||
let primaryLabel
|
let primaryLabel
|
||||||
let secondaryLabel
|
let secondaryLabel
|
||||||
|
@ -87,10 +88,15 @@
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSearchTerm = e => {
|
||||||
|
searchTerm = e.detail
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<PickerDropdown
|
<PickerDropdown
|
||||||
|
{searchTerm}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{dataCy}
|
{dataCy}
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
|
@ -116,6 +122,7 @@
|
||||||
{secondaryLabel}
|
{secondaryLabel}
|
||||||
on:pickprimary={onPickPrimary}
|
on:pickprimary={onPickPrimary}
|
||||||
on:picksecondary={onPickSecondary}
|
on:picksecondary={onPickSecondary}
|
||||||
|
on:search={updateSearchTerm}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
on:blur
|
on:blur
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { get } from "svelte/store"
|
||||||
|
|
||||||
export const FEATURE_FLAGS = {
|
export const FEATURE_FLAGS = {
|
||||||
LICENSING: "LICENSING",
|
LICENSING: "LICENSING",
|
||||||
|
USER_GROUPS: "USER_GROUPS",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isEnabled = featureFlag => {
|
export const isEnabled = featureFlag => {
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
|
@ -52,11 +53,6 @@
|
||||||
href: "/builder/portal/manage/users",
|
href: "/builder/portal/manage/users",
|
||||||
heading: "Manage",
|
heading: "Manage",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "User Groups",
|
|
||||||
href: "/builder/portal/manage/groups",
|
|
||||||
},
|
|
||||||
|
|
||||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||||
{
|
{
|
||||||
|
@ -70,6 +66,15 @@
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (isEnabled(FEATURE_FLAGS.USER_GROUPS)) {
|
||||||
|
let item = {
|
||||||
|
title: "User Groups",
|
||||||
|
href: "/builder/portal/manage/groups",
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.splice(1, 0, item)
|
||||||
|
}
|
||||||
|
|
||||||
if (!$adminStore.cloud) {
|
if (!$adminStore.cloud) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
|
|
|
@ -41,6 +41,11 @@
|
||||||
let allAppList = []
|
let allAppList = []
|
||||||
let user
|
let user
|
||||||
$: fetchUser(userId)
|
$: fetchUser(userId)
|
||||||
|
|
||||||
|
$: fullName = $userFetch?.data?.firstName
|
||||||
|
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
|
||||||
|
: ""
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -127,7 +132,7 @@
|
||||||
if (detail === "developer") {
|
if (detail === "developer") {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
||||||
} else if (detail === "admin") {
|
} else if (detail === "admin") {
|
||||||
toggleFlags({ admin: { global: true }, builder: { global: false } })
|
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
||||||
} else if (detail === "appUser") {
|
} else if (detail === "appUser") {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
||||||
}
|
}
|
||||||
|
@ -186,15 +191,25 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<Avatar size="XXL" initials="PC" />
|
<Avatar
|
||||||
<div class="subtitle">
|
size="XXL"
|
||||||
<Heading size="S"
|
initials={user?.email
|
||||||
>{$userFetch?.data?.firstName +
|
.split(" ")
|
||||||
" " +
|
.map(x => x[0])
|
||||||
$userFetch?.data?.lastName}</Heading
|
.join("")}
|
||||||
>
|
/>
|
||||||
<Body size="XS">{$userFetch?.data?.email}</Body>
|
|
||||||
</div>
|
{#if fullName}
|
||||||
|
<div class="subtitle">
|
||||||
|
<Heading size="S">{fullName}</Heading>
|
||||||
|
|
||||||
|
<Body size="XS">{$userFetch?.data?.email}</Body>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alignEmail">
|
||||||
|
<Heading size="S">{$userFetch?.data?.email}</Heading>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -372,4 +387,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alignEmail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,15 +6,17 @@
|
||||||
Multiselect,
|
Multiselect,
|
||||||
InputDropdown,
|
InputDropdown,
|
||||||
Layout,
|
Layout,
|
||||||
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { groups, auth } from "stores/portal"
|
import { groups, auth } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import { emailValidator } from "helpers/validation"
|
||||||
|
|
||||||
export let showOnboardingTypeModal
|
export let showOnboardingTypeModal
|
||||||
const password = Math.random().toString(36).substring(2, 22)
|
const password = Math.random().toString(36).substring(2, 22)
|
||||||
let disabled
|
let disabled
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
|
$: errors = []
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -27,6 +29,10 @@
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function removeInput(idx) {
|
||||||
|
userData = userData.filter((e, i) => i !== idx)
|
||||||
|
}
|
||||||
function addNewInput() {
|
function addNewInput() {
|
||||||
userData = [
|
userData = [
|
||||||
...userData,
|
...userData,
|
||||||
|
@ -38,6 +44,18 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateInput(email, index) {
|
||||||
|
if (email) {
|
||||||
|
if (emailValidator(email) === true) {
|
||||||
|
errors[index] = true
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
errors[index] = false
|
||||||
|
return emailValidator(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -49,18 +67,40 @@
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
|
disabled={errors.some(x => x === false) ||
|
||||||
|
userData.some(x => x.email === "" || x.email === null)}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Email Address</Label>
|
<Label>Email Address</Label>
|
||||||
|
|
||||||
{#each userData as input, index}
|
{#each userData as input, index}
|
||||||
<InputDropdown
|
<div
|
||||||
inputType="email"
|
style="display: flex;
|
||||||
bind:inputValue={input.email}
|
align-items: center;
|
||||||
bind:dropdownValue={input.role}
|
flex-direction: row;"
|
||||||
options={Constants.BbRoles}
|
>
|
||||||
error={input.error}
|
<div style="width: 90%">
|
||||||
/>
|
<InputDropdown
|
||||||
|
inputType="email"
|
||||||
|
bind:inputValue={input.email}
|
||||||
|
bind:dropdownValue={input.role}
|
||||||
|
options={Constants.BbRoles}
|
||||||
|
error={validateInput(input.email, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class:fix-height={errors.length && !errors[index]}
|
||||||
|
class:normal-height={errors.length && !!errors[index]}
|
||||||
|
style="width: 10% "
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => removeInput(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||||
|
@ -80,6 +120,14 @@
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.fix-height {
|
||||||
|
margin-bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal-height {
|
||||||
|
margin-bottom: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.spectrum-Picker) {
|
:global(.spectrum-Picker) {
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
</div>
|
</div>
|
||||||
{value}
|
{value}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text">Not Available</div>
|
<div class="text">-</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -72,19 +72,12 @@
|
||||||
name: {},
|
name: {},
|
||||||
email: {},
|
email: {},
|
||||||
role: {
|
role: {
|
||||||
noPropagation: true,
|
|
||||||
sortable: false,
|
sortable: false,
|
||||||
},
|
},
|
||||||
...(hasGroupsLicense && {
|
...(hasGroupsLicense && {
|
||||||
userGroups: { sortable: false, displayName: "User groups" },
|
userGroups: { sortable: false, displayName: "User groups" },
|
||||||
}),
|
}),
|
||||||
apps: { width: "120px" },
|
apps: {},
|
||||||
settings: {
|
|
||||||
sortable: false,
|
|
||||||
width: "60px",
|
|
||||||
displayName: "",
|
|
||||||
align: "Right",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: userData = []
|
$: userData = []
|
||||||
|
@ -323,6 +316,13 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
||||||
|
|
||||||
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
||||||
$: tabs = ["Overview", "Automation History", "Backups", "Settings"]
|
$: tabs = ["Overview", "Automation History", "Backups", "Settings", "Access"]
|
||||||
$: selectedTab = "Overview"
|
$: selectedTab = "Overview"
|
||||||
|
|
||||||
const backToAppList = () => {
|
const backToAppList = () => {
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let fixedAppId
|
let fixedAppId
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, search)
|
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
|
@ -37,12 +36,6 @@
|
||||||
|
|
||||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||||
|
|
||||||
$: appUsers =
|
|
||||||
$users.data?.filter(x => {
|
|
||||||
return Object.keys(x.roles).find(y => {
|
|
||||||
return y === fixedAppId
|
|
||||||
})
|
|
||||||
}) || []
|
|
||||||
$: appGroups = $groups.filter(x => {
|
$: appGroups = $groups.filter(x => {
|
||||||
return x.apps.includes(app.appId)
|
return x.apps.includes(app.appId)
|
||||||
})
|
})
|
||||||
|
@ -130,6 +123,12 @@
|
||||||
pageInfo.loading()
|
pageInfo.loading()
|
||||||
await users.search({ page, appId: fixedAppId })
|
await users.search({ page, appId: fixedAppId })
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
|
appUsers =
|
||||||
|
$users.data?.filter(x => {
|
||||||
|
return Object.keys(x.roles).find(y => {
|
||||||
|
return y === fixedAppId
|
||||||
|
})
|
||||||
|
}) || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error getting user list")
|
||||||
}
|
}
|
||||||
|
@ -137,6 +136,8 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
await fetchUsers(page, search)
|
||||||
|
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
await apps.load()
|
await apps.load()
|
||||||
await roles.fetch()
|
await roles.fetch()
|
||||||
|
@ -212,8 +213,14 @@
|
||||||
page={$pageInfo.pageNumber}
|
page={$pageInfo.pageNumber}
|
||||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||||
goToPrevPage={pageInfo.prevPage}
|
goToPrevPage={async () => {
|
||||||
goToNextPage={pageInfo.nextPage}
|
await pageInfo.prevPage()
|
||||||
|
fetchUsers(page, search)
|
||||||
|
}}
|
||||||
|
goToNextPage={async () => {
|
||||||
|
await pageInfo.nextPage()
|
||||||
|
fetchUsers(page, search)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -264,4 +271,11 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
export let app
|
export let app
|
||||||
export let addData
|
export let addData
|
||||||
export let appUsers = []
|
export let appUsers = []
|
||||||
|
|
||||||
let prevSearch = undefined,
|
let prevSearch = undefined,
|
||||||
search = undefined
|
search = undefined
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
|
@ -32,7 +31,7 @@
|
||||||
prevSearch = search
|
prevSearch = search
|
||||||
try {
|
try {
|
||||||
pageInfo.loading()
|
pageInfo.loading()
|
||||||
await users.search({ page, search })
|
await users.search({ page, email: search })
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error getting user list")
|
||||||
|
@ -83,10 +82,10 @@
|
||||||
<PickerDropdown
|
<PickerDropdown
|
||||||
autocomplete
|
autocomplete
|
||||||
primaryOptions={optionSections}
|
primaryOptions={optionSections}
|
||||||
placeholder={"Search Users"}
|
|
||||||
secondaryOptions={$roles}
|
secondaryOptions={$roles}
|
||||||
bind:primaryValue={input.id}
|
bind:primaryValue={input.id}
|
||||||
bind:secondaryValue={input.role}
|
bind:secondaryValue={input.role}
|
||||||
|
bind:searchTerm={search}
|
||||||
getPrimaryOptionLabel={group => group.name}
|
getPrimaryOptionLabel={group => group.name}
|
||||||
getPrimaryOptionValue={group => group.name}
|
getPrimaryOptionValue={group => group.name}
|
||||||
getPrimaryOptionIcon={group => group.icon}
|
getPrimaryOptionIcon={group => group.icon}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
export let app
|
export let app
|
||||||
export let deployments
|
export let deployments
|
||||||
export let navigateTab
|
export let navigateTab
|
||||||
|
let userCount
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const unpublishApp = () => {
|
const unpublishApp = () => {
|
||||||
|
@ -40,7 +40,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await users.search({ page: undefined, appId: "app_" + app.appId })
|
let resp = await users.getUserCountByApp({ appId: "app_" + app.appId })
|
||||||
|
userCount = resp.userCount
|
||||||
|
await users.search({ appId: "app_" + app.appId, limit: 4 })
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -155,7 +157,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="users-text">
|
<div class="users-text">
|
||||||
{$users?.data.length} users have access to this app
|
{userCount}
|
||||||
|
{userCount > 1 ? `users have` : `user has`} access to this app
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -61,6 +61,7 @@ export function createUsersStore() {
|
||||||
break
|
break
|
||||||
case "admin":
|
case "admin":
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
|
body.builder = { global: true }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +78,10 @@ export function createUsersStore() {
|
||||||
update(users => users.filter(user => user._id !== id))
|
update(users => users.filter(user => user._id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUserCountByApp({ appId }) {
|
||||||
|
return await API.getUserCountByApp({ appId })
|
||||||
|
}
|
||||||
|
|
||||||
async function bulkDelete(userIds) {
|
async function bulkDelete(userIds) {
|
||||||
await API.deleteUsers(userIds)
|
await API.deleteUsers(userIds)
|
||||||
}
|
}
|
||||||
|
@ -99,6 +104,7 @@ export function createUsersStore() {
|
||||||
create,
|
create,
|
||||||
save,
|
save,
|
||||||
bulkDelete,
|
bulkDelete,
|
||||||
|
getUserCountByApp,
|
||||||
delete: del,
|
delete: del,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,4 +172,15 @@ export const buildUserEndpoints = API => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts an invite to join the platform and creates a user.
|
||||||
|
* @param inviteCode the invite code sent in the email
|
||||||
|
* @param password the password for the newly created user
|
||||||
|
*/
|
||||||
|
getUserCountByApp: async ({ appId }) => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/global/users/count/${appId}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -32,8 +32,10 @@ exports.updateAppRole = (user, { appId } = {}) => {
|
||||||
// if a role wasn't found then either set as admin (builder) or public (everyone else)
|
// if a role wasn't found then either set as admin (builder) or public (everyone else)
|
||||||
if (!user.roleId && user.builder && user.builder.global) {
|
if (!user.roleId && user.builder && user.builder.global) {
|
||||||
user.roleId = BUILTIN_ROLE_IDS.ADMIN
|
user.roleId = BUILTIN_ROLE_IDS.ADMIN
|
||||||
} else if (!user.roleId) {
|
} else if (!user.roleId && !user?.userGroups?.length) {
|
||||||
user.roleId = BUILTIN_ROLE_IDS.PUBLIC
|
user.roleId = BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
} else if (user?.userGroups?.length) {
|
||||||
|
user.roleId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
delete user.roles
|
delete user.roles
|
||||||
|
@ -41,10 +43,8 @@ exports.updateAppRole = (user, { appId } = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkGroupRoles(user, { appId } = {}) {
|
async function checkGroupRoles(user, { appId } = {}) {
|
||||||
if (!user.roleId) {
|
let roleId = await groups.getGroupRoleId(user, appId)
|
||||||
let roleId = await groups.getGroupRoleId(user, appId)
|
user.roleId = roleId
|
||||||
user.roleId = roleId
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ async function processUser(user, { appId } = {}) {
|
||||||
delete user.password
|
delete user.password
|
||||||
}
|
}
|
||||||
user = await exports.updateAppRole(user, { appId })
|
user = await exports.updateAppRole(user, { appId })
|
||||||
if (user?.userGroups?.length) {
|
if (!user.roleId && user?.userGroups?.length) {
|
||||||
user = await checkGroupRoles(user, { appId })
|
user = await checkGroupRoles(user, { appId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { checkInviteCode } from "../../../utilities/redis"
|
||||||
import { sendEmail } from "../../../utilities/email"
|
import { sendEmail } from "../../../utilities/email"
|
||||||
import { users } from "../../../sdk"
|
import { users } from "../../../sdk"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { User, CloudAccount, UserGroup } from "@budibase/types"
|
import { User, CloudAccount } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
events,
|
events,
|
||||||
errors,
|
errors,
|
||||||
|
@ -114,6 +114,16 @@ export const adminUser = async (ctx: any) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const countByApp = async (ctx: any) => {
|
||||||
|
const appId = ctx.params.appId
|
||||||
|
try {
|
||||||
|
const response = await users.countUsersByApp(appId)
|
||||||
|
ctx.body = response
|
||||||
|
} catch (err: any) {
|
||||||
|
ctx.throw(err.status || 400, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const destroy = async (ctx: any) => {
|
export const destroy = async (ctx: any) => {
|
||||||
const id = ctx.params.id
|
const id = ctx.params.id
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ router
|
||||||
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
||||||
.delete("/api/global/users/:id", adminOnly, controller.destroy)
|
.delete("/api/global/users/:id", adminOnly, controller.destroy)
|
||||||
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
|
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
|
||||||
|
.get("/api/global/users/count/:appId", adminOnly, controller.countByApp)
|
||||||
.get("/api/global/roles/:appId")
|
.get("/api/global/roles/:appId")
|
||||||
.post(
|
.post(
|
||||||
"/api/global/users/invite",
|
"/api/global/users/invite",
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { groups as groupUtils } from "@budibase/pro"
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
|
|
||||||
export const allUsers = async (newDb?: any) => {
|
export const allUsers = async () => {
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
dbUtils.getGlobalUserParams(null, {
|
dbUtils.getGlobalUserParams(null, {
|
||||||
|
@ -30,6 +30,13 @@ export const allUsers = async (newDb?: any) => {
|
||||||
return response.rows.map((row: any) => row.doc)
|
return response.rows.map((row: any) => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const countUsersByApp = async (appId: string) => {
|
||||||
|
let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
|
||||||
|
return {
|
||||||
|
userCount: response.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const paginatedUsers = async ({
|
export const paginatedUsers = async ({
|
||||||
page,
|
page,
|
||||||
email,
|
email,
|
||||||
|
|
Loading…
Reference in New Issue