Merge pull request #10533 from Budibase/budi-6158/allow_searching_users

Allow searching users on group views
This commit is contained in:
Adria Navarro 2023-05-15 13:45:51 +02:00 committed by GitHub
commit a6455b4b4d
7 changed files with 183 additions and 10 deletions

View File

@ -21,7 +21,7 @@ export enum ViewName {
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email", ACCOUNT_BY_EMAIL = "account_by_email",
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
USER_BY_GROUP = "by_group_user", USER_BY_GROUP = "user_by_group",
APP_BACKUP_BY_TRIGGER = "by_trigger", APP_BACKUP_BY_TRIGGER = "by_trigger",
} }

View File

@ -1,7 +1,7 @@
<script> <script>
import EditUserPicker from "./EditUserPicker.svelte" import EditUserPicker from "./EditUserPicker.svelte"
import { Heading, Pagination, Table } from "@budibase/bbui" import { Heading, Pagination, Table, Search } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { API } from "api" import { API } from "api"
@ -12,7 +12,9 @@
export let groupId export let groupId
const fetchGroupUsers = fetchData({ let emailSearch
let fetchGroupUsers
$: fetchGroupUsers = fetchData({
API, API,
datasource: { datasource: {
type: "groupUser", type: "groupUser",
@ -20,6 +22,7 @@
options: { options: {
query: { query: {
groupId, groupId,
emailSearch,
}, },
}, },
}) })
@ -59,24 +62,31 @@
</script> </script>
<div class="header"> <div class="header">
<Heading size="S">Users</Heading>
{#if !scimEnabled} {#if !scimEnabled}
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} /> <EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
{:else} {:else}
<ScimBanner /> <ScimBanner />
{/if} {/if}
</div>
<div class="controls-right">
<Search bind:value={emailSearch} placeholder="Search email" />
</div>
</div>
<Table <Table
schema={userSchema} schema={userSchema}
data={$fetchGroupUsers?.rows} data={$fetchGroupUsers?.rows}
loading={$fetchGroupUsers.loading}
allowEditRows={false} allowEditRows={false}
customPlaceholder customPlaceholder
customRenderers={customUserTableRenderers} customRenderers={customUserTableRenderers}
on:click={e => $goto(`../users/${e.detail._id}`)} on:click={e => $goto(`../users/${e.detail._id}`)}
> >
<div class="placeholder" slot="placeholder"> <div class="placeholder" slot="placeholder">
<Heading size="S">This user group doesn't have any users</Heading> <Heading size="S"
>{emailSearch
? `No users found matching the email "${emailSearch}"`
: "This user group doesn't have any users"}</Heading
>
</div> </div>
</Table> </Table>
@ -98,7 +108,7 @@
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: space-between;
align-items: center; align-items: center;
gap: var(--spacing-l); gap: var(--spacing-l);
} }
@ -109,4 +119,15 @@
width: 100%; width: 100%;
text-align: center; 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;
}
</style> </style>

View File

@ -55,10 +55,13 @@ export const buildGroupsEndpoints = API => {
/** /**
* Gets a group users by the group id * Gets a group users by the group id
*/ */
getGroupUsers: async ({ id, bookmark }) => { getGroupUsers: async ({ id, bookmark, emailSearch }) => {
let url = `/api/global/groups/${id}/users?` let url = `/api/global/groups/${id}/users?`
if (bookmark) { if (bookmark) {
url += `bookmark=${bookmark}` url += `bookmark=${bookmark}&`
}
if (emailSearch) {
url += `emailSearch=${emailSearch}&`
} }
return await API.get({ return await API.get({

View File

@ -31,6 +31,7 @@ export default class GroupUserFetch extends DataFetch {
try { try {
const res = await this.API.getGroupUsers({ const res = await this.API.getGroupUsers({
id: query.groupId, id: query.groupId,
emailSearch: query.emailSearch,
bookmark: cursor, bookmark: cursor,
}) })

@ -1 +1 @@
Subproject commit 14345384f7a6755d1e2de327104741e0f208f55d Subproject commit 64a2025727c25d5813832c92eb360de3947b7aa6

View File

@ -1,5 +1,9 @@
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { generator } from "@budibase/backend-core/tests"
import { structures, TestConfiguration, mocks } from "../../../../tests" import { structures, TestConfiguration, mocks } from "../../../../tests"
import { UserGroup } from "@budibase/types"
mocks.licenses.useGroups()
describe("/api/global/groups", () => { describe("/api/global/groups", () => {
const config = new TestConfiguration() 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,
})
})
})
})
})
}) })

View File

@ -23,4 +23,34 @@ export class GroupsAPI extends TestAPI {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .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)
}
} }