Add new UserFetch for searching users table and use in users page. Add debounce utility
This commit is contained in:
parent
e42b854825
commit
39dc0afdc9
|
@ -24,41 +24,53 @@
|
||||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants, Utils, fetchData } from "@budibase/frontend-core"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
const fetch = fetchData({
|
||||||
|
API,
|
||||||
|
datasource: {
|
||||||
|
type: "user",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
let enrichedUsers = []
|
let enrichedUsers = []
|
||||||
let createUserModal,
|
let createUserModal,
|
||||||
inviteConfirmationModal,
|
inviteConfirmationModal,
|
||||||
onboardingTypeModal,
|
onboardingTypeModal,
|
||||||
passwordModal,
|
passwordModal,
|
||||||
importUsersModal
|
importUsersModal
|
||||||
let pageInfo = createPaginationStore()
|
let searchEmail = undefined
|
||||||
let prevEmail = undefined,
|
|
||||||
searchEmail = undefined
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let customRenderers = [
|
let customRenderers = [
|
||||||
{ column: "userGroups", component: GroupsTableRenderer },
|
{ column: "userGroups", component: GroupsTableRenderer },
|
||||||
{ column: "apps", component: AppsTableRenderer },
|
{ column: "apps", component: AppsTableRenderer },
|
||||||
{ column: "role", component: RoleTableRenderer },
|
{ column: "role", component: RoleTableRenderer },
|
||||||
]
|
]
|
||||||
|
let userData = []
|
||||||
|
|
||||||
|
$: debouncedUpdateFetch(searchEmail)
|
||||||
$: schema = {
|
$: schema = {
|
||||||
email: {},
|
email: {
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
role: {
|
role: {
|
||||||
sortable: false,
|
sortable: false,
|
||||||
},
|
},
|
||||||
...($auth.groupsEnabled && {
|
...($auth.groupsEnabled && {
|
||||||
userGroups: { sortable: false, displayName: "User groups" },
|
userGroups: {
|
||||||
|
sortable: false,
|
||||||
|
displayName: "User groups",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
apps: {},
|
apps: {
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
$: userData = []
|
|
||||||
$: page = $pageInfo.page
|
|
||||||
$: fetchUsers(page, searchEmail)
|
|
||||||
$: {
|
$: {
|
||||||
enrichedUsers = $users.data?.map(user => {
|
enrichedUsers = $fetch.rows?.map(user => {
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
$groups.forEach(group => {
|
$groups.forEach(group => {
|
||||||
if (group.users) {
|
if (group.users) {
|
||||||
|
@ -78,6 +90,18 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFetch = email => {
|
||||||
|
if (email == null && $fetch.query.email == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetch.update({
|
||||||
|
query: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const debouncedUpdateFetch = Utils.debounce(updateFetch, 250)
|
||||||
|
|
||||||
const showOnboardingTypeModal = async addUsersData => {
|
const showOnboardingTypeModal = async addUsersData => {
|
||||||
userData = await removingDuplicities(addUsersData)
|
userData = await removingDuplicities(addUsersData)
|
||||||
if (!userData?.users?.length) return
|
if (!userData?.users?.length) return
|
||||||
|
@ -161,14 +185,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
await groups.actions.init()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error fetching User Group data")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteRows = async () => {
|
const deleteRows = async () => {
|
||||||
try {
|
try {
|
||||||
let ids = selectedRows.map(user => user._id)
|
let ids = selectedRows.map(user => user._id)
|
||||||
|
@ -179,88 +195,81 @@
|
||||||
await users.bulkDelete(ids)
|
await users.bulkDelete(ids)
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
await fetchUsers(page, searchEmail)
|
await fetch.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting rows")
|
notifications.error("Error deleting rows")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUsers(page, email) {
|
onMount(async () => {
|
||||||
if ($pageInfo.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// need to remove the page if they've started searching
|
|
||||||
if (email && !prevEmail) {
|
|
||||||
pageInfo.reset()
|
|
||||||
page = undefined
|
|
||||||
}
|
|
||||||
prevEmail = email
|
|
||||||
try {
|
try {
|
||||||
pageInfo.loading()
|
loaded = false
|
||||||
await users.search({ page, email })
|
await groups.actions.init()
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
loaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error fetching User Group data")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="M">
|
{#if loaded && $fetch.loaded}
|
||||||
<Layout gap="XS" noPadding>
|
<Layout noPadding gap="M">
|
||||||
<Heading>Users</Heading>
|
<Layout gap="XS" noPadding>
|
||||||
<Body>Add users and control who gets access to your published apps</Body>
|
<Heading>Users</Heading>
|
||||||
</Layout>
|
<Body>Add users and control who gets access to your published apps</Body>
|
||||||
<Divider />
|
</Layout>
|
||||||
<div class="controls">
|
<Divider />
|
||||||
<ButtonGroup>
|
<div class="controls">
|
||||||
<Button
|
<ButtonGroup>
|
||||||
dataCy="add-user"
|
<Button
|
||||||
on:click={createUserModal.show}
|
dataCy="add-user"
|
||||||
icon="UserAdd"
|
on:click={createUserModal.show}
|
||||||
cta>Add users</Button
|
icon="UserAdd"
|
||||||
>
|
cta>Add users</Button
|
||||||
<Button
|
>
|
||||||
on:click={importUsersModal.show}
|
<Button
|
||||||
icon="Import"
|
on:click={importUsersModal.show}
|
||||||
secondary
|
icon="Import"
|
||||||
newStyles
|
secondary
|
||||||
>
|
newStyles
|
||||||
Import users
|
>
|
||||||
</Button>
|
Import users
|
||||||
</ButtonGroup>
|
</Button>
|
||||||
<div class="controls-right">
|
</ButtonGroup>
|
||||||
<Search bind:value={searchEmail} placeholder="Search" />
|
<div class="controls-right">
|
||||||
{#if selectedRows.length > 0}
|
<Search bind:value={searchEmail} placeholder="Search" />
|
||||||
<DeleteRowsButton
|
{#if selectedRows.length > 0}
|
||||||
item="user"
|
<DeleteRowsButton
|
||||||
on:updaterows
|
item="user"
|
||||||
{selectedRows}
|
on:updaterows
|
||||||
{deleteRows}
|
{selectedRows}
|
||||||
/>
|
{deleteRows}
|
||||||
{/if}
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Table
|
||||||
<Table
|
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
{schema}
|
||||||
{schema}
|
bind:selectedRows
|
||||||
bind:selectedRows
|
data={enrichedUsers}
|
||||||
data={enrichedUsers}
|
allowEditColumns={false}
|
||||||
allowEditColumns={false}
|
allowEditRows={false}
|
||||||
allowEditRows={false}
|
allowSelectRows={true}
|
||||||
allowSelectRows={true}
|
showHeaderBorder={false}
|
||||||
showHeaderBorder={false}
|
{customRenderers}
|
||||||
{customRenderers}
|
|
||||||
/>
|
|
||||||
<div class="pagination">
|
|
||||||
<Pagination
|
|
||||||
page={$pageInfo.pageNumber}
|
|
||||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
|
||||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
|
||||||
goToPrevPage={pageInfo.prevPage}
|
|
||||||
goToNextPage={pageInfo.nextPage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div class="pagination">
|
||||||
</Layout>
|
<Pagination
|
||||||
|
page={$fetch.pageNumber + 1}
|
||||||
|
hasPrevPage={$fetch.loading ? false : $fetch.hasPrevPage}
|
||||||
|
hasNextPage={$fetch.loading ? false : $fetch.hasNextPage}
|
||||||
|
goToPrevPage={fetch.prevPage}
|
||||||
|
goToNextPage={fetch.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
<AddUserModal {showOnboardingTypeModal} />
|
<AddUserModal {showOnboardingTypeModal} />
|
||||||
|
@ -272,11 +281,11 @@
|
||||||
title="Invites sent!"
|
title="Invites sent!"
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
>
|
>
|
||||||
<Body size="S"
|
<Body size="S">
|
||||||
>Your users should now recieve an email invite to get access to their
|
Your users should now recieve an email invite to get access to their
|
||||||
Budibase account</Body
|
Budibase account
|
||||||
></ModalContent
|
</Body>
|
||||||
>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={onboardingTypeModal}>
|
<Modal bind:this={onboardingTypeModal}>
|
||||||
|
|
|
@ -158,6 +158,8 @@ export default class DataFetch {
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
cursors: [],
|
||||||
|
cursor: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Actually fetch data
|
// Actually fetch data
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import DataFetch from "./DataFetch.js"
|
||||||
|
import { TableNames } from "../constants"
|
||||||
|
|
||||||
|
export default class UserFetch extends DataFetch {
|
||||||
|
constructor(opts) {
|
||||||
|
super({
|
||||||
|
...opts,
|
||||||
|
datasource: {
|
||||||
|
tableId: TableNames.USERS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
determineFeatureFlags() {
|
||||||
|
return {
|
||||||
|
supportsSearch: true,
|
||||||
|
supportsSort: false,
|
||||||
|
supportsPagination: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData() {
|
||||||
|
const { cursor, query } = get(this.store)
|
||||||
|
try {
|
||||||
|
// "query" normally contains a lucene query, but users uses a non-standard
|
||||||
|
// search endpoint so we use query uniquely here
|
||||||
|
const res = await this.API.searchUsers({
|
||||||
|
page: cursor,
|
||||||
|
email: query.email,
|
||||||
|
appId: query.appId,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
rows: res?.data || [],
|
||||||
|
hasNextPage: res?.hasNextPage || false,
|
||||||
|
cursor: res?.nextPage || null,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
hasNextPage: false,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,12 +5,14 @@ import RelationshipFetch from "./RelationshipFetch.js"
|
||||||
import NestedProviderFetch from "./NestedProviderFetch.js"
|
import NestedProviderFetch from "./NestedProviderFetch.js"
|
||||||
import FieldFetch from "./FieldFetch.js"
|
import FieldFetch from "./FieldFetch.js"
|
||||||
import JSONArrayFetch from "./JSONArrayFetch.js"
|
import JSONArrayFetch from "./JSONArrayFetch.js"
|
||||||
|
import UserFetch from "./UserFetch.js"
|
||||||
|
|
||||||
const DataFetchMap = {
|
const DataFetchMap = {
|
||||||
table: TableFetch,
|
table: TableFetch,
|
||||||
view: ViewFetch,
|
view: ViewFetch,
|
||||||
query: QueryFetch,
|
query: QueryFetch,
|
||||||
link: RelationshipFetch,
|
link: RelationshipFetch,
|
||||||
|
user: UserFetch,
|
||||||
|
|
||||||
// Client specific datasource types
|
// Client specific datasource types
|
||||||
provider: NestedProviderFetch,
|
provider: NestedProviderFetch,
|
||||||
|
|
|
@ -19,3 +19,25 @@ export const sequential = fn => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to debounce an async function and ensure a minimum delay between
|
||||||
|
* invocations is enforced.
|
||||||
|
* @param callback an async function to run
|
||||||
|
* @param minDelay the minimum delay between invocations
|
||||||
|
* @returns {Promise} a debounced version of the callback
|
||||||
|
*/
|
||||||
|
export const debounce = (callback, minDelay = 1000) => {
|
||||||
|
let timeout
|
||||||
|
return async (...params) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(async () => {
|
||||||
|
console.log("timeout reached!")
|
||||||
|
resolve(await callback(...params))
|
||||||
|
}, minDelay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue