Skip to content

Commit

Permalink
feat(website): add mutation filter to the search (#818)
Browse files Browse the repository at this point in the history
  • Loading branch information
chaoran-chen authored Jan 23, 2024
1 parent 830d923 commit c7b4e6c
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 42 deletions.
19 changes: 16 additions & 3 deletions website/src/components/SearchPage/SearchForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { SearchForm } from './SearchForm';
import { testConfig, testOrganism } from '../../../vitest.setup.ts';
import { routes } from '../../routes.ts';
import type { Filter } from '../../types/config.ts';
import type { MetadataFilter } from '../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';

vi.mock('../../config', () => ({
Expand All @@ -22,13 +23,25 @@ const defaultSearchFormFilters = [
{ name: 'field3', type: 'pango_lineage' as const, label: 'Field 3', autocomplete: true, filterValue: '' },
];

const defaultReferenceGenomesSequenceNames = {
nucleotideSequences: ['main'],
genes: ['gene1', 'gene2'],
};

function renderSearchForm(
searchFormFilters: Filter[] = [...defaultSearchFormFilters],
searchFormFilters: MetadataFilter[] = [...defaultSearchFormFilters],
clientConfig: ClientConfig = testConfig.public,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames = defaultReferenceGenomesSequenceNames,
) {
render(
<QueryClientProvider client={queryClient}>
<SearchForm organism={testOrganism} filters={searchFormFilters} clientConfig={clientConfig} />
<SearchForm
organism={testOrganism}
filters={searchFormFilters}
initialMutationFilter={{}}
clientConfig={clientConfig}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
/>
</QueryClientProvider>,
);
}
Expand Down
30 changes: 24 additions & 6 deletions website/src/components/SearchPage/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { type FC, type FormEventHandler, useMemo, useState } from 'react';

import { AutoCompleteField } from './fields/AutoCompleteField';
import { DateField } from './fields/DateField';
import { MutationField } from './fields/MutationField.tsx';
import { NormalTextField } from './fields/NormalTextField';
import { PangoLineageField } from './fields/PangoLineageField';
import { getClientLogger } from '../../clientLogger.ts';
import { getLapisUrl } from '../../config.ts';
import { useOffCanvas } from '../../hooks/useOffCanvas';
import { routes } from '../../routes.ts';
import type { Filter } from '../../types/config.ts';
import type { MetadataFilter, MutationFilter } from '../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
import { OffCanvasOverlay } from '../OffCanvasOverlay';
import { SandwichIcon } from '../SandwichIcon';
Expand All @@ -22,19 +24,28 @@ const queryClient = new QueryClient();

interface SearchFormProps {
organism: string;
filters: Filter[];
filters: MetadataFilter[];
initialMutationFilter: MutationFilter;
clientConfig: ClientConfig;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
}

const clientLogger = getClientLogger('SearchForm');

export const SearchForm: FC<SearchFormProps> = ({ organism, filters, clientConfig }) => {
const [fieldValues, setFieldValues] = useState<(Filter & { label: string })[]>(
export const SearchForm: FC<SearchFormProps> = ({
organism,
filters,
initialMutationFilter,
clientConfig,
referenceGenomesSequenceNames,
}) => {
const [fieldValues, setFieldValues] = useState<(MetadataFilter & { label: string })[]>(
filters.map((filter) => ({
...filter,
label: filter.label ?? sentenceCase(filter.name),
})),
);
const [mutationFilter, setMutationFilter] = useState<MutationFilter>(initialMutationFilter);
const [isLoading, setIsLoading] = useState(false);
const { isOpen: isMobileOpen, close: closeOnMobile, toggle: toggleMobileOpen } = useOffCanvas();

Expand All @@ -53,7 +64,7 @@ export const SearchForm: FC<SearchFormProps> = ({ organism, filters, clientConfi
const handleSearch: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
setIsLoading(true);
location.href = routes.searchPage(organism, fieldValues);
location.href = routes.searchPage(organism, fieldValues, mutationFilter);
};

const resetSearch = async () => {
Expand Down Expand Up @@ -119,7 +130,14 @@ export const SearchForm: FC<SearchFormProps> = ({ organism, filters, clientConfi
</button>
</div>
<form onSubmit={handleSearch}>
<div className='flex flex-col'>{fields}</div>
<div className='flex flex-col'>
<MutationField
referenceGenomes={referenceGenomesSequenceNames}
value={mutationFilter}
onChange={setMutationFilter}
/>
{fields}
</div>
<div className='sticky bottom-0 z-10'>
<div
className='h-3'
Expand Down
19 changes: 13 additions & 6 deletions website/src/components/SearchPage/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { capitalCase } from 'change-case';
import type { FC, ReactElement } from 'react';

import { routes } from '../../routes.ts';
import type { Filter, Schema } from '../../types/config.ts';
import type { MetadataFilter, MutationFilter, Schema } from '../../types/config.ts';
import type { OrderBy } from '../../types/lapis.ts';
import MdiTriangle from '~icons/mdi/triangle';
import MdiTriangleDown from '~icons/mdi/triangle-down';
Expand All @@ -15,12 +15,13 @@ type TableProps = {
organism: string;
schema: Schema;
data: TableSequenceData[];
filters: Filter[];
metadataFilter: MetadataFilter[];
mutationFilter: MutationFilter;
page: number;
orderBy?: OrderBy;
};

export const Table: FC<TableProps> = ({ organism, data, schema, filters, page, orderBy }) => {
export const Table: FC<TableProps> = ({ organism, data, schema, metadataFilter, mutationFilter, page, orderBy }) => {
const primaryKey = schema.primaryKey;

const columns = schema.tableColumns.map((field) => ({
Expand All @@ -31,12 +32,18 @@ export const Table: FC<TableProps> = ({ organism, data, schema, filters, page, o
const handleSort = (field: string) => {
if (orderBy?.field === field) {
if (orderBy.type === 'ascending') {
location.href = routes.searchPage(organism, filters, page, { field, type: 'descending' });
location.href = routes.searchPage(organism, metadataFilter, mutationFilter, page, {
field,
type: 'descending',
});
} else {
location.href = routes.searchPage(organism, filters);
location.href = routes.searchPage(organism, metadataFilter, mutationFilter);
}
} else {
location.href = routes.searchPage(organism, filters, page, { field, type: 'ascending' });
location.href = routes.searchPage(organism, metadataFilter, mutationFilter, page, {
field,
type: 'ascending',
});
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type FC, useEffect, useMemo, useState } from 'react';
import type { FieldProps } from './FieldProps';
import { getClientLogger } from '../../../clientLogger.ts';
import { lapisClientHooks } from '../../../services/serviceHooks.ts';
import type { Filter } from '../../../types/config.ts';
import type { MetadataFilter } from '../../../types/config.ts';

const logger = getClientLogger('AutoCompleteField');

Expand Down Expand Up @@ -74,7 +74,7 @@ export const AutoCompleteField: FC<FieldProps> = ({ field, allFields, handleFiel
);
};

function getOtherFieldsFilter(allFields: Filter[], field: Filter) {
function getOtherFieldsFilter(allFields: MetadataFilter[], field: MetadataFilter) {
return allFields
.filter((f) => f.name !== field.name && f.filterValue !== '')
.reduce((acc, f) => ({ ...acc, [f.name]: f.filterValue }), {});
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/SearchPage/fields/FieldProps.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Filter } from '../../../types/config.ts';
import type { MetadataFilter } from '../../../types/config.ts';

export type FieldProps = {
field: Filter;
allFields: Filter[];
field: MetadataFilter;
allFields: MetadataFilter[];
handleFieldChange: (metadataName: string, filter: string) => void;
isLoading: boolean;
lapisUrl: string;
Expand Down
77 changes: 77 additions & 0 deletions website/src/components/SearchPage/fields/MutationField.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';

import { MutationField } from './MutationField.tsx';
import type { MutationFilter } from '../../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';

const singleSegmentedReferenceGenome: ReferenceGenomesSequenceNames = {
nucleotideSequences: ['main'],
genes: ['gene1', 'gene2'],
};

const multiSegmentedReferenceGenome: ReferenceGenomesSequenceNames = {
nucleotideSequences: ['seg1', 'seg2'],
genes: ['gene1', 'gene2'],
};

function renderField(
value: MutationFilter,
onChange: (mutationFilter: MutationFilter) => void,
referenceGenome: ReferenceGenomesSequenceNames,
) {
render(<MutationField value={value} onChange={onChange} referenceGenomes={referenceGenome} />);
}

describe('MutationField', () => {
test('should render provided value', async () => {
const handleChange = vi.fn();
renderField(
{
aminoAcidMutationQueries: ['gene1:10Y'],
nucleotideMutationQueries: ['A20T'],
nucleotideInsertionQueries: ['ins_30:G?G'],
},
handleChange,
singleSegmentedReferenceGenome,
);
expect(screen.queryByText('gene1:10Y')).toBeInTheDocument();
expect(screen.queryByText('A20T')).toBeInTheDocument();
expect(screen.queryByText('ins_30:G?G')).toBeInTheDocument();
});

test('should accept input and dispatch events (single-segmented)', async () => {
const handleChange = vi.fn();
renderField({}, handleChange, singleSegmentedReferenceGenome);

await userEvent.type(screen.getByLabelText('Mutations'), 'G100A{enter}');
expect(handleChange).toHaveBeenCalledWith({
nucleotideMutationQueries: ['G100A'],
aminoAcidMutationQueries: [],
nucleotideInsertionQueries: [],
aminoAcidInsertionQueries: [],
});
});

test('should accept input and dispatch events (multi-segmented)', async () => {
const handleChange = vi.fn();
renderField({}, handleChange, multiSegmentedReferenceGenome);

await userEvent.type(screen.getByLabelText('Mutations'), 'seg1:G100A{enter}');
expect(handleChange).toHaveBeenCalledWith({
nucleotideMutationQueries: ['seg1:G100A'],
aminoAcidMutationQueries: [],
nucleotideInsertionQueries: [],
aminoAcidInsertionQueries: [],
});
});

test('should reject invalid input', async () => {
const handleChange = vi.fn();
renderField({}, handleChange, singleSegmentedReferenceGenome);

await userEvent.type(screen.getByLabelText('Mutations'), 'main:G200A{enter}');
expect(handleChange).toHaveBeenCalledTimes(0);
});
});
Loading

0 comments on commit c7b4e6c

Please sign in to comment.