diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index aa40f13775..be49b9f261 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -21,7 +21,7 @@ export enum ViewName { AUTOMATION_LOGS = "automation_logs", ACCOUNT_BY_EMAIL = "account_by_email", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", - USER_BY_GROUP = "by_group_user", + USER_BY_GROUP = "user_by_group", APP_BACKUP_BY_TRIGGER = "by_trigger", } 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 index 9c18008e44..3159425b06 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -1,7 +1,7 @@
- Users {#if !scimEnabled} {:else} {/if} -
+
+ +
+ $goto(`../users/${e.detail._id}`)} >
- This user group doesn't have any users + {emailSearch + ? `No users found matching the email "${emailSearch}"` + : "This user group doesn't have any users"}
@@ -98,7 +108,7 @@ .header { display: flex; flex-direction: row; - justify-content: flex-start; + justify-content: space-between; align-items: center; gap: var(--spacing-l); } @@ -109,4 +119,15 @@ width: 100%; text-align: center; } + + .controls-right { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: var(--spacing-xl); + } + .controls-right :global(.spectrum-Search) { + width: 200px; + } diff --git a/packages/frontend-core/src/api/groups.js b/packages/frontend-core/src/api/groups.js index cbc5bfd72a..72cbe30718 100644 --- a/packages/frontend-core/src/api/groups.js +++ b/packages/frontend-core/src/api/groups.js @@ -55,10 +55,13 @@ export const buildGroupsEndpoints = API => { /** * Gets a group users by the group id */ - getGroupUsers: async ({ id, bookmark }) => { + getGroupUsers: async ({ id, bookmark, emailSearch }) => { let url = `/api/global/groups/${id}/users?` if (bookmark) { - url += `bookmark=${bookmark}` + url += `bookmark=${bookmark}&` + } + if (emailSearch) { + url += `emailSearch=${emailSearch}&` } return await API.get({ diff --git a/packages/frontend-core/src/fetch/GroupUserFetch.js b/packages/frontend-core/src/fetch/GroupUserFetch.js index b0ca9a5388..bd2cf264c5 100644 --- a/packages/frontend-core/src/fetch/GroupUserFetch.js +++ b/packages/frontend-core/src/fetch/GroupUserFetch.js @@ -31,6 +31,7 @@ export default class GroupUserFetch extends DataFetch { try { const res = await this.API.getGroupUsers({ id: query.groupId, + emailSearch: query.emailSearch, bookmark: cursor, }) diff --git a/packages/pro b/packages/pro index 14345384f7..64a2025727 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 14345384f7a6755d1e2de327104741e0f208f55d +Subproject commit 64a2025727c25d5813832c92eb360de3947b7aa6 diff --git a/packages/worker/src/api/routes/global/tests/groups.spec.ts b/packages/worker/src/api/routes/global/tests/groups.spec.ts index 5b7fb9db9e..afeaae952c 100644 --- a/packages/worker/src/api/routes/global/tests/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/groups.spec.ts @@ -1,5 +1,9 @@ import { events } from "@budibase/backend-core" +import { generator } from "@budibase/backend-core/tests" import { structures, TestConfiguration, mocks } from "../../../../tests" +import { UserGroup } from "@budibase/types" + +mocks.licenses.useGroups() describe("/api/global/groups", () => { const config = new TestConfiguration() @@ -113,4 +117,118 @@ describe("/api/global/groups", () => { }) }) }) + + describe("find users", () => { + describe("without users", () => { + let group: UserGroup + beforeAll(async () => { + group = structures.groups.UserGroup() + await config.api.groups.saveGroup(group) + }) + + it("should return empty", async () => { + const result = await config.api.groups.searchUsers(group._id!) + expect(result.body).toEqual({ + users: [], + bookmark: undefined, + hasNextPage: false, + }) + }) + }) + + describe("existing users", () => { + let groupId: string + let users: { _id: string; email: string }[] = [] + + beforeAll(async () => { + groupId = ( + await config.api.groups.saveGroup(structures.groups.UserGroup()) + ).body._id + + await Promise.all( + Array.from({ length: 30 }).map(async (_, i) => { + const email = `user${i}@${generator.domain()}` + const user = await config.api.users.saveUser({ + ...structures.users.user(), + email, + }) + users.push({ _id: user.body._id, email }) + }) + ) + users = users.sort((a, b) => a._id.localeCompare(b._id)) + await config.api.groups.updateGroupUsers(groupId, { + add: users.map(u => u._id), + remove: [], + }) + }) + + describe("pagination", () => { + it("should return first page", async () => { + const result = await config.api.groups.searchUsers(groupId) + expect(result.body).toEqual({ + users: users.slice(0, 10), + bookmark: users[10]._id, + hasNextPage: true, + }) + }) + + it("given a bookmark, should return skip items", async () => { + const result = await config.api.groups.searchUsers(groupId, { + bookmark: users[7]._id, + }) + expect(result.body).toEqual({ + users: users.slice(7, 17), + bookmark: users[17]._id, + hasNextPage: true, + }) + }) + + it("bookmarking the last page, should return last page info", async () => { + const result = await config.api.groups.searchUsers(groupId, { + bookmark: users[20]._id, + }) + expect(result.body).toEqual({ + users: users.slice(20), + bookmark: undefined, + hasNextPage: false, + }) + }) + }) + + describe("search by email", () => { + it('should be able to search "starting" by email', async () => { + const result = await config.api.groups.searchUsers(groupId, { + emailSearch: `user1`, + }) + + const matchedUsers = users + .filter(u => u.email.startsWith("user1")) + .sort((a, b) => a.email.localeCompare(b.email)) + + expect(result.body).toEqual({ + users: matchedUsers.slice(0, 10), + bookmark: matchedUsers[10].email, + hasNextPage: true, + }) + }) + + it("should be able to bookmark when searching by email", async () => { + const matchedUsers = users + .filter(u => u.email.startsWith("user1")) + .sort((a, b) => a.email.localeCompare(b.email)) + + const result = await config.api.groups.searchUsers(groupId, { + emailSearch: `user1`, + bookmark: matchedUsers[4].email, + }) + + expect(result.body).toEqual({ + users: matchedUsers.slice(4), + bookmark: undefined, + hasNextPage: false, + }) + }) + }) + }) + }) }) diff --git a/packages/worker/src/tests/api/groups.ts b/packages/worker/src/tests/api/groups.ts index 5524d2a811..91f7c92c7d 100644 --- a/packages/worker/src/tests/api/groups.ts +++ b/packages/worker/src/tests/api/groups.ts @@ -23,4 +23,34 @@ export class GroupsAPI extends TestAPI { .expect("Content-Type", /json/) .expect(200) } + + searchUsers = ( + id: string, + params?: { bookmark?: string; emailSearch?: string } + ) => { + let url = `/api/global/groups/${id}/users?` + if (params?.bookmark) { + url += `bookmark=${params.bookmark}&` + } + if (params?.emailSearch) { + url += `emailSearch=${params.emailSearch}&` + } + return this.request + .get(url) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } + + updateGroupUsers = ( + id: string, + body: { add: string[]; remove: string[] } + ) => { + return this.request + .post(`/api/global/groups/${id}/users`) + .send(body) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } }