diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.jsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.jsx deleted file mode 100644 index 92c69cd796..0000000000 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import Table from 'old_ui/Table' - -import { useRepoBranchContentsTable } from '../hooks' -import { Loader, RepoContentsResult } from '../shared' - -function CodeTreeTable() { - const { - data, - headers, - handleSort, - isSearching, - isMissingHeadReport, - isLoading, - hasFlagsSelected, - hasComponentsSelected, - pathContentsType, - } = useRepoBranchContentsTable() - - if (pathContentsType === 'UnknownPath') { - return ( -

- Unknown filepath. Please ensure that files/directories exist and are not - empty. -

- ) - } - - if (pathContentsType === 'MissingCoverage') { - return

No coverage data available.

- } - - return ( -
- - - {data?.length === 0 && !isLoading ? ( - - ) : null} - - ) -} - -export default CodeTreeTable diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.jsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.tsx similarity index 83% rename from src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.jsx rename to src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.tsx index e4cc641610..55fa1671bd 100644 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.jsx +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.spec.tsx @@ -60,7 +60,7 @@ const mockUnknownPath = { } const mockTreeData = { - username: 'nicholas-codecov', + username: 'codecov-tree', repository: { branch: { head: { @@ -76,6 +76,21 @@ const mockTreeData = { path: 'src', percentCovered: 100.0, }, + ], + __typename: 'PathContents', + }, + }, + }, + }, +} + +const mockTreeDataNested = { + username: 'codecov-tree', + repository: { + branch: { + head: { + pathContents: { + results: [ { __typename: 'PathContentFile', hits: 9, @@ -85,6 +100,7 @@ const mockTreeData = { name: 'file.js', path: 'a/b/c/file.js', percentCovered: 100.0, + isCriticalFile: false, }, ], __typename: 'PathContents', @@ -100,9 +116,8 @@ const mockNoHeadReport = { branch: { head: { pathContents: { - results: [], + __typename: 'MissingHeadReport', }, - __typename: 'MissingHeadReport', }, }, }, @@ -123,12 +138,14 @@ const mockOverview = { } const wrapper = - (initialEntries = '/gh/codecov/cool-repo/tree/main/a/b/c') => + ( + initialEntries = '/gh/codecov/cool-repo/tree/main/' + ): React.FC => ({ children }) => { return ( - + {children} @@ -150,21 +167,14 @@ afterAll(() => { }) describe('CodeTreeTable', () => { - function setup( - { - noFiles = false, - noHeadReport = false, - noFlagCoverage = false, - missingCoverage = false, - unknownPath = false, - } = { - noFiles: false, - noHeadReport: false, - noFlagCoverage: false, - missingCoverage: false, - unknownPath: false, - } - ) { + function setup({ + noFiles = false, + noHeadReport = false, + noFlagCoverage = false, + missingCoverage = false, + unknownPath = false, + isNestedTreeData = false, + }) { const user = userEvent.setup() const requestFilters = jest.fn() @@ -194,6 +204,10 @@ describe('CodeTreeTable', () => { return res(ctx.status(200), ctx.data({ owner: mockNoFiles })) } + if (isNestedTreeData) { + return res(ctx.status(200), ctx.data({ owner: mockTreeDataNested })) + } + return res(ctx.status(200), ctx.data({ owner: mockTreeData })) }), graphql.query('GetRepoOverview', (req, res, ctx) => { @@ -207,7 +221,7 @@ describe('CodeTreeTable', () => { describe('rendering table', () => { describe('displaying the table head', () => { it('has a files column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const files = await screen.findByText('Files') @@ -215,7 +229,7 @@ describe('CodeTreeTable', () => { }) it('has a tracked lines column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const trackedLines = await screen.findByText('Tracked lines') @@ -223,7 +237,7 @@ describe('CodeTreeTable', () => { }) it('has a covered column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const covered = await screen.findByText('Covered') @@ -231,7 +245,7 @@ describe('CodeTreeTable', () => { }) it('has a partial column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const partial = await screen.findByText('Partial') @@ -239,7 +253,7 @@ describe('CodeTreeTable', () => { }) it('has a missed column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const missed = await screen.findByText('Missed') @@ -247,7 +261,7 @@ describe('CodeTreeTable', () => { }) it('has a coverage column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const coverage = await screen.findByText('Coverage %') @@ -258,7 +272,8 @@ describe('CodeTreeTable', () => { describe('table is displaying file tree', () => { describe('default sort is set', () => { it('sets default sort to name asc', async () => { - const { requestFilters } = setup() + const { requestFilters } = setup({}) + render(, { wrapper: wrapper() }) await waitFor(() => @@ -278,7 +293,7 @@ describe('CodeTreeTable', () => { describe('displaying a directory', () => { it('has the correct url', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('src')).toBeTruthy() @@ -287,18 +302,19 @@ describe('CodeTreeTable', () => { const table = await screen.findByRole('table') const links = await within(table).findAllByRole('link') - - expect(links[1]).toHaveAttribute( + expect(links[0]).toHaveAttribute( 'href', - '/gh/codecov/cool-repo/tree/main/a%2Fb%2Fc%2Fsrc' + '/gh/codecov/cool-repo/tree/main/src' ) }) }) describe('displaying a file', () => { it('has the correct url', async () => { - setup() - render(, { wrapper: wrapper() }) + setup({ isNestedTreeData: true }) + render(, { + wrapper: wrapper('/gh/codecov/cool-repo/tree/main/a/b/c/'), + }) expect(await screen.findByText('file.js')).toBeTruthy() const file = screen.getByText('file.js') @@ -306,8 +322,7 @@ describe('CodeTreeTable', () => { const table = await screen.findByRole('table') const links = await within(table).findAllByRole('link') - - expect(links[2]).toHaveAttribute( + expect(links[1]).toHaveAttribute( 'href', '/gh/codecov/cool-repo/blob/main/a%2Fb%2Fc%2Ffile.js' ) @@ -376,7 +391,7 @@ describe('CodeTreeTable', () => { setup({ noFlagCoverage: true }) render(, { wrapper: wrapper( - `/gh/codecov/cool-repo/tree/main/a/b/c${qs.stringify( + `/gh/codecov/cool-repo/tree/main/${qs.stringify( { flags: ['flag-1'] }, { addQueryPrefix: true } )}` @@ -395,7 +410,7 @@ describe('CodeTreeTable', () => { describe('sorting on head column', () => { describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) @@ -415,7 +430,7 @@ describe('CodeTreeTable', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Files')).toBeTruthy() @@ -438,9 +453,9 @@ describe('CodeTreeTable', () => { }) describe('sorting on tracked lines column', () => { - describe('sorting in asc order', () => { + describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Tracked lines')).toBeTruthy() @@ -450,16 +465,16 @@ describe('CodeTreeTable', () => { await waitFor(() => expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'ASC', parameter: 'LINES' }, + ordering: { direction: 'DESC', parameter: 'LINES' }, }) ) ) }) }) - describe('sorting in desc order', () => { + describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Tracked lines')).toBeTruthy() @@ -473,7 +488,7 @@ describe('CodeTreeTable', () => { await waitFor(() => { expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'DESC', parameter: 'LINES' }, + ordering: { direction: 'ASC', parameter: 'LINES' }, }) ) }) @@ -482,9 +497,9 @@ describe('CodeTreeTable', () => { }) describe('sorting on the covered column', () => { - describe('sorting in asc order', () => { + describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Covered')).toBeTruthy() @@ -494,16 +509,16 @@ describe('CodeTreeTable', () => { await waitFor(() => expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'ASC', parameter: 'HITS' }, + ordering: { direction: 'DESC', parameter: 'HITS' }, }) ) ) }) }) - describe('sorting in desc order', () => { + describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Covered')).toBeTruthy() @@ -517,7 +532,7 @@ describe('CodeTreeTable', () => { await waitFor(() => { expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'DESC', parameter: 'HITS' }, + ordering: { direction: 'ASC', parameter: 'HITS' }, }) ) }) @@ -526,9 +541,9 @@ describe('CodeTreeTable', () => { }) describe('sorting on the partial column', () => { - describe('sorting in asc order', () => { + describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Partial')).toBeTruthy() @@ -538,16 +553,16 @@ describe('CodeTreeTable', () => { await waitFor(() => expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'ASC', parameter: 'PARTIALS' }, + ordering: { direction: 'DESC', parameter: 'PARTIALS' }, }) ) ) }) }) - describe('sorting in desc order', () => { + describe('sorting in ASC order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Partial')).toBeTruthy() @@ -561,7 +576,7 @@ describe('CodeTreeTable', () => { await waitFor(() => { expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'DESC', parameter: 'PARTIALS' }, + ordering: { direction: 'ASC', parameter: 'PARTIALS' }, }) ) }) @@ -569,10 +584,10 @@ describe('CodeTreeTable', () => { }) }) - describe('sorting on the coverage line', () => { - describe('sorting in asc order', () => { + describe('sorting on the misses line', () => { + describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Missed')).toBeTruthy() @@ -581,29 +596,65 @@ describe('CodeTreeTable', () => { expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'ASC', parameter: 'MISSES' }, + ordering: { direction: 'DESC', parameter: 'MISSES' }, }) ) }) }) - describe('sorting in desc order', () => { + describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) expect(await screen.findByText('Missed')).toBeTruthy() let missed = screen.getByText('Missed') await user.click(missed) - - expect(await screen.findByText('Missed')).toBeTruthy() - missed = screen.getByText('Missed') await user.click(missed) await waitFor(() => { expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ - ordering: { direction: 'DESC', parameter: 'MISSES' }, + ordering: { direction: 'ASC', parameter: 'MISSES' }, + }) + ) + }) + }) + }) + }) + + describe('sorting on the coverage line', () => { + describe('sorting in desc order', () => { + it('sets the correct api variables', async () => { + const { requestFilters, user } = setup({}) + render(, { wrapper: wrapper() }) + + expect(await screen.findByText('Coverage %')).toBeTruthy() + const coverage = screen.getByText('Coverage %') + await user.click(coverage) + + expect(requestFilters).toHaveBeenCalledWith( + expect.objectContaining({ + ordering: { direction: 'DESC', parameter: 'COVERAGE' }, + }) + ) + }) + }) + + describe('sorting in asc order', () => { + it('sets the correct api variables', async () => { + const { requestFilters, user } = setup({}) + render(, { wrapper: wrapper() }) + + expect(await screen.findByText('Coverage %')).toBeTruthy() + let coverage = screen.getByText('Coverage %') + await user.click(coverage) + await user.click(coverage) + + await waitFor(() => { + expect(requestFilters).toHaveBeenCalledWith( + expect.objectContaining({ + ordering: { direction: 'ASC', parameter: 'COVERAGE' }, }) ) }) diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.tsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.tsx new file mode 100644 index 0000000000..3d2c1b450a --- /dev/null +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/CodeTreeTable/CodeTreeTable.tsx @@ -0,0 +1,217 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' +import cs from 'classnames' +import { useMemo, useState } from 'react' + +import { OrderingDirection } from 'services/repos' +import { Row } from 'shared/ContentsTable/utils' +import Icon from 'ui/Icon' + +import { useRepoBranchContentsTable } from '../hooks' +import { Loader, RepoContentsResult } from '../shared' + +const columnHelper = createColumnHelper() + +function getOrderingDirection(sorting: Array<{ id: string; desc: boolean }>) { + const state = sorting.at(0) + + if (state) { + const direction = state?.desc + ? OrderingDirection.DESC + : OrderingDirection.ASC + let ordering = undefined + if (state.id === 'name') { + ordering = 'NAME' + } + + if (state.id === 'percentCovered') { + ordering = 'COVERAGE' + } + + if (state.id === 'hits') { + ordering = 'HITS' + } + + if (state.id === 'misses') { + ordering = 'MISSES' + } + + if (state.id === 'partials') { + ordering = 'PARTIALS' + } + + if (state.id === 'lines') { + ordering = 'LINES' + } + + return { direction, ordering } + } + + return undefined +} + +export const getBaseColumns = () => { + const baseColumns = [ + columnHelper.accessor('name', { + id: 'name', + header: () => 'Files', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('lines', { + id: 'lines', + header: () => 'Tracked lines', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('hits', { + id: 'hits', + header: () => 'Covered', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('partials', { + id: 'partials', + header: () => 'Partial', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('misses', { + id: 'misses', + header: () => 'Missed', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('coverage', { + id: 'percentCovered', + header: () => 'Coverage %', + cell: ({ renderValue }) => renderValue(), + }), + ] + + return baseColumns +} + +function CodeTreeTable() { + const [sorting, setSorting] = useState([{ id: 'name', desc: false }]) + const ordering = useMemo(() => getOrderingDirection(sorting), [sorting]) + const { + data, + isSearching, + isMissingHeadReport, + isLoading, + hasFlagsSelected, + hasComponentsSelected, + pathContentsType, + } = useRepoBranchContentsTable(ordering) + + const table = useReactTable({ + columns: getBaseColumns(), + getCoreRowModel: getCoreRowModel(), + data: data ?? [], + state: { + sorting, + }, + onSortingChange: setSorting, + manualSorting: true, + }) + + if (pathContentsType === 'UnknownPath') { + return ( +

+ Unknown filepath. Please ensure that files/directories exist and are not + empty. +

+ ) + } + + if (pathContentsType === 'MissingCoverage') { + return

No coverage data available.

+ } + return ( +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + +
+
+ +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ + {data?.length === 0 && !isLoading ? ( + + ) : null} + + ) +} + +export default CodeTreeTable diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileExplorer.jsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileExplorer.jsx index 8eadab31ca..c25ae9a037 100644 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileExplorer.jsx +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileExplorer.jsx @@ -39,7 +39,7 @@ function FileExplorer() { return ( <> -
+
- Unknown filepath. Please ensure that files/directories exist and are not - empty. -

- ) - } - - if (pathContentsType === 'MissingCoverage') { - return

No coverage data available.

- } - - return ( -
- - - {data?.length === 0 && !isLoading ? ( - - ) : null} - - ) -} - -export default FileListTable diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.jsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.tsx similarity index 85% rename from src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.jsx rename to src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.tsx index de93598378..df71923c7a 100644 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.jsx +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.spec.tsx @@ -90,10 +90,9 @@ const mockNoHeadReport = { branch: { head: { pathContents: { - __typename: 'PathContents', + __typename: 'MissingHeadReport', results: [], }, - __typename: 'MissingHeadReport', }, }, }, @@ -114,12 +113,14 @@ const mockOverview = { } const wrapper = - (initialEntries = '/gh/codecov/cool-repo/tree/main/a/b/c') => + ( + initialEntries = '/gh/codecov/cool-repo/tree/main/a/b/c' + ): React.FC => ({ children }) => { return ( - + {children} @@ -139,22 +140,14 @@ afterAll(() => { }) describe('FileListTable', () => { - function setup( - { - noFiles = false, - noHeadReport = false, - noFlagCoverage = false, - missingCoverage = false, - unknownPath = false, - } = { - noFiles: false, - noHeadReport: false, - noFlagCoverage: false, - missingCoverage: false, - unknownPath: false, - } - ) { - const user = userEvent.setup() + function setup({ + noFiles = false, + noHeadReport = false, + noFlagCoverage = false, + missingCoverage = false, + unknownPath = false, + }) { + const user = userEvent.setup({}) const requestFilters = jest.fn() server.use( @@ -196,7 +189,7 @@ describe('FileListTable', () => { describe('rendering table', () => { describe('displaying the table head', () => { it('has a files column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const files = await screen.findByText('Files') @@ -204,7 +197,7 @@ describe('FileListTable', () => { }) it('has a tracked lines column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const trackedLines = await screen.findByText('Tracked lines') @@ -212,7 +205,7 @@ describe('FileListTable', () => { }) it('has a covered column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const covered = await screen.findByText('Covered') @@ -220,7 +213,7 @@ describe('FileListTable', () => { }) it('has a partial column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const partial = await screen.findByText('Partial') @@ -228,7 +221,7 @@ describe('FileListTable', () => { }) it('has a missed column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const missed = await screen.findByText('Missed') @@ -236,7 +229,7 @@ describe('FileListTable', () => { }) it('has a coverage column', async () => { - setup() + setup({}) render(, { wrapper: wrapper() }) const coverage = await screen.findByText('Coverage %') @@ -247,7 +240,7 @@ describe('FileListTable', () => { describe('table is displaying file list', () => { describe('display type is set', () => { it('set to list', async () => { - const { requestFilters } = setup() + const { requestFilters } = setup({}) render(, { wrapper: wrapper( `/gh/codecov/cool-repo/tree/main/a/b/c${qs.stringify( @@ -270,7 +263,7 @@ describe('FileListTable', () => { describe('displaying a file', () => { it('has the correct url', async () => { - setup() + setup({}) render(, { wrapper: wrapper( `/gh/codecov/cool-repo/tree/main/a/b/c${qs.stringify( @@ -294,7 +287,9 @@ describe('FileListTable', () => { describe('there is no results found', () => { it('displays error fetching data message', async () => { setup({ noFiles: true }) - render(, { wrapper: wrapper() }) + render(, { + wrapper: wrapper('/gh/codecov/cool-repo/tree/main/'), + }) const message = await screen.findByText( 'There is no coverage on the default branch for this repository. Use the Branch Context selector above to choose a different branch.' @@ -306,7 +301,9 @@ describe('FileListTable', () => { describe('when head commit has no reports', () => { it('renders no report uploaded message', async () => { setup({ noHeadReport: true }) - render(, { wrapper: wrapper() }) + render(, { + wrapper: wrapper('/gh/codecov/cool-repo/tree/main'), + }) const message = await screen.findByText( 'No coverage report uploaded for this branch head commit' @@ -375,12 +372,13 @@ describe('FileListTable', () => { describe('sorting on head column', () => { describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let files = await screen.findByText('Files') await user.click(files) + await user.click(files) expect(requestFilters).toHaveBeenCalledWith( expect.objectContaining({ @@ -392,7 +390,7 @@ describe('FileListTable', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let files = await screen.findByText('Files') @@ -414,7 +412,7 @@ describe('FileListTable', () => { describe('sorting on tracked lines column', () => { describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let trackedLines = await screen.findByText('Tracked lines') @@ -433,7 +431,7 @@ describe('FileListTable', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let trackedLines = await screen.findByText('Tracked lines') @@ -456,7 +454,7 @@ describe('FileListTable', () => { describe('sorting on the covered column', () => { describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) const covered = await screen.findByText('Covered') @@ -474,7 +472,7 @@ describe('FileListTable', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let covered = await screen.findByText('Covered') @@ -497,7 +495,7 @@ describe('FileListTable', () => { describe('sorting on the partial column', () => { describe('sorting in asc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let partial = await screen.findByText('Partial') @@ -516,7 +514,7 @@ describe('FileListTable', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let partial = await screen.findByText('Partial') @@ -536,16 +534,14 @@ describe('FileListTable', () => { }) }) - describe('sorting on the coverage line', () => { + describe('sorting on the misses line', () => { describe('sorting in desc order', () => { it('sets the correct api variables', async () => { - const { requestFilters, user } = setup() + const { requestFilters, user } = setup({}) render(, { wrapper: wrapper() }) let missed = await screen.findByText('Missed') await user.click(missed) - - missed = await screen.findByText('Missed') await user.click(missed) await waitFor(() => { @@ -558,5 +554,41 @@ describe('FileListTable', () => { }) }) }) + + describe('sorting on the coverage line', () => { + describe('sorting in desc order', () => { + it('sets the correct api variables', async () => { + const { requestFilters, user } = setup({}) + render(, { wrapper: wrapper() }) + + expect(await screen.findByText('Coverage %')).toBeTruthy() + const coverage = screen.getByText('Coverage %') + await user.click(coverage) + await user.click(coverage) + expect(requestFilters).toHaveBeenCalledWith( + expect.objectContaining({ + ordering: { direction: 'DESC', parameter: 'COVERAGE' }, + }) + ) + }) + }) + + describe('sorting in asc order', () => { + it('sets the correct api variables', async () => { + const { requestFilters, user } = setup({}) + render(, { wrapper: wrapper() }) + expect(await screen.findByText('Coverage %')).toBeTruthy() + let coverage = screen.getByText('Coverage %') + await user.click(coverage) + await waitFor(() => { + expect(requestFilters).toHaveBeenCalledWith( + expect.objectContaining({ + ordering: { direction: 'ASC', parameter: 'COVERAGE' }, + }) + ) + }) + }) + }) + }) }) }) diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.tsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.tsx new file mode 100644 index 0000000000..3ac0dbeb45 --- /dev/null +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/FileListTable/FileListTable.tsx @@ -0,0 +1,217 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' +import cs from 'classnames' +import { useMemo, useState } from 'react' + +import { OrderingDirection } from 'services/repos' +import { Row } from 'shared/ContentsTable/utils' +import Icon from 'ui/Icon' + +import { useRepoBranchContentsTable } from '../hooks' +import { Loader, RepoContentsResult } from '../shared' + +const columnHelper = createColumnHelper() + +function getOrderingDirection(sorting: Array<{ id: string; desc: boolean }>) { + const state = sorting.at(0) + + if (state) { + const direction = state?.desc + ? OrderingDirection.DESC + : OrderingDirection.ASC + + let ordering = undefined + if (state.id === 'name') { + ordering = 'NAME' + } + + if (state.id === 'percentCovered') { + ordering = 'COVERAGE' + } + + if (state.id === 'hits') { + ordering = 'HITS' + } + + if (state.id === 'misses') { + ordering = 'MISSES' + } + + if (state.id === 'partials') { + ordering = 'PARTIALS' + } + + if (state.id === 'lines') { + ordering = 'LINES' + } + + return { direction, ordering } + } + + return undefined +} + +export const getBaseColumns = () => { + const baseColumns = [ + columnHelper.accessor('name', { + id: 'name', + header: () => 'Files', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('lines', { + id: 'lines', + header: () => 'Tracked lines', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('hits', { + id: 'hits', + header: () => 'Covered', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('partials', { + id: 'partials', + header: () => 'Partial', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('misses', { + id: 'misses', + header: () => 'Missed', + cell: ({ renderValue }) => renderValue(), + }), + columnHelper.accessor('coverage', { + id: 'percentCovered', + header: () => 'Coverage %', + cell: ({ renderValue }) => renderValue(), + }), + ] + + return baseColumns +} + +function FileListTable() { + const [sorting, setSorting] = useState([{ id: 'misses', desc: true }]) + const ordering = useMemo(() => getOrderingDirection(sorting), [sorting]) + const { + data, + isSearching, + isMissingHeadReport, + isLoading, + hasFlagsSelected, + hasComponentsSelected, + pathContentsType, + } = useRepoBranchContentsTable(ordering) + const table = useReactTable({ + columns: getBaseColumns(), + getCoreRowModel: getCoreRowModel(), + data: data ?? [], + state: { + sorting, + }, + onSortingChange: setSorting, + manualSorting: true, + }) + + if (pathContentsType === 'UnknownPath') { + return ( +

+ Unknown filepath. Please ensure that files/directories exist and are not + empty. +

+ ) + } + + if (pathContentsType === 'MissingCoverage') { + return

No coverage data available.

+ } + return ( +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + +
+
+ +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ + {data?.length === 0 && !isLoading ? ( + + ) : null} +
+ ) +} + +export default FileListTable diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.js b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.js deleted file mode 100644 index ce7d740e69..0000000000 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.js +++ /dev/null @@ -1,253 +0,0 @@ -import isEqual from 'lodash/isEqual' -import { useCallback, useMemo } from 'react' -import { useParams } from 'react-router-dom' - -import { SortingDirection } from 'old_ui/Table/constants' -import { useLocationParams } from 'services/navigation' -import { useRepoBranchContents } from 'services/pathContents/branch/dir' -import { useRepoOverview } from 'services/repo' -import { displayTypeParameter } from 'shared/ContentsTable/constants' -import BranchDirEntry from 'shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry' -import BranchFileEntry from 'shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry' -import { useTableDefaultSort } from 'shared/ContentsTable/useTableDefaultSort' -import { adjustListIfUpDir } from 'shared/ContentsTable/utils' -import { useTreePaths } from 'shared/treePaths' -import { CommitErrorTypes } from 'shared/utils/commit' -import { determineProgressColor } from 'shared/utils/determineProgressColor' -import CoverageProgress from 'ui/CoverageProgress' - -function determineDisplayType({ filters, isSearching }) { - return filters?.displayType === displayTypeParameter.list || isSearching - ? displayTypeParameter.list - : displayTypeParameter.tree -} - -function createTableData({ - tableData, - branch, - urlPath, - isSearching, - filters, - treePaths, - indicationRange, -}) { - if (tableData?.length > 0) { - const displayType = determineDisplayType({ filters, isSearching }) - - const rawTableRows = tableData?.map( - ({ - name, - percentCovered, - __typename, - path, - isCriticalFile, - misses, - partials, - hits, - lines, - }) => ({ - name: - __typename === 'PathContentDir' ? ( - - ) : ( - - ), - lines, - misses, - hits, - partials, - coverage: ( - - ), - }) - ) - - const finalizedTableRows = adjustListIfUpDir({ - treePaths, - displayType, - rawTableRows, - }) - - return finalizedTableRows - } - - return [] -} - -const headers = [ - { - id: 'name', - header: 'Files', - accessorKey: 'name', - cell: (info) => info.getValue(), - width: 'w-2/12 md:w-5/12', - justifyStart: true, - }, - { - id: 'lines', - header: Tracked lines, - accessorKey: 'lines', - cell: (info) => info.getValue(), - width: 'w-2/12 md:w-1/12 justify-end font-lato', - }, - { - id: 'hits', - header: 'Covered', - accessorKey: 'hits', - cell: (info) => info.getValue(), - width: 'w-2/12 md:w-1/12 justify-end font-lato', - }, - { - id: 'partials', - header: 'Partial', - accessorKey: 'partials', - cell: (info) => info.getValue(), - width: 'w-2/12 md:w-1/12 justify-end font-lato', - }, - { - id: 'misses', - header: 'Missed', - accessorKey: 'misses', - cell: (info) => info.getValue(), - width: 'w-2/12 justify-end font-lato', - }, - { - id: 'coverage', - header: 'Coverage %', - accessorKey: 'coverage', - cell: (info) => info.getValue(), - width: 'w-2/12 md:w-3/12', - }, -] - -const defaultQueryParams = { - search: '', - displayType: '', - flags: [], - components: [], -} - -const sortingParameter = Object.freeze({ - name: 'NAME', - coverage: 'COVERAGE', - hits: 'HITS', - misses: 'MISSES', - partials: 'PARTIALS', - lines: 'LINES', -}) - -const getQueryFilters = ({ params, sortBy }) => { - return { - ...(params?.search && { searchValue: params.search }), - // doing a ternary here because it's an array and arrays + && do not go well - ...(params?.flags ? { flags: params.flags } : {}), - ...(params?.components ? { components: params.components } : {}), - ...(params?.displayType && { - displayType: displayTypeParameter[params?.displayType], - }), - ...(sortBy && { - ordering: { - direction: sortBy?.desc ? SortingDirection.DESC : SortingDirection.ASC, - parameter: sortingParameter[sortBy?.id], - }, - }), - } -} - -export function useRepoBranchContentsTable() { - const { - provider, - owner, - repo, - path: pathParam, - branch: branchParam, - } = useParams() - const { params } = useLocationParams(defaultQueryParams) - const { treePaths } = useTreePaths() - const [sortBy, setSortBy] = useTableDefaultSort() - - const { data: repoOverview, isLoadingRepo } = useRepoOverview({ - provider, - repo, - owner, - }) - - const branch = branchParam || repoOverview?.defaultBranch - const filters = getQueryFilters({ params, sortBy: sortBy[0] }) - const urlPath = pathParam || '' - - const { data: branchData, isLoading } = useRepoBranchContents({ - provider, - owner, - repo, - filters, - branch, - path: urlPath, - suspense: false, - }) - - const data = useMemo( - () => - createTableData({ - tableData: branchData?.results, - branch, - urlPath, - isSearching: !!params?.search, - filters, - treePaths, - indicationRange: branchData?.indicationRange, - }), - [ - branchData?.results, - branchData?.indicationRange, - branch, - urlPath, - params?.search, - filters, - treePaths, - ] - ) - - const handleSort = useCallback( - (tableSortBy) => { - if (tableSortBy.length > 0 && !isEqual(sortBy, tableSortBy)) { - setSortBy(tableSortBy) - } - }, - [sortBy, setSortBy] - ) - - return { - data, - headers, - handleSort, - hasFlagsSelected: params?.flags ? params?.flags?.length > 0 : false, - hasComponentsSelected: params?.components - ? params?.components?.length > 0 - : false, - isLoading: isLoadingRepo || isLoading, - isSearching: !!params?.search, - isMissingHeadReport: - branchData?.__typename === CommitErrorTypes.MISSING_HEAD_REPORT, - pathContentsType: branchData?.pathContentsType, - } -} diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.js b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.tsx similarity index 69% rename from src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.js rename to src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.tsx index 03740ea4e9..410a3d0058 100644 --- a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.js +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.spec.tsx @@ -3,12 +3,12 @@ import { renderHook, waitFor } from '@testing-library/react' import { graphql } from 'msw' import { setupServer } from 'msw/node' import qs from 'qs' +import React from 'react' import { MemoryRouter, Route } from 'react-router-dom' -import { act } from 'react-test-renderer' import { useRepoBranchContentsTable } from './useRepoBranchContentsTable' -const mockCommitContentData = { +const mockBranchContentData = { owner: { repository: { repositoryConfig: { @@ -23,17 +23,23 @@ const mockCommitContentData = { __typename: 'PathContents', results: [ { + hits: 9, + misses: 0, + partials: 0, + lines: 10, name: 'src', - filePath: null, - percentCovered: 50.0, - type: 'dir', + path: 'src', + percentCovered: 100.0, __typename: 'PathContentDir', }, { + hits: 9, + misses: 0, + partials: 0, + lines: 10, name: 'file.ts', - filePath: null, - percentCovered: 50.0, - type: 'file', + path: 'src/file.ts', + percentCovered: 100.0, __typename: 'PathContentFile', }, ], @@ -71,7 +77,9 @@ const queryClient = new QueryClient({ const server = setupServer() const wrapper = - (initialEntries = '/gh/test-org/test-repo/tree/main') => + ( + initialEntries = '/gh/test-org/test-repo/tree/main' + ): React.FC => ({ children }) => ( @@ -113,24 +121,24 @@ const mockOverview = { describe('useRepoBranchContentsTable', () => { function setup({ noData } = { noData: false }) { - const calledCommitContents = jest.fn() + const calledBranchContents = jest.fn() server.use( graphql.query('BranchContents', (req, res, ctx) => { - calledCommitContents(req?.variables) + calledBranchContents(req?.variables) if (noData) { return res(ctx.status(200), ctx.data(mockCommitNoContentData)) } - return res(ctx.status(200), ctx.data(mockCommitContentData)) + return res(ctx.status(200), ctx.data(mockBranchContentData)) }), graphql.query('GetRepoOverview', (req, res, ctx) => { return res(ctx.status(200), ctx.data(mockOverview)) }) ) - return { calledCommitContents } + return { calledBranchContents } } describe('calling the hook', () => { @@ -166,18 +174,6 @@ describe('useRepoBranchContentsTable', () => { await waitFor(() => expect(result.current.data.length).toBe(3)) }) }) - - it('sets the correct headers', async () => { - setup() - const { result } = renderHook(() => useRepoBranchContentsTable(), { - wrapper: wrapper(), - }) - - await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) - await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - - expect(result.current.headers.length).toBe(6) - }) }) describe('when there is no data', () => { @@ -197,7 +193,7 @@ describe('useRepoBranchContentsTable', () => { describe('when there is a search param', () => { it('makes a gql request with the search value', async () => { - const { calledCommitContents } = setup() + const { calledBranchContents } = setup() renderHook(() => useRepoBranchContentsTable(), { wrapper: wrapper( `/gh/test-org/test-repo/tree/main${qs.stringify( @@ -210,17 +206,12 @@ describe('useRepoBranchContentsTable', () => { await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - expect(calledCommitContents).toHaveBeenCalled() - expect(calledCommitContents).toHaveBeenCalledWith({ + expect(calledBranchContents).toHaveBeenCalled() + expect(calledBranchContents).toHaveBeenCalledWith({ branch: 'main', filters: { searchValue: 'file.js', - flags: [], - components: [], - ordering: { - direction: 'ASC', - parameter: 'NAME', - }, + displayType: 'LIST', }, name: 'test-org', repo: 'test-repo', @@ -231,7 +222,7 @@ describe('useRepoBranchContentsTable', () => { describe('when called with the list param', () => { it('makes a gql request with the list param', async () => { - const { calledCommitContents } = setup() + const { calledBranchContents } = setup() renderHook(() => useRepoBranchContentsTable(), { wrapper: wrapper( `/gh/test-org/test-repo/tree/main${qs.stringify( @@ -244,17 +235,11 @@ describe('useRepoBranchContentsTable', () => { await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - expect(calledCommitContents).toHaveBeenCalled() - expect(calledCommitContents).toHaveBeenCalledWith({ + expect(calledBranchContents).toHaveBeenCalled() + expect(calledBranchContents).toHaveBeenCalledWith({ branch: 'main', filters: { displayType: 'LIST', - flags: [], - components: [], - ordering: { - direction: 'DESC', - parameter: 'MISSES', - }, }, name: 'test-org', repo: 'test-repo', @@ -265,7 +250,7 @@ describe('useRepoBranchContentsTable', () => { describe('when there is a flags param', () => { it('makes a gql request with the flags param', async () => { - const { calledCommitContents } = setup() + const { calledBranchContents } = setup() renderHook(() => useRepoBranchContentsTable(), { wrapper: wrapper( `/gh/test-org/test-repo/tree/main${qs.stringify( @@ -278,16 +263,12 @@ describe('useRepoBranchContentsTable', () => { await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - expect(calledCommitContents).toHaveBeenCalled() - expect(calledCommitContents).toHaveBeenCalledWith({ + expect(calledBranchContents).toHaveBeenCalled() + expect(calledBranchContents).toHaveBeenCalledWith({ branch: 'main', filters: { flags: ['flag-1'], - components: [], - ordering: { - direction: 'ASC', - parameter: 'NAME', - }, + displayType: 'TREE', }, name: 'test-org', repo: 'test-repo', @@ -298,7 +279,7 @@ describe('useRepoBranchContentsTable', () => { describe('when there is a components param', () => { it('makes a gql request with the components param', async () => { - const { calledCommitContents } = setup() + const { calledBranchContents } = setup() renderHook(() => useRepoBranchContentsTable(), { wrapper: wrapper( `/gh/test-org/test-repo/tree/main${qs.stringify( @@ -311,51 +292,12 @@ describe('useRepoBranchContentsTable', () => { await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - expect(calledCommitContents).toHaveBeenCalled() - expect(calledCommitContents).toHaveBeenCalledWith({ + expect(calledBranchContents).toHaveBeenCalled() + expect(calledBranchContents).toHaveBeenCalledWith({ branch: 'main', filters: { components: ['component-1'], - flags: [], - ordering: { - direction: 'ASC', - parameter: 'NAME', - }, - }, - name: 'test-org', - repo: 'test-repo', - path: '', - }) - }) - }) - - describe('when handleSort is triggered', () => { - it('makes a gql request with the updated params', async () => { - const { calledCommitContents } = setup() - const { result } = renderHook(() => useRepoBranchContentsTable(), { - wrapper: wrapper(), - }) - - await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) - await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - - act(() => { - result.current.handleSort([{ desc: true, id: 'name' }]) - }) - - await waitFor(() => expect(queryClient.isFetching()).toBeGreaterThan(0)) - await waitFor(() => expect(queryClient.isFetching()).toBe(0)) - - expect(calledCommitContents).toHaveBeenCalledTimes(2) - expect(calledCommitContents).toHaveBeenNthCalledWith(2, { - branch: 'main', - filters: { - flags: [], - components: [], - ordering: { - direction: 'DESC', - parameter: 'NAME', - }, + displayType: 'TREE', }, name: 'test-org', repo: 'test-repo', diff --git a/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.tsx b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.tsx new file mode 100644 index 0000000000..84d6f8e18d --- /dev/null +++ b/src/pages/RepoPage/CoverageTab/subroute/FileExplorer/hooks/useRepoBranchContentsTable.tsx @@ -0,0 +1,206 @@ +import qs, { ParsedQs } from 'qs' +import { useMemo } from 'react' +import { useLocation, useParams } from 'react-router-dom' + +import { useLocationParams } from 'services/navigation' +import { + PathContentResultType, + useRepoBranchContents, +} from 'services/pathContents/branch/dir' +import { useRepoOverview } from 'services/repo' +import { displayTypeParameter } from 'shared/ContentsTable/constants' +import BranchDirEntry from 'shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry' +import BranchFileEntry from 'shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry' +import { adjustListIfUpDir } from 'shared/ContentsTable/utils' +import { useTreePaths } from 'shared/treePaths' +import { CommitErrorTypes } from 'shared/utils/commit' +import { determineProgressColor } from 'shared/utils/determineProgressColor' +import CoverageProgress from 'ui/CoverageProgress' + +function determineDisplayType( + displayType?: string | string[] | ParsedQs | ParsedQs[], + isSearching?: boolean +) { + return displayType?.toString().toUpperCase() === displayTypeParameter.list || + isSearching + ? displayTypeParameter.list + : displayTypeParameter.tree +} + +const defaultQueryParams = { + search: '', + displayType: '', + flags: [], + components: [], +} + +interface URLParams { + provider: string + owner: string + repo: string + path: string + branch: string +} + +export function useRepoBranchContentsTable(sortItem?: { + ordering?: string + direction: string +}) { + const { + provider, + owner, + repo, + path: pathParam, + branch: branchParam, + } = useParams() + const { params } = useLocationParams(defaultQueryParams) + const { data: repoOverview } = useRepoOverview({ + provider, + repo, + owner, + }) + + const branch = (branchParam || repoOverview?.defaultBranch) as string + const location = useLocation() + + const queryParams = qs.parse(location.search, { + ignoreQueryPrefix: true, + depth: 1, + }) + + const urlPath = pathParam || '' + // useLocationParams needs to be updated to have full types + // @ts-expect-error + const isSearching = !!params?.search + const selectedDisplayType = determineDisplayType( + queryParams?.displayType, + isSearching + ) + + const filters = useMemo(() => { + return { + ...(queryParams?.flags ? { flags: queryParams.flags } : {}), + ...(queryParams?.components + ? { components: queryParams.components } + : {}), + displayType: selectedDisplayType, + ...(sortItem && { + ordering: { + direction: sortItem?.direction, + parameter: sortItem?.ordering, + }, + }), + ...(queryParams?.search && { searchValue: queryParams.search }), + } + }, [ + queryParams.flags, + queryParams.components, + sortItem, + queryParams.search, + selectedDisplayType, + ]) + + const { data: branchData, isLoading } = useRepoBranchContents({ + provider, + owner, + repo, + filters, + branch, + path: urlPath, + opts: { + suspense: false, + }, + }) + + const indicationRange = branchData?.indicationRange + + const { treePaths } = useTreePaths() + + const finalizedTableRows = useMemo(() => { + const createTableData = (branchData: PathContentResultType[]) => { + const rawTableRows = branchData.map((result) => { + let name + if (result?.__typename === 'PathContentDir') { + name = ( + + ) + } + if (result?.__typename === 'PathContentFile') { + name = ( + + ) + } + const lines = result?.lines + const hits = result?.hits + const partials = result?.partials + const misses = result?.misses + const coverage = ( + + ) + + return { + name, + lines, + hits, + partials, + misses, + coverage, + } + }) + + return adjustListIfUpDir({ + treePaths, + displayType: selectedDisplayType, + rawTableRows, + }) + } + + let rawData = createTableData(branchData?.results ?? []) + return rawData + }, [ + branchData?.results, + branch, + indicationRange, + treePaths, + urlPath, + selectedDisplayType, + ]) + + return { + data: finalizedTableRows ?? [], + indicationRange: branchData?.indicationRange, + // useLocationParams needs to be updated to have full types + // @ts-expect-error + hasFlagsSelected: params?.flags ? params?.flags?.length > 0 : false, + // @ts-expect-error + hasComponentsSelected: params?.components + ? // useLocationParams needs to be updated to have full types + // @ts-expect-error + params?.components?.length > 0 + : false, + isLoading, + branch, + isSearching, + isMissingHeadReport: + branchData?.pathContentsType === CommitErrorTypes.MISSING_HEAD_REPORT, + pathContentsType: branchData?.pathContentsType, + urlPath, + } +} diff --git a/src/services/pathContents/branch/dir/useRepoBranchContents.js b/src/services/pathContents/branch/dir/useRepoBranchContents.js deleted file mode 100644 index f0a026211b..0000000000 --- a/src/services/pathContents/branch/dir/useRepoBranchContents.js +++ /dev/null @@ -1,77 +0,0 @@ -import { useQuery } from '@tanstack/react-query' - -import Api from 'shared/api' - -import { query } from './constants' - -function fetchRepoContents({ - provider, - owner, - repo, - branch, - path, - filters, - signal, -}) { - return Api.graphql({ - provider, - query, - signal, - variables: { - name: owner, - repo, - branch, - path, - filters, - }, - }).then((res) => { - let results - const pathContentsType = - res?.data?.owner?.repository?.branch?.head?.pathContents?.__typename - if (pathContentsType === 'PathContents') { - results = - res?.data?.owner?.repository?.branch?.head?.pathContents?.results - } - return { - results: results ?? null, - pathContentsType, - indicationRange: - res?.data?.owner?.repository?.repositoryConfig?.indicationRange, - __typename: res?.data?.owner?.repository?.branch?.head?.__typename, - } - }) -} - -export function useRepoBranchContents({ - provider, - owner, - repo, - branch, - path, - filters, - ...options -}) { - return useQuery({ - queryKey: [ - 'BranchContents', - provider, - owner, - repo, - branch, - path, - filters, - query, - ], - queryFn: ({ signal }) => - fetchRepoContents({ - provider, - owner, - repo, - branch, - path, - filters, - signal, - }), - ...options, - }) -} diff --git a/src/services/pathContents/branch/dir/useRepoBranchContents.spec.js b/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx similarity index 91% rename from src/services/pathContents/branch/dir/useRepoBranchContents.spec.js rename to src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx index f160d295bd..abdeb0aaf8 100644 --- a/src/services/pathContents/branch/dir/useRepoBranchContents.spec.js +++ b/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx @@ -10,7 +10,7 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) -const wrapper = ({ children }) => ( +const wrapper: React.FC = ({ children }) => ( {children} @@ -42,10 +42,14 @@ const dataReturned = { pathContents: { results: [ { - name: 'flag1', - filePath: null, + __typename: 'PathContentDir', + hits: 9, + misses: 0, + partials: 0, + lines: 10, + name: 'src', + path: 'src', percentCovered: 100.0, - type: 'dir', }, ], __typename: 'PathContents', @@ -157,15 +161,18 @@ describe('useRepoBranchContents', () => { await waitFor(() => result.current.isLoading) await waitFor(() => !result.current.isLoading) await waitFor(() => result.current.isSuccess) - - expect(queryClient.getQueryState().data).toEqual( + expect(result.current.data).toEqual( expect.objectContaining({ results: [ { - name: 'flag1', - filePath: null, + __typename: 'PathContentDir', + hits: 9, + misses: 0, + partials: 0, + lines: 10, + name: 'src', + path: 'src', percentCovered: 100.0, - type: 'dir', }, ], indicationRange: { diff --git a/src/services/pathContents/branch/dir/useRepoBranchContents.tsx b/src/services/pathContents/branch/dir/useRepoBranchContents.tsx new file mode 100644 index 0000000000..69bd4f30a7 --- /dev/null +++ b/src/services/pathContents/branch/dir/useRepoBranchContents.tsx @@ -0,0 +1,182 @@ +import { useQuery } from '@tanstack/react-query' +import { z } from 'zod' + +import { RepoConfig } from 'services/repo/useRepoConfig' +import Api from 'shared/api' + +import { query } from './constants' + +interface FetchRepoContentsArgs { + provider: string + owner: string + repo: string + branch: string + path: string + filters?: {} + signal?: AbortSignal +} + +const BasePathContentSchema = z.object({ + hits: z.number().nullable(), + misses: z.number().nullable(), + partials: z.number().nullable(), + lines: z.number().nullable(), + name: z.string(), + path: z.string().nullable(), + percentCovered: z.number().nullable(), +}) + +const PathContentFileSchema = BasePathContentSchema.extend({ + __typename: z.literal('PathContentFile'), + isCriticalFile: z.boolean().nullish(), +}) + +const PathContentDirSchema = BasePathContentSchema.extend({ + __typename: z.literal('PathContentDir'), +}) + +const PathContentsResultSchema = z + .union([PathContentFileSchema, PathContentDirSchema]) + .nullable() + +export const PathContentsSchema = z.object({ + __typename: z.literal('PathContents'), + results: z.array(PathContentsResultSchema), +}) + +export type PathContentsSchemaType = z.infer + +export const UnknownPathSchema = z.object({ + __typename: z.literal('UnknownPath'), + message: z.string().nullish(), +}) + +export const MissingCoverageSchema = z.object({ + __typename: z.literal('MissingCoverage'), + message: z.string().nullish(), +}) + +const MissingHeadReportSchema = z.object({ + __typename: z.literal('MissingHeadReport'), + message: z.string().nullish(), +}) + +const PathContentsUnionSchema = z.discriminatedUnion('__typename', [ + PathContentsSchema, + UnknownPathSchema, + MissingCoverageSchema, + MissingHeadReportSchema, +]) + +export type PathContentResultType = z.infer + +const BranchContentsSchema = z.object({ + owner: z + .object({ + repository: z.object({ + repositoryConfig: RepoConfig, + branch: z.object({ + head: z + .object({ + pathContents: PathContentsUnionSchema.nullish(), + }) + .nullable(), + }), + }), + }) + .nullable(), +}) + +function fetchRepoContents({ + provider, + owner, + repo, + branch, + path, + filters, + signal, +}: FetchRepoContentsArgs) { + return Api.graphql({ + provider, + query, + signal, + variables: { + name: owner, + repo, + branch, + path, + filters, + }, + }).then((res) => { + const parsedData = BranchContentsSchema.safeParse(res?.data) + + if (!parsedData.success) { + console.log('FAIL', parsedData.error) + return null + } + + let results + const pathContentsType = + parsedData?.data?.owner?.repository?.branch?.head?.pathContents + ?.__typename + if (pathContentsType === 'PathContents') { + results = + parsedData?.data?.owner?.repository?.branch?.head?.pathContents?.results + } + + return { + results: results ?? null, + pathContentsType, + indicationRange: + parsedData?.data?.owner?.repository?.repositoryConfig?.indicationRange, + __typename: res?.data?.owner?.repository?.branch?.head?.__typename, + } + }) +} + +interface RepoBranchContentsArgs { + provider: string + owner: string + repo: string + branch: string + path: string + filters?: {} + opts?: { + suspense?: boolean + enabled?: boolean + } +} + +export function useRepoBranchContents({ + provider, + owner, + repo, + branch, + path, + filters, + ...options +}: RepoBranchContentsArgs) { + return useQuery({ + queryKey: [ + 'BranchContents', + provider, + owner, + repo, + branch, + path, + filters, + query, + ], + queryFn: ({ signal }) => + fetchRepoContents({ + provider, + owner, + repo, + branch, + path, + filters, + signal, + }), + ...options, + }) +} diff --git a/src/services/repo/useRepoConfig.ts b/src/services/repo/useRepoConfig.ts index 65697ca095..18e4f632f7 100644 --- a/src/services/repo/useRepoConfig.ts +++ b/src/services/repo/useRepoConfig.ts @@ -3,18 +3,21 @@ import { z } from 'zod' import Api from 'shared/api' +const IndicationRangeSchema = z + .object({ + lowerRange: z.number(), + upperRange: z.number(), + }) + .nullish() + export const RepoConfig = z .object({ - indicationRange: z - .object({ - lowerRange: z.number(), - upperRange: z.number(), - }) - .nullish(), + indicationRange: IndicationRangeSchema, }) .nullish() type RepoConfigData = z.infer +export type IndicationRangeType = z.infer export interface UseRepoConfigArgs { provider: string diff --git a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.jsx b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.jsx index b62bb33436..2b35e1c786 100644 --- a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.jsx +++ b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.jsx @@ -1,28 +1,37 @@ import PropTypes from 'prop-types' +import qs from 'qs' +import { useLocation } from 'react-router-dom' import { usePrefetchBranchDirEntry } from 'services/pathContents/branch/dir' import DirEntry from '../BaseEntries/DirEntry' -function BranchDirEntry({ branch, urlPath, name, filters }) { +function BranchDirEntry({ branch, urlPath, name }) { + const location = useLocation() + + const queryParams = qs.parse(location.search, { + ignoreQueryPrefix: true, + depth: 1, + }) + + const filters = { + ...(queryParams?.flags ? { flags: queryParams.flags } : {}), + ...(queryParams?.components ? { components: queryParams.components } : {}), + } + const { runPrefetch } = usePrefetchBranchDirEntry({ branch, path: name, filters, }) - const queryParams = { - flags: filters?.flags, - components: filters?.components, - } - return ( ) } @@ -31,15 +40,6 @@ BranchDirEntry.propTypes = { branch: PropTypes.string.isRequired, name: PropTypes.string.isRequired, urlPath: PropTypes.string, - filters: PropTypes.shape({ - ordering: PropTypes.shape({ - direction: PropTypes.string, - parameter: PropTypes.any, - }), - searchValue: PropTypes.any, - flags: PropTypes.arrayOf(PropTypes.string), - components: PropTypes.arrayOf(PropTypes.string), - }), } export default BranchDirEntry diff --git a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx index 081937a9be..c4fd2071f9 100644 --- a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx +++ b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx @@ -45,15 +45,18 @@ const queryClient = new QueryClient({ }) const server = setupServer() -const wrapper = ({ children }) => ( - - - - {children} - - - -) +const wrapper = + (initialEntries = ['/gh/codecov/test-repo/tree/main/src/']) => + ({ children }) => + ( + + + + {children} + + + + ) beforeAll(() => server.listen()) afterEach(() => { @@ -79,7 +82,7 @@ describe('BranchDirEntry', () => { setup() render( , - { wrapper } + { wrapper: wrapper() } ) const dir = await screen.findByText('dir') @@ -90,7 +93,7 @@ describe('BranchDirEntry', () => { setup() render( , - { wrapper } + { wrapper: wrapper() } ) const a = await screen.findByRole('link') @@ -100,7 +103,7 @@ describe('BranchDirEntry', () => { ) }) - describe('flags filters is passed', () => { + describe('flags filter is set', () => { it('sets the correct href', async () => { setup() render( @@ -113,18 +116,21 @@ describe('BranchDirEntry', () => { components: [], }} />, - { wrapper } + { + wrapper: wrapper([ + '/gh/codecov/test-repo/tree/main/src?flags=flag-1', + ]), + } ) - const a = await screen.findByRole('link') expect(a).toHaveAttribute( 'href', - '/gh/codecov/test-repo/tree/branch/path%2Fto%2Fdirectory%2Fdir?flags%5B0%5D=flag-1' + '/gh/codecov/test-repo/tree/branch/path%2Fto%2Fdirectory%2Fdir?flags=flag-1' ) }) }) - describe('components and flags filters is passed', () => { + describe('components and flags filters is set', () => { it('sets the correct href', async () => { setup() render( @@ -137,13 +143,17 @@ describe('BranchDirEntry', () => { components: ['component-1'], }} />, - { wrapper } + { + wrapper: wrapper([ + '/gh/codecov/test-repo/tree/main/src?flags=flag-1&components=component-1', + ]), + } ) const a = await screen.findByRole('link') expect(a).toHaveAttribute( 'href', - '/gh/codecov/test-repo/tree/branch/path%2Fto%2Fdirectory%2Fdir?flags%5B0%5D=flag-1&components%5B0%5D=component-1' + '/gh/codecov/test-repo/tree/branch/path%2Fto%2Fdirectory%2Fdir?flags=flag-1&components=component-1' ) }) }) @@ -153,7 +163,7 @@ describe('BranchDirEntry', () => { render( , - { wrapper } + { wrapper: wrapper() } ) await user.hover(screen.getByText('dir')) diff --git a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.jsx b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.jsx index df774c20d7..15df29b156 100644 --- a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.jsx +++ b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.jsx @@ -1,4 +1,6 @@ import PropTypes from 'prop-types' +import qs from 'qs' +import { useLocation } from 'react-router-dom' import { usePrefetchBranchFileEntry } from 'services/pathContents/branch/file' @@ -12,8 +14,18 @@ function BranchFileEntry({ name, urlPath, displayType, - filters, }) { + const location = useLocation() + const queryParams = qs.parse(location.search, { + ignoreQueryPrefix: true, + depth: 1, + }) + + const filters = { + ...(queryParams?.flags ? { flags: queryParams.flags } : {}), + ...(queryParams?.components ? { components: queryParams.components } : {}), + } + const flags = filters?.flags?.length > 0 ? filters?.flags : [] const { runPrefetch } = usePrefetchBranchFileEntry({ @@ -22,11 +34,6 @@ function BranchFileEntry({ flags, }) - const queryParams = { - flags: filters?.flags, - components: filters?.components, - } - return ( ) } @@ -48,10 +55,6 @@ BranchFileEntry.propTypes = { name: PropTypes.string.isRequired, displayType: PropTypes.oneOf(Object.values(displayTypeParameter)), urlPath: PropTypes.string.isRequired, - filters: PropTypes.shape({ - flags: PropTypes.arrayOf(PropTypes.string), - components: PropTypes.arrayOf(PropTypes.string), - }), } export default BranchFileEntry diff --git a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.spec.jsx b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.spec.jsx index 3cb3c9a4c2..dcfab65bd7 100644 --- a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.spec.jsx +++ b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchFileEntry.spec.jsx @@ -57,14 +57,16 @@ const queryClient = new QueryClient({ }) const server = setupServer() -const wrapper = ({ children }) => ( - - - {children} - - -) - +const wrapper = + (initialEntries = ['/gh/codecov/test-repo/']) => + ({ children }) => + ( + + + {children} + + + ) beforeAll(() => { server.listen() }) @@ -107,7 +109,7 @@ describe('BranchFileEntry', () => { isCriticalFile={false} displayType={displayTypeParameter.list} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('dir/file.js') @@ -128,7 +130,7 @@ describe('BranchFileEntry', () => { isCriticalFile={false} displayType={displayTypeParameter.tree} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('file.js') @@ -147,7 +149,7 @@ describe('BranchFileEntry', () => { isCriticalFile={false} displayType={displayTypeParameter.tree} />, - { wrapper } + { wrapper: wrapper() } ) await waitFor(() => queryClient.isFetching) @@ -171,7 +173,7 @@ describe('BranchFileEntry', () => { isCriticalFile={true} displayType={displayTypeParameter.list} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('Critical File') @@ -192,7 +194,7 @@ describe('BranchFileEntry', () => { isCriticalFile={false} displayType={displayTypeParameter.list} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('dir/file.js') @@ -200,7 +202,7 @@ describe('BranchFileEntry', () => { }) }) - describe('flags filters is passed', () => { + describe('flags filters is set', () => { it('sets the correct href', async () => { setup() render( @@ -211,12 +213,12 @@ describe('BranchFileEntry', () => { urlPath="dir" isCriticalFile={false} displayType={displayTypeParameter.tree} - filters={{ - flags: ['flag-1'], - components: [], - }} />, - { wrapper } + { + wrapper: wrapper([ + '/gh/codecov/test-repo/blob/main/dir%2Ffile.js?flags%5B0%5D=flag-1', + ]), + } ) const a = await screen.findByRole('link') @@ -238,12 +240,12 @@ describe('BranchFileEntry', () => { urlPath="dir" isCriticalFile={false} displayType={displayTypeParameter.tree} - filters={{ - flags: ['flag-1'], - components: ['component-3.1415924'], - }} />, - { wrapper } + { + wrapper: wrapper([ + '/gh/codecov/test-repo/blob/main/dir%2Ffile.js?flags%5B0%5D=flag-1&components%5B0%5D=component-3.1415924', + ]), + } ) const a = await screen.findByRole('link') @@ -267,7 +269,7 @@ describe('BranchFileEntry', () => { isCriticalFile={false} displayType={displayTypeParameter.tree} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('file.js') @@ -318,9 +320,12 @@ describe('BranchFileEntry', () => { urlPath="dir" isCriticalFile={false} displayType={displayTypeParameter.tree} - filters={{ flags: ['flag-1'] }} />, - { wrapper } + { + wrapper: wrapper([ + '/gh/codecov/test-repo/blob/main/dir%2Ffile.js?flags%5B0%5D=flag-1&components%5B0%5D=component-3.1415924', + ]), + } ) const file = await screen.findByText('file.js') @@ -352,7 +357,7 @@ describe('BranchFileEntry', () => { displayType={displayTypeParameter.tree} filters={{ flags: [] }} />, - { wrapper } + { wrapper: wrapper() } ) const file = await screen.findByText('file.js') diff --git a/src/shared/ContentsTable/utils/adjustListIfUpDir.tsx b/src/shared/ContentsTable/utils/adjustListIfUpDir.tsx index b2a0635ff3..fdae81a321 100644 --- a/src/shared/ContentsTable/utils/adjustListIfUpDir.tsx +++ b/src/shared/ContentsTable/utils/adjustListIfUpDir.tsx @@ -15,13 +15,13 @@ type TreePath = { } } -type Row = { +export type Row = { name: ReactNode - lines: string - hits: string - misses: string - partials: string - coverage: ReactNode + lines?: string | number | null + hits?: string | number | null + misses?: string | number | null + partials?: string | number | null + coverage?: ReactNode | number | null } export function adjustListIfUpDir({ diff --git a/src/shared/treePaths/useTreePaths.js b/src/shared/treePaths/useTreePaths.js index bc4d6df393..c9ef7a2442 100644 --- a/src/shared/treePaths/useTreePaths.js +++ b/src/shared/treePaths/useTreePaths.js @@ -1,20 +1,24 @@ import dropRight from 'lodash/dropRight' import qs from 'qs' +import { useMemo } from 'react' import { useLocation, useParams } from 'react-router-dom' import { useRepoOverview } from 'services/repo' import { getFilePathParts } from 'shared/utils/url' -function getTreeLocation(paths, location, index) { +function getTreeLocation(paths, index) { return dropRight(paths, paths.length - index - 1).join('/') } export function useTreePaths(passedPath) { const location = useLocation() - const params = qs.parse(location.search, { - ignoreQueryPrefix: true, - depth: 1, - }) + + const params = useMemo(() => { + return qs.parse(location.search, { + ignoreQueryPrefix: true, + depth: 1, + }) + }, [location.search]) const { provider, @@ -34,37 +38,39 @@ export function useTreePaths(passedPath) { { suspense: false } ) - const branch = urlBranch && decodeURIComponent(urlBranch) - const ref = urlRef && decodeURIComponent(urlRef) - const path = urlPath && decodeURIComponent(urlPath) - const filePaths = getFilePathParts(passedPath || path) - const defaultBranch = repoOverview?.defaultBranch + const treePaths = useMemo(() => { + const branch = urlBranch && decodeURIComponent(urlBranch) + const ref = urlRef && decodeURIComponent(urlRef) + const path = urlPath && decodeURIComponent(urlPath) + const filePaths = getFilePathParts(passedPath || path) + const defaultBranch = repoOverview?.defaultBranch - let queryParams = undefined - if (Object.keys(params).length > 0) { - queryParams = params - } + let queryParams = undefined + if (Object.keys(params).length > 0) { + queryParams = params + } - const paths = filePaths?.map((location, index) => ({ - pageName: 'treeView', - text: location, - options: { - tree: getTreeLocation(filePaths, location, index), - ref: branch ?? ref ?? defaultBranch, - queryParams, - }, - })) + const paths = filePaths?.map((location, index) => ({ + pageName: 'treeView', + text: location, + options: { + tree: getTreeLocation(filePaths, index), + ref: branch ?? ref ?? defaultBranch, + queryParams, + }, + })) - const repoPath = { - pageName: 'treeView', - text: repo, - options: { - ref: branch ?? ref ?? defaultBranch, - queryParams, - }, - } + const repoPath = { + pageName: 'treeView', + text: repo, + options: { + ref: branch ?? ref ?? defaultBranch, + queryParams, + }, + } - const treePaths = [repoPath, ...paths] + return [repoPath, ...paths] + }, [urlBranch, urlRef, urlPath, passedPath, repoOverview, params, repo]) return { treePaths } } diff --git a/src/shared/utils/determineProgressColor.ts b/src/shared/utils/determineProgressColor.ts index 6e8c26e664..6bd760d436 100644 --- a/src/shared/utils/determineProgressColor.ts +++ b/src/shared/utils/determineProgressColor.ts @@ -6,17 +6,16 @@ export const determineProgressColor = ({ lowerRange, }: { coverage: number | null - upperRange: number - lowerRange: number + upperRange?: number | null + lowerRange?: number | null }) => { - if (isNumber(coverage)) { - if (coverage < lowerRange) { - return 'danger' - } else if (coverage >= lowerRange && coverage < upperRange) { - return 'warning' - } + if (!isNumber(coverage) || !isNumber(upperRange) || !isNumber(lowerRange)) { return 'primary' } - + if (coverage < lowerRange) { + return 'danger' + } else if (coverage >= lowerRange && coverage < upperRange) { + return 'warning' + } return 'primary' }