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 0c139ad8be..4b0c6974e5 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte @@ -1,47 +1,28 @@ + +
+ +
+ + user._id)} + list={$users.data} + on:select={async e => { + await groups.actions.addUser(groupId, e.detail) + onUsersUpdated() + }} + on:deselect={async e => { + await groups.actions.removeUser(groupId, e.detail) + onUsersUpdated() + }} + /> + diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte new file mode 100644 index 0000000000..9c18008e44 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -0,0 +1,112 @@ + + +
+ Users + {#if !scimEnabled} + + {:else} + + {/if} +
+ + $goto(`../users/${e.detail._id}`)} +> +
+ 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/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.")