Merge pull request #10497 from Budibase/budi-6158/paginage-group-users
BUDI-6158 - Paginage group users
This commit is contained in:
commit
bb86465541
10 changed files with 173 additions and 31 deletions
|
@ -11,7 +11,10 @@
|
|||
ActionMenu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Pagination,
|
||||
} from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { users, apps, groups, auth, features } from "stores/portal"
|
||||
|
@ -28,6 +31,18 @@
|
|||
|
||||
export let groupId
|
||||
|
||||
const fetchGroupUsers = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "groupUser",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
groupId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
$: userSchema = {
|
||||
email: {
|
||||
width: "1fr",
|
||||
|
@ -71,16 +86,15 @@
|
|||
let popover
|
||||
let searchTerm = ""
|
||||
let prevSearch = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let searchUsersPageInfo = createPaginationStore()
|
||||
let loaded = false
|
||||
let editModal, deleteModal
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !$auth.isAdmin || scimEnabled
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, searchTerm)
|
||||
$: page = $searchUsersPageInfo.page
|
||||
$: searchUsers(page, searchTerm)
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
$: filtered = $users.data
|
||||
$: groupApps = $apps
|
||||
.filter(app =>
|
||||
groups.actions
|
||||
|
@ -97,20 +111,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchUsers(page, search) {
|
||||
if ($pageInfo.loading) {
|
||||
async function searchUsers(page, search) {
|
||||
if ($searchUsersPageInfo.loading) {
|
||||
return
|
||||
}
|
||||
// need to remove the page if they've started searching
|
||||
if (search && !prevSearch) {
|
||||
pageInfo.reset()
|
||||
searchUsersPageInfo.reset()
|
||||
page = undefined
|
||||
}
|
||||
prevSearch = search
|
||||
try {
|
||||
pageInfo.loading()
|
||||
searchUsersPageInfo.loading()
|
||||
await users.search({ page, email: search })
|
||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||
searchUsersPageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user list")
|
||||
}
|
||||
|
@ -136,6 +150,7 @@
|
|||
|
||||
const removeUser = async id => {
|
||||
await groups.actions.removeUser(groupId, id)
|
||||
fetchGroupUsers.refresh()
|
||||
}
|
||||
|
||||
const removeApp = async app => {
|
||||
|
@ -203,15 +218,21 @@
|
|||
labelKey="email"
|
||||
selected={group.users?.map(user => user._id)}
|
||||
list={$users.data}
|
||||
on:select={e => groups.actions.addUser(groupId, e.detail)}
|
||||
on:deselect={e => groups.actions.removeUser(groupId, e.detail)}
|
||||
on:select={async e => {
|
||||
await groups.actions.addUser(groupId, e.detail)
|
||||
fetchGroupUsers.getInitialData()
|
||||
}}
|
||||
on:deselect={async e => {
|
||||
await groups.actions.removeUser(groupId, e.detail)
|
||||
fetchGroupUsers.getInitialData()
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
schema={userSchema}
|
||||
data={group?.users}
|
||||
data={$fetchGroupUsers?.rows}
|
||||
allowEditRows={false}
|
||||
customPlaceholder
|
||||
customRenderers={customUserTableRenderers}
|
||||
|
@ -221,6 +242,24 @@
|
|||
<Heading size="S">This user group doesn't have any users</Heading>
|
||||
</div>
|
||||
</Table>
|
||||
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$fetchGroupUsers.pageNumber + 1}
|
||||
hasPrevPage={$fetchGroupUsers.loading
|
||||
? false
|
||||
: $fetchGroupUsers.hasPrevPage}
|
||||
hasNextPage={$fetchGroupUsers.loading
|
||||
? false
|
||||
: $fetchGroupUsers.hasNextPage}
|
||||
goToPrevPage={$fetchGroupUsers.loading
|
||||
? null
|
||||
: fetchGroupUsers.prevPage}
|
||||
goToNextPage={$fetchGroupUsers.loading
|
||||
? null
|
||||
: fetchGroupUsers.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
|
|
|
@ -28,7 +28,7 @@ export function createGroupsStore() {
|
|||
// on the backend anyway
|
||||
if (get(licensing).groupsEnabled) {
|
||||
const groups = await API.getGroups()
|
||||
store.set(groups)
|
||||
store.set(groups.data)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -52,6 +52,20 @@ export const buildGroupsEndpoints = API => {
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a group users by the group id
|
||||
*/
|
||||
getGroupUsers: async ({ id, bookmark }) => {
|
||||
let url = `/api/global/groups/${id}/users?`
|
||||
if (bookmark) {
|
||||
url += `bookmark=${bookmark}`
|
||||
}
|
||||
|
||||
return await API.get({
|
||||
url,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds users to a group
|
||||
* @param groupId The group to update
|
||||
|
|
|
@ -362,13 +362,35 @@ export default class DataFetch {
|
|||
return
|
||||
}
|
||||
this.store.update($store => ({ ...$store, loading: true }))
|
||||
const { rows, info, error } = await this.getPage()
|
||||
const { rows, info, error, cursor } = await this.getPage()
|
||||
|
||||
let { cursors } = get(this.store)
|
||||
const { pageNumber } = get(this.store)
|
||||
|
||||
if (!rows.length && pageNumber > 0) {
|
||||
// If the full page is gone but we have previous pages, navigate to the previous page
|
||||
this.store.update($store => ({
|
||||
...$store,
|
||||
loading: false,
|
||||
cursors: cursors.slice(0, pageNumber),
|
||||
}))
|
||||
return await this.prevPage()
|
||||
}
|
||||
|
||||
const currentNextCursor = cursors[pageNumber + 1]
|
||||
if (currentNextCursor != cursor) {
|
||||
// If the current cursor changed, all the next pages need to be updated, so we mark them as stale
|
||||
cursors = cursors.slice(0, pageNumber + 1)
|
||||
cursors[pageNumber + 1] = cursor
|
||||
}
|
||||
|
||||
this.store.update($store => ({
|
||||
...$store,
|
||||
rows,
|
||||
info,
|
||||
loading: false,
|
||||
error,
|
||||
cursors,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
50
packages/frontend-core/src/fetch/GroupUserFetch.js
Normal file
50
packages/frontend-core/src/fetch/GroupUserFetch.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
export default class GroupUserFetch extends DataFetch {
|
||||
constructor(opts) {
|
||||
super({
|
||||
...opts,
|
||||
datasource: {
|
||||
tableId: TableNames.USERS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
supportsSort: false,
|
||||
supportsPagination: true,
|
||||
}
|
||||
}
|
||||
|
||||
async getDefinition() {
|
||||
return {
|
||||
schema: {},
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { query, cursor } = get(this.store)
|
||||
try {
|
||||
const res = await this.API.getGroupUsers({
|
||||
id: query.groupId,
|
||||
bookmark: cursor,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: res?.users || [],
|
||||
hasNextPage: res?.hasNextPage || false,
|
||||
cursor: res?.bookmark || null,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
rows: [],
|
||||
hasNextPage: false,
|
||||
error,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import NestedProviderFetch from "./NestedProviderFetch.js"
|
|||
import FieldFetch from "./FieldFetch.js"
|
||||
import JSONArrayFetch from "./JSONArrayFetch.js"
|
||||
import UserFetch from "./UserFetch.js"
|
||||
import GroupUserFetch from "./GroupUserFetch.js"
|
||||
|
||||
const DataFetchMap = {
|
||||
table: TableFetch,
|
||||
|
@ -13,6 +14,7 @@ const DataFetchMap = {
|
|||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
user: UserFetch,
|
||||
groupUser: GroupUserFetch,
|
||||
|
||||
// Client specific datasource types
|
||||
provider: NestedProviderFetch,
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 3a6dba1a7292384553900a284fa63422ee070bfa
|
||||
Subproject commit 4d640bfa74945d2e8675af0022fba42c738679f0
|
|
@ -1,3 +1,4 @@
|
|||
import { PaginationResponse } from "../../api"
|
||||
import { Document } from "../document"
|
||||
|
||||
export interface UserGroup extends Document {
|
||||
|
@ -21,3 +22,15 @@ export interface GroupUser {
|
|||
export interface UserGroupRoles {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface SearchGroupRequest {}
|
||||
export interface SearchGroupResponse {
|
||||
data: UserGroup[]
|
||||
}
|
||||
|
||||
export interface SearchUserGroupResponse extends PaginationResponse {
|
||||
users: {
|
||||
_id: any
|
||||
email: any
|
||||
}[]
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ export type DatabaseQueryOpts = {
|
|||
key?: string
|
||||
keys?: string[]
|
||||
group?: boolean
|
||||
startkey_docid?: string
|
||||
}
|
||||
|
||||
export const isDocument = (doc: any): doc is Document => {
|
||||
|
|
|
@ -69,9 +69,11 @@ const bulkCreate = async (users: User[], groupIds: string[]) => {
|
|||
return await userSdk.bulkCreate(users, groupIds)
|
||||
}
|
||||
|
||||
export const bulkUpdate = async (ctx: any) => {
|
||||
export const bulkUpdate = async (
|
||||
ctx: Ctx<BulkUserRequest, BulkUserResponse>
|
||||
) => {
|
||||
const currentUserId = ctx.user._id
|
||||
const input = ctx.request.body as BulkUserRequest
|
||||
const input = ctx.request.body
|
||||
let created, deleted
|
||||
try {
|
||||
if (input.create) {
|
||||
|
@ -83,7 +85,7 @@ export const bulkUpdate = async (ctx: any) => {
|
|||
} catch (err: any) {
|
||||
ctx.throw(err.status || 400, err?.message || err)
|
||||
}
|
||||
ctx.body = { created, deleted } as BulkUserResponse
|
||||
ctx.body = { created, deleted }
|
||||
}
|
||||
|
||||
const parseBooleanParam = (param: any) => {
|
||||
|
@ -184,15 +186,15 @@ export const destroy = async (ctx: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getAppUsers = async (ctx: any) => {
|
||||
const body = ctx.request.body as SearchUsersRequest
|
||||
export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||
const body = ctx.request.body
|
||||
const users = await userSdk.getUsersByAppAccess(body?.appId)
|
||||
|
||||
ctx.body = { data: users }
|
||||
}
|
||||
|
||||
export const search = async (ctx: any) => {
|
||||
const body = ctx.request.body as SearchUsersRequest
|
||||
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||
const body = ctx.request.body
|
||||
|
||||
if (body.paginated === false) {
|
||||
await getAppUsers(ctx)
|
||||
|
@ -238,8 +240,8 @@ export const tenantUserLookup = async (ctx: any) => {
|
|||
/*
|
||||
Encapsulate the app user onboarding flows here.
|
||||
*/
|
||||
export const onboardUsers = async (ctx: any) => {
|
||||
const request = ctx.request.body as InviteUsersRequest | BulkUserRequest
|
||||
export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||
const request = ctx.request.body
|
||||
const isBulkCreate = "create" in request
|
||||
|
||||
const emailConfigured = await isEmailConfigured()
|
||||
|
@ -255,7 +257,7 @@ export const onboardUsers = async (ctx: any) => {
|
|||
} else if (emailConfigured) {
|
||||
onboardingResponse = await inviteMultiple(ctx)
|
||||
} else if (!emailConfigured) {
|
||||
const inviteRequest = ctx.request.body as InviteUsersRequest
|
||||
const inviteRequest = ctx.request.body
|
||||
|
||||
let createdPasswords: any = {}
|
||||
|
||||
|
@ -295,10 +297,10 @@ export const onboardUsers = async (ctx: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const invite = async (ctx: any) => {
|
||||
const request = ctx.request.body as InviteUserRequest
|
||||
export const invite = async (ctx: Ctx<InviteUserRequest>) => {
|
||||
const request = ctx.request.body
|
||||
|
||||
let multiRequest = [request] as InviteUsersRequest
|
||||
let multiRequest = [request]
|
||||
const response = await userSdk.invite(multiRequest)
|
||||
|
||||
// explicitly throw for single user invite
|
||||
|
@ -318,8 +320,8 @@ export const invite = async (ctx: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const inviteMultiple = async (ctx: any) => {
|
||||
const request = ctx.request.body as InviteUsersRequest
|
||||
export const inviteMultiple = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||
const request = ctx.request.body
|
||||
ctx.body = await userSdk.invite(request)
|
||||
}
|
||||
|
||||
|
@ -424,7 +426,6 @@ export const inviteAccept = async (
|
|||
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
|
||||
// explicitly re-throw limit exceeded errors
|
||||
ctx.throw(400, err)
|
||||
return
|
||||
}
|
||||
console.warn("Error inviting user", err)
|
||||
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
||||
|
|
Loading…
Reference in a new issue