diff --git a/packages/hoppscotch-backend/src/admin/admin.service.ts b/packages/hoppscotch-backend/src/admin/admin.service.ts index d8192dcd..40330c25 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.ts @@ -220,12 +220,30 @@ export class AdminService { * @param cursorID team id * @param take number of items to fetch * @returns an array of teams + * @deprecated use fetchAllTeamsV2 instead */ async fetchAllTeams(cursorID: string, take: number) { const allTeams = await this.teamService.fetchAllTeams(cursorID, take); return allTeams; } + /** + * Fetch all the teams in the infra. + * @param searchString search on team name or ID + * @param paginationOption pagination options + * @returns an array of teams + */ + async fetchAllTeamsV2( + searchString: string, + paginationOption: OffsetPaginationArgs, + ) { + const allTeams = await this.teamService.fetchAllTeamsV2( + searchString, + paginationOption, + ); + return allTeams; + } + /** * Fetch the count of all the members in a team. * @param teamID team id diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index c3ce53e4..b822fb13 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -120,12 +120,32 @@ export class InfraResolver { @ResolveField(() => [Team], { description: 'Returns a list of all the teams in the infra', + deprecationReason: 'Use allTeamsV2 instead', }) async allTeams(@Args() args: PaginationArgs): Promise { const teams = await this.adminService.fetchAllTeams(args.cursor, args.take); return teams; } + @ResolveField(() => [Team], { + description: 'Returns a list of all the teams in the infra', + }) + async allTeamsV2( + @Args({ + name: 'searchString', + nullable: true, + description: 'Search on team name or ID', + }) + searchString: string, + @Args() paginationOption: OffsetPaginationArgs, + ): Promise { + const teams = await this.adminService.fetchAllTeamsV2( + searchString, + paginationOption, + ); + return teams; + } + @ResolveField(() => Team, { description: 'Returns a team info by ID when requested by Admin', }) diff --git a/packages/hoppscotch-backend/src/team/team.service.spec.ts b/packages/hoppscotch-backend/src/team/team.service.spec.ts index 6126405e..0ab92145 100644 --- a/packages/hoppscotch-backend/src/team/team.service.spec.ts +++ b/packages/hoppscotch-backend/src/team/team.service.spec.ts @@ -962,6 +962,86 @@ describe('fetchAllTeams', () => { }); }); +describe('fetchAllTeamsV2', () => { + test('should return teams with offset pagination when no search string', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce(teams); + + const result = await teamService.fetchAllTeamsV2('', { + skip: 0, + take: 20, + }); + + expect(result).toEqual(teams); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should search by name and id when search string is provided', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce([teams[0]]); + + const result = await teamService.fetchAllTeamsV2('team', { + skip: 0, + take: 20, + }); + + expect(result).toEqual([teams[0]]); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: { + OR: [ + { name: { contains: 'team', mode: 'insensitive' } }, + { id: { contains: 'team', mode: 'insensitive' } }, + ], + }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should apply skip for pagination', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce(teams); + + const result = await teamService.fetchAllTeamsV2('', { + skip: 20, + take: 20, + }); + + expect(result).toEqual(teams); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 20, + take: 20, + where: undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should return empty array when no teams match', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce([]); + + const result = await teamService.fetchAllTeamsV2('nonexistent', { + skip: 0, + take: 20, + }); + + expect(result).toEqual([]); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: { + OR: [ + { name: { contains: 'nonexistent', mode: 'insensitive' } }, + { id: { contains: 'nonexistent', mode: 'insensitive' } }, + ], + }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); +}); + describe('getCountOfMembersInTeam', () => { test('should resolve right and return a total team member count ', async () => { mockPrisma.teamMember.count.mockResolvedValueOnce(2); diff --git a/packages/hoppscotch-backend/src/team/team.service.ts b/packages/hoppscotch-backend/src/team/team.service.ts index bad4bd1a..225c5fe5 100644 --- a/packages/hoppscotch-backend/src/team/team.service.ts +++ b/packages/hoppscotch-backend/src/team/team.service.ts @@ -23,6 +23,7 @@ import * as T from 'fp-ts/Task'; import * as A from 'fp-ts/Array'; import { isValidLength, throwErr } from 'src/utils'; import { AuthUser } from '../types/AuthUser'; +import { OffsetPaginationArgs } from 'src/types/input-types.args'; @Injectable() export class TeamService implements UserDataHandler, OnModuleInit { @@ -522,6 +523,7 @@ export class TeamService implements UserDataHandler, OnModuleInit { * @param cursorID string of teamID or undefined * @param take number of items to query * @returns an array of `Team` object + * @deprecated use fetchAllTeamsV2 instead */ async fetchAllTeams(cursorID: string, take: number) { const options = { @@ -534,6 +536,32 @@ export class TeamService implements UserDataHandler, OnModuleInit { return fetchedTeams; } + /** + * Fetch all the teams in the `Team` table with offset pagination and search + * @param searchString search on team name or ID + * @param paginationOption pagination options + * @returns an array of `Team` object + */ + async fetchAllTeamsV2( + searchString: string, + paginationOption: OffsetPaginationArgs, + ) { + const fetchedTeams = await this.prisma.team.findMany({ + skip: paginationOption.skip, + take: paginationOption.take, + where: searchString + ? { + OR: [ + { name: { contains: searchString, mode: 'insensitive' } }, + { id: { contains: searchString, mode: 'insensitive' } }, + ], + } + : undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + return fetchedTeams; + } + /** * Fetch list of all the Teams in the DB * diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index 7e185125..88cf1337 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -404,6 +404,7 @@ "name": "Workspace Name", "no_members": "No members in this workspace. Add members to this workspace to collaborate", "no_pending_invites": "No pending invites", + "no_search_results": "No workspaces found matching your search", "no_teams": "No workspaces found..", "pending_invites": "Pending invites", "roles": "Roles", @@ -412,6 +413,7 @@ "rename": "Rename", "save": "Save", "save_changes": "Save Changes", + "search_placeholder": "Search by workspace name or ID..", "send_invite": "Send Invite", "show_more": "Show more", "team_details": "Workspace details", diff --git a/packages/hoppscotch-sh-admin/src/components.d.ts b/packages/hoppscotch-sh-admin/src/components.d.ts index 999b779a..27a89ad6 100644 --- a/packages/hoppscotch-sh-admin/src/components.d.ts +++ b/packages/hoppscotch-sh-admin/src/components.d.ts @@ -1,8 +1,11 @@ /* eslint-disable */ // @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 -// biome-ignore lint: disable + export {} /* prettier-ignore */ diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamList.graphql b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamList.graphql deleted file mode 100644 index 7560c5d6..00000000 --- a/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamList.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query TeamList($cursor: ID, $take: Int) { - infra { - allTeams(cursor: $cursor, take: $take) { - id - name - teamMembers { - membershipID - } - } - } -} diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamListV2.graphql b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamListV2.graphql new file mode 100644 index 00000000..999084bb --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamListV2.graphql @@ -0,0 +1,11 @@ +query TeamListV2($searchString: String, $skip: Int, $take: Int) { + infra { + allTeamsV2(searchString: $searchString, skip: $skip, take: $take) { + id + name + teamMembers { + membershipID + } + } + } +} diff --git a/packages/hoppscotch-sh-admin/src/pages/teams/index.vue b/packages/hoppscotch-sh-admin/src/pages/teams/index.vue index 7947712a..ca3da6bf 100644 --- a/packages/hoppscotch-sh-admin/src/pages/teams/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/teams/index.vue @@ -1,29 +1,71 @@