diff --git a/src/services/repos/index.js b/src/services/repos/index.js index 0052098845..d541d18530 100644 --- a/src/services/repos/index.js +++ b/src/services/repos/index.js @@ -1,2 +1,3 @@ export * from './useRepos' export * from './config' +export * from './useReposTeam' diff --git a/src/services/repos/useReposTeam.spec.tsx b/src/services/repos/useReposTeam.spec.tsx new file mode 100644 index 0000000000..281f7531ab --- /dev/null +++ b/src/services/repos/useReposTeam.spec.tsx @@ -0,0 +1,173 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { graphql } from 'msw' +import { setupServer } from 'msw/node' +import { MemoryRouter, Route } from 'react-router-dom' + +import { useReposTeam } from './useReposTeam' + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) +const wrapper: React.FC = ({ children }) => ( + + + {children} + + +) + +const repo1 = { + name: 'codecov-bash', + active: true, + activated: true, + private: false, + latestCommitAt: '2021-04-22T14:09:39.826948+00:00', + lines: 99, + author: { + username: 'codecov', + }, +} + +const repo2 = { + name: 'codecov-bash-2', + active: true, + activated: true, + private: false, + latestCommitAt: '2021-04-22T14:09:39.826948+00:00', + lines: 99, + author: { + username: 'codecov', + }, +} + +const repo3 = { + name: 'codecov-bash-3', + active: true, + activated: true, + private: false, + latestCommitAt: '2021-04-22T14:09:39.826948+00:00', + lines: 99, + author: { + username: 'codecov', + }, +} + +const repo4 = { + name: 'codecov-bash-4', + active: true, + activated: true, + private: false, + latestCommitAt: '2021-04-22T14:09:39.826948+00:00', + lines: 99, + author: { + username: 'codecov', + }, +} + +const server = setupServer() + +beforeAll(() => server.listen()) +beforeEach(() => { + server.resetHandlers() + queryClient.clear() +}) +afterAll(() => server.close()) + +describe('useReposTeam', () => { + function setup() { + server.use( + graphql.query('GetReposTeam', (req, res, ctx) => { + const data = { + owner: { + repositories: { + edges: req.variables.after + ? [ + { + node: repo3, + }, + { + node: repo4, + }, + ] + : [ + { + node: repo1, + }, + { + node: repo2, + }, + ], + pageInfo: { + hasNextPage: req.variables.after ? false : true, + endCursor: req.variables.after + ? 'aa' + : 'MjAyMC0wOC0xMSAxNzozMDowMiswMDowMHwxMDA=', + }, + }, + }, + } + return res(ctx.status(200), ctx.data(data)) + }) + ) + } + + describe('when called', () => { + beforeEach(() => { + setup() + }) + + it('returns repositories', async () => { + const { result } = renderHook( + () => + useReposTeam({ + activated: true, + owner: 'codecov', + }), + { + wrapper, + } + ) + + await waitFor(() => + expect(result.current.data).toEqual({ + repos: [repo1, repo2], + }) + ) + }) + }) + + describe('when call next page', () => { + beforeEach(async () => { + setup() + }) + + it('returns repositories of the user', async () => { + const { result } = renderHook( + () => + useReposTeam({ + owner: 'codecov', + activated: true, + first: 2, + }), + { + wrapper, + } + ) + + await waitFor(() => result.current.isFetching) + await waitFor(() => !result.current.isFetching) + + result.current.fetchNextPage() + + await waitFor(() => result.current.isFetching) + await waitFor(() => !result.current.isFetching) + + await waitFor(() => + expect(result.current.data).toEqual({ + repos: [repo1, repo2, repo3, repo4], + }) + ) + }) + }) +}) diff --git a/src/services/repos/useReposTeam.tsx b/src/services/repos/useReposTeam.tsx new file mode 100644 index 0000000000..a1c0326d25 --- /dev/null +++ b/src/services/repos/useReposTeam.tsx @@ -0,0 +1,177 @@ +import { useInfiniteQuery } from '@tanstack/react-query' +import { useParams } from 'react-router-dom' +import { z } from 'zod' + +import Api from 'shared/api' +import { mapEdges } from 'shared/utils/graphql' + +import { orderingOptions } from './config' + +const RepositorySchema = z.object({ + name: z.string(), + active: z.boolean(), + activated: z.boolean(), + private: z.boolean(), + latestCommitAt: z.string().nullable(), + lines: z.number().nullable(), + author: z.object({ + username: z.string().nullable(), + }), +}) + +const repositoryFragment = ` + fragment RepoForList on Repository { + name + active + activated + private + latestCommitAt + lines + author { + username + } + } +` + +interface FetchReposTeamArgs { + provider: string + owner: string + variables: { + filters: { + activated: boolean + term?: string + repoNames?: string[] + } + ordering?: string + direction?: string + first?: number + } + after?: string + signal?: AbortSignal +} + +const RequestSchema = z.object({ + owner: z + .object({ + repositories: z + .object({ + edges: z.array( + z.object({ + node: RepositorySchema, + }) + ), + pageInfo: z.object({ + hasNextPage: z.boolean(), + endCursor: z.string().nullable(), + }), + }) + .nullable(), + }) + .nullable(), +}) + +function fetchReposForOwner({ + provider, + owner, + variables, + after, + signal, +}: FetchReposTeamArgs) { + const query = ` + query GetReposTeam($filters: RepositorySetFilters!, $owner: String!, $ordering: RepositoryOrdering!, $direction: OrderingDirection!, $after: String, $first: Int) { + owner(username: $owner) { + repositories(filters: $filters, ordering: $ordering, orderingDirection: $direction, first: $first, after: $after) { + edges { + node { + ...RepoForList + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + ${repositoryFragment} + ` + + return Api.graphql({ + provider, + query, + signal, + variables: { + ...variables, + owner, + after, + }, + }).then((res) => { + const parsedRes = RequestSchema.safeParse(res?.data) + + if (!parsedRes.success) { + return Promise.reject({ + status: 404, + data: null, + }) + } + + const owner = parsedRes.data?.owner + + return { + repos: mapEdges(owner?.repositories), + pageInfo: owner?.repositories?.pageInfo, + } + }) +} + +interface UseReposTeamArgs { + activated: boolean + term?: string + owner: string + sortItem?: { + ordering: string + direction: string + } + first?: number + repoNames?: string[] +} + +export function useReposTeam({ + activated, + term, + owner, + sortItem = orderingOptions[0], + first = 20, + repoNames, + ...options +}: UseReposTeamArgs) { + const { provider } = useParams<{ provider: string }>() + const variables = { + filters: { activated, term, repoNames }, + ordering: sortItem?.ordering, + direction: sortItem?.direction, + first, + } + + const { data, ...rest } = useInfiniteQuery( + ['GetReposTeam', provider, variables, owner], + ({ pageParam, signal }) => { + return fetchReposForOwner({ + provider, + variables, + owner, + after: pageParam, + signal, + }) + }, + { + getNextPageParam: (data) => + data?.pageInfo?.hasNextPage ? data.pageInfo.endCursor : undefined, + ...options, + } + ) + return { + data: { repos: data?.pages.map((page) => page.repos).flat() }, + ...rest, + } +}