diff --git a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte index 1be019b83e..589df5c599 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte @@ -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() + }} /> This user group doesn't have any users
+ + diff --git a/packages/builder/src/stores/portal/groups.js b/packages/builder/src/stores/portal/groups.js index eda3961e2b..c7a54c7e6d 100644 --- a/packages/builder/src/stores/portal/groups.js +++ b/packages/builder/src/stores/portal/groups.js @@ -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) } }, diff --git a/packages/frontend-core/src/api/groups.js b/packages/frontend-core/src/api/groups.js index c27f11e0ea..cbc5bfd72a 100644 --- a/packages/frontend-core/src/api/groups.js +++ b/packages/frontend-core/src/api/groups.js @@ -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 diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index f68b37dcca..18a00c08d5 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -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, })) } diff --git a/packages/frontend-core/src/fetch/GroupUserFetch.js b/packages/frontend-core/src/fetch/GroupUserFetch.js new file mode 100644 index 0000000000..b0ca9a5388 --- /dev/null +++ b/packages/frontend-core/src/fetch/GroupUserFetch.js @@ -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, + } + } + } +} diff --git a/packages/frontend-core/src/fetch/fetchData.js b/packages/frontend-core/src/fetch/fetchData.js index 4974816496..c4968eabc0 100644 --- a/packages/frontend-core/src/fetch/fetchData.js +++ b/packages/frontend-core/src/fetch/fetchData.js @@ -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, diff --git a/packages/pro b/packages/pro index 3a6dba1a72..4d640bfa74 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 3a6dba1a7292384553900a284fa63422ee070bfa +Subproject commit 4d640bfa74945d2e8675af0022fba42c738679f0 diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts index fedd8426f0..b74e59c020 100644 --- a/packages/types/src/documents/global/userGroup.ts +++ b/packages/types/src/documents/global/userGroup.ts @@ -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 + }[] +} diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 2cccd3be6a..58b3b7e5bc 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -65,6 +65,7 @@ export type DatabaseQueryOpts = { key?: string keys?: string[] group?: boolean + startkey_docid?: string } export const isDocument = (doc: any): doc is Document => { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index d9ebc87517..33335379c0 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -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 +) => { 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) => { + 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) => { + 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) => { + 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) => { + 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) => { + 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.")