Add sorting to variant analysis repositories
This adds sorting to the variant analysis repositories on the outcome panels. The sort state is shared between all panels, so unlike the design this doesn't disable the sort when you are on e.g. the no access panel.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSearchSortRow as RepositoriesSearchSortRowComponent } from '../../view/variant-analysis/RepositoriesSearchSortRow';
|
||||
import { defaultFilterSortState } from '../../view/variant-analysis/filterSort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Search and Sort Row',
|
||||
component: RepositoriesSearchSortRowComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof RepositoriesSearchSortRowComponent>;
|
||||
|
||||
export const RepositoriesSearchSortRow = () => {
|
||||
const [value, setValue] = useState(defaultFilterSortState);
|
||||
|
||||
return (
|
||||
<RepositoriesSearchSortRowComponent value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSort as RepositoriesSortComponent } from '../../view/variant-analysis/RepositoriesSort';
|
||||
import { SortKey } from '../../view/variant-analysis/filterSort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Sort',
|
||||
component: RepositoriesSortComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof RepositoriesSortComponent>;
|
||||
|
||||
export const RepositoriesSort = () => {
|
||||
const [value, setValue] = useState(SortKey.Name);
|
||||
|
||||
return (
|
||||
<RepositoriesSortComponent value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
@@ -11,9 +11,11 @@ const TextField = styled(VSCodeTextField)`
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RepositoriesSearch = ({ value, onChange }: Props) => {
|
||||
export const RepositoriesSearch = ({ value, onChange, className }: Props) => {
|
||||
const handleInput = useCallback((e: InputEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
@@ -25,6 +27,7 @@ export const RepositoriesSearch = ({ value, onChange }: Props) => {
|
||||
placeholder='Filter by repository owner/name'
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
className={className}
|
||||
>
|
||||
<Codicon name="search" label="Search..." slot="start" />
|
||||
</TextField>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RepositoriesFilterSortState, SortKey } from './filterSort';
|
||||
import { RepositoriesSearch } from './RepositoriesSearch';
|
||||
import { RepositoriesSort } from './RepositoriesSort';
|
||||
|
||||
type Props = {
|
||||
value: RepositoriesFilterSortState;
|
||||
onChange: Dispatch<SetStateAction<RepositoriesFilterSortState>>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const RepositoriesSearchColumn = styled(RepositoriesSearch)`
|
||||
flex: 3;
|
||||
`;
|
||||
|
||||
const RepositoriesSortColumn = styled(RepositoriesSort)`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
|
||||
const handleSearchValueChange = useCallback((searchValue: string) => {
|
||||
onChange(oldValue => ({
|
||||
...oldValue,
|
||||
searchValue,
|
||||
}));
|
||||
}, [onChange]);
|
||||
|
||||
const handleSortKeyChange = useCallback((sortKey: SortKey) => {
|
||||
onChange(oldValue => ({
|
||||
...oldValue,
|
||||
sortKey,
|
||||
}));
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<RepositoriesSearchColumn value={value.searchValue} onChange={handleSearchValueChange} />
|
||||
<RepositoriesSortColumn value={value.sortKey} onChange={handleSortKeyChange} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
|
||||
import { SortKey } from './filterSort';
|
||||
import { Codicon } from '../common';
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: SortKey;
|
||||
onChange: (value: SortKey) => void;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RepositoriesSort = ({ value, onChange, className }: Props) => {
|
||||
const handleInput = useCallback((e: InputEvent) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(target.value as SortKey);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
className={className}
|
||||
>
|
||||
<Codicon name="sort-precedence" label="Sort..." slot="indicator" />
|
||||
<VSCodeOption value={SortKey.Name}>Name</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.ResultsCount}>Results</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.Stars}>Stars</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.LastUpdated}>Last commit</VSCodeOption>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { matchesSearchValue } from './filterSort';
|
||||
import { compareWithResults, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
@@ -21,14 +21,14 @@ export type VariantAnalysisAnalyzedReposProps = {
|
||||
repositoryStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
|
||||
searchValue?: string;
|
||||
filterSortState?: RepositoriesFilterSortState;
|
||||
}
|
||||
|
||||
export const VariantAnalysisAnalyzedRepos = ({
|
||||
variantAnalysis,
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
searchValue,
|
||||
filterSortState,
|
||||
}: VariantAnalysisAnalyzedReposProps) => {
|
||||
const repositoryStateById = useMemo(() => {
|
||||
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
|
||||
@@ -47,14 +47,10 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
}, [repositoryResults]);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
if (searchValue) {
|
||||
return variantAnalysis.scannedRepos?.filter((repoTask) => {
|
||||
return matchesSearchValue(repoTask.repository, searchValue);
|
||||
});
|
||||
}
|
||||
|
||||
return variantAnalysis.scannedRepos;
|
||||
}, [searchValue, variantAnalysis.scannedRepos]);
|
||||
return variantAnalysis.scannedRepos?.filter((repoTask) => {
|
||||
return matchesFilter(repoTask.repository, filterSortState);
|
||||
})?.sort(compareWithResults(filterSortState));
|
||||
}, [filterSortState, variantAnalysis.scannedRepos]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
import { RepositoriesSearch } from './RepositoriesSearch';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from './filterSort';
|
||||
import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow';
|
||||
|
||||
export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
@@ -44,7 +45,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
|
||||
@@ -74,12 +75,12 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
|
||||
<RepositoriesSearchSortRow value={filterSortState} onChange={setFilterSortState} />
|
||||
<VariantAnalysisAnalyzedRepos
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
searchValue={searchValue}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -88,7 +89,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
|
||||
<RepositoriesSearchSortRow value={filterSortState} onChange={setFilterSortState} />
|
||||
<VSCodePanels>
|
||||
<Tab>
|
||||
Analyzed
|
||||
@@ -111,7 +112,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
searchValue={searchValue}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
@@ -120,7 +121,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
alertTitle='No access'
|
||||
alertMessage='The following repositories could not be scanned because you do not have read access.'
|
||||
skippedRepositoryGroup={notFoundRepos}
|
||||
searchValue={searchValue}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>}
|
||||
{noCodeqlDbRepos?.repositoryCount &&
|
||||
@@ -129,7 +130,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
alertTitle='No database'
|
||||
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
|
||||
skippedRepositoryGroup={noCodeqlDbRepos}
|
||||
searchValue={searchValue}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>}
|
||||
</VSCodePanels>
|
||||
|
||||
@@ -4,14 +4,14 @@ import styled from 'styled-components';
|
||||
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
|
||||
import { Alert } from '../common';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import { matchesSearchValue } from './filterSort';
|
||||
import { compareRepository, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
|
||||
export type VariantAnalysisSkippedRepositoriesTabProps = {
|
||||
alertTitle: string,
|
||||
alertMessage: string,
|
||||
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
|
||||
|
||||
searchValue?: string,
|
||||
filterSortState?: RepositoriesFilterSortState,
|
||||
};
|
||||
|
||||
function getSkipReasonAlert(
|
||||
@@ -43,17 +43,13 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
|
||||
alertTitle,
|
||||
alertMessage,
|
||||
skippedRepositoryGroup,
|
||||
searchValue,
|
||||
filterSortState,
|
||||
}: VariantAnalysisSkippedRepositoriesTabProps) => {
|
||||
const repositories = useMemo(() => {
|
||||
if (searchValue) {
|
||||
return skippedRepositoryGroup.repositories?.filter((repo) => {
|
||||
return matchesSearchValue(repo, searchValue);
|
||||
});
|
||||
}
|
||||
|
||||
return skippedRepositoryGroup.repositories;
|
||||
}, [searchValue, skippedRepositoryGroup.repositories]);
|
||||
return skippedRepositoryGroup.repositories?.filter((repo) => {
|
||||
return matchesFilter(repo, filterSortState);
|
||||
})?.sort(compareRepository(filterSortState));
|
||||
}, [filterSortState, skippedRepositoryGroup.repositories]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { VariantAnalysisAnalyzedRepos, VariantAnalysisAnalyzedReposProps } from
|
||||
import { createMockVariantAnalysis } from '../../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { createMockScannedRepo } from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
|
||||
describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
const defaultVariantAnalysis = createMockVariantAnalysis(VariantAnalysisStatus.InProgress, [
|
||||
@@ -19,7 +20,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
stargazersCount: 5_000,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
@@ -29,7 +32,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-world-2',
|
||||
private: false,
|
||||
stargazersCount: 20_000,
|
||||
},
|
||||
resultCount: 200,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
@@ -39,7 +44,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-world-3',
|
||||
private: true,
|
||||
stargazersCount: 20,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
},
|
||||
{
|
||||
@@ -49,9 +56,35 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 4,
|
||||
fullName: 'octodemo/hello-world-4',
|
||||
private: false,
|
||||
stargazersCount: 8_000,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.InProgress,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 5,
|
||||
fullName: 'octodemo/hello-world-5',
|
||||
private: false,
|
||||
stargazersCount: 50_000,
|
||||
},
|
||||
resultCount: 55_323,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 6,
|
||||
fullName: 'octodemo/hello-world-6',
|
||||
private: false,
|
||||
stargazersCount: 1,
|
||||
},
|
||||
resultCount: 10_000,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
]);
|
||||
|
||||
const render = (props: Partial<VariantAnalysisAnalyzedReposProps> = {}) => {
|
||||
@@ -110,7 +143,10 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
|
||||
it('uses the search value', () => {
|
||||
render({
|
||||
searchValue: 'world-2',
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
searchValue: 'world-2',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.queryByText('octodemo/hello-world-1')).not.toBeInTheDocument();
|
||||
@@ -118,4 +154,42 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
expect(screen.queryByText('octodemo/hello-world-3')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-world-4')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the sort key', async () => {
|
||||
render({
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(6);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-world-5');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world-2');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world-4');
|
||||
expect(rows[3]).toHaveTextContent('octodemo/hello-world-1');
|
||||
expect(rows[4]).toHaveTextContent('octodemo/hello-world-3');
|
||||
expect(rows[5]).toHaveTextContent('octodemo/hello-world-6');
|
||||
});
|
||||
|
||||
it('uses the results count sort key', async () => {
|
||||
render({
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(6);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-world-5');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world-6');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world-2');
|
||||
expect(rows[3]).toHaveTextContent('octodemo/hello-world-1');
|
||||
expect(rows[4]).toHaveTextContent('octodemo/hello-world-3');
|
||||
expect(rows[5]).toHaveTextContent('octodemo/hello-world-4');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisSkippedRepositoriesTab, VariantAnalysisSkippedRepositoriesTabProps } from '../VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
|
||||
describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
const render = (props: VariantAnalysisSkippedRepositoriesTabProps) =>
|
||||
@@ -116,11 +117,81 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
searchValue: 'world',
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
searchValue: 'world',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-galaxy')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-universe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the sort key', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
stargazersCount: 300,
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
stargazersCount: 50,
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
stargazersCount: 500,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-universe');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-galaxy');
|
||||
});
|
||||
|
||||
it('does not use the result count sort key', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
},
|
||||
],
|
||||
},
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-galaxy');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-universe');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { matchesSearchValue } from '../filterSort';
|
||||
import { compareRepository, compareWithResults, defaultFilterSortState, matchesFilter, SortKey } from '../filterSort';
|
||||
|
||||
describe(matchesSearchValue.name, () => {
|
||||
describe(matchesFilter.name, () => {
|
||||
const repository = {
|
||||
fullName: 'github/codeql'
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
{ searchValue: undefined, matches: true },
|
||||
{ searchValue: '', matches: true },
|
||||
{ searchValue: 'github/codeql', matches: true },
|
||||
{ searchValue: 'github', matches: true },
|
||||
@@ -22,6 +21,259 @@ describe(matchesSearchValue.name, () => {
|
||||
];
|
||||
|
||||
test.each(testCases)('returns $matches if searching for $searchValue', ({ searchValue, matches }) => {
|
||||
expect(matchesSearchValue(repository, searchValue)).toBe(matches);
|
||||
expect(matchesFilter(repository, {
|
||||
...defaultFilterSortState,
|
||||
searchValue,
|
||||
})).toBe(matches);
|
||||
});
|
||||
});
|
||||
|
||||
describe(compareRepository.name, () => {
|
||||
describe('when sort key is undefined', () => {
|
||||
const sorter = compareRepository(undefined);
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy'
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world'
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is name', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Name,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy'
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world'
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is stars', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy',
|
||||
stargazersCount: 1,
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world',
|
||||
stargazersCount: 10,
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
stargazersCount: left.stargazersCount,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
stargazersCount: undefined,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is last updated', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.LastUpdated,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy',
|
||||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world',
|
||||
updatedAt: '2021-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
updatedAt: left.updatedAt,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter({
|
||||
...left,
|
||||
updatedAt: undefined,
|
||||
}, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(compareWithResults.name, () => {
|
||||
describe('when sort key is undefined', () => {
|
||||
const sorter = compareWithResults(undefined);
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is stars', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
stargazersCount: 1,
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
stargazersCount: 10,
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is last updated', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.LastUpdated,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
updatedAt: '2021-01-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is results count', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
resultCount: 10,
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
},
|
||||
resultCount: 100,
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
resultCount: left.resultCount,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter({
|
||||
...left,
|
||||
resultCount: undefined,
|
||||
}, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,73 @@
|
||||
import { Repository } from '../../remote-queries/shared/repository';
|
||||
import { Repository, RepositoryWithMetadata } from '../../remote-queries/shared/repository';
|
||||
import { parseDate } from '../../pure/date';
|
||||
|
||||
export function matchesSearchValue(repo: Pick<Repository, 'fullName'>, searchValue: string | undefined): boolean {
|
||||
if (!searchValue) {
|
||||
export enum SortKey {
|
||||
Name = 'name',
|
||||
Stars = 'stars',
|
||||
LastUpdated = 'lastUpdated',
|
||||
ResultsCount = 'resultsCount',
|
||||
}
|
||||
|
||||
export type RepositoriesFilterSortState = {
|
||||
searchValue: string;
|
||||
sortKey: SortKey;
|
||||
}
|
||||
|
||||
export const defaultFilterSortState: RepositoriesFilterSortState = {
|
||||
searchValue: '',
|
||||
sortKey: SortKey.Name,
|
||||
};
|
||||
|
||||
export function matchesFilter(repo: Pick<Repository, 'fullName'>, filterSortState: RepositoriesFilterSortState | undefined): boolean {
|
||||
if (!filterSortState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return repo.fullName.toLowerCase().includes(searchValue.toLowerCase());
|
||||
return repo.fullName.toLowerCase().includes(filterSortState.searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
type SortableRepository = Pick<Repository, 'fullName'> & Partial<Pick<RepositoryWithMetadata, 'stargazersCount' | 'updatedAt'>>;
|
||||
|
||||
export function compareRepository(filterSortState: RepositoriesFilterSortState | undefined): (left: SortableRepository, right: SortableRepository) => number {
|
||||
return (left: SortableRepository, right: SortableRepository) => {
|
||||
// Highest to lowest
|
||||
if (filterSortState?.sortKey === SortKey.Stars) {
|
||||
const stargazersCount = (right.stargazersCount ?? 0) - (left.stargazersCount ?? 0);
|
||||
if (stargazersCount !== 0) {
|
||||
return stargazersCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Newest to oldest
|
||||
if (filterSortState?.sortKey === SortKey.LastUpdated) {
|
||||
const lastUpdated = (parseDate(right.updatedAt)?.getTime() ?? 0) - (parseDate(left.updatedAt)?.getTime() ?? 0);
|
||||
if (lastUpdated !== 0) {
|
||||
return lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back on name compare
|
||||
return left.fullName.localeCompare(right.fullName, undefined, { sensitivity: 'base' });
|
||||
};
|
||||
}
|
||||
|
||||
type SortableResult = {
|
||||
repository: SortableRepository;
|
||||
resultCount?: number;
|
||||
}
|
||||
|
||||
export function compareWithResults(filterSortState: RepositoriesFilterSortState | undefined): (left: SortableResult, right: SortableResult) => number {
|
||||
const fallbackSort = compareRepository(filterSortState);
|
||||
|
||||
return (left: SortableResult, right: SortableResult) => {
|
||||
// Highest to lowest
|
||||
if (filterSortState?.sortKey === SortKey.ResultsCount) {
|
||||
const resultCount = (right.resultCount ?? 0) - (left.resultCount ?? 0);
|
||||
if (resultCount !== 0) {
|
||||
return resultCount;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackSort(left.repository, right.repository);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user