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 @@
+
+
+
+
$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)
+ }
}