Skip to content

Commit

Permalink
Merge branch 'main' into data-use-terms
Browse files Browse the repository at this point in the history
  • Loading branch information
bh-ethz authored Apr 11, 2024
2 parents 3148b2d + 8b0c343 commit 73e9073
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 9 deletions.
7 changes: 7 additions & 0 deletions kubernetes/loculus/templates/_common-metadata.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ fields:
autocomplete: true
- name: submittedAt
type: timestamp
displayName: Date submitted
- name: releasedAt
type: timestamp
displayName: Date released
- name: dataUseTerms
type: string
generateIndex: true
autocomplete: true
displayName: Data use terms
initiallyVisible: true
customDisplay:
type: dataUseTerms
- name: versionStatus
Expand Down Expand Up @@ -91,6 +95,9 @@ fields:
{{- if .notSearchable }}
notSearchable: {{ .notSearchable }}
{{- end }}
{{- if .initiallyVisible }}
initiallyVisible: {{ .initiallyVisible }}
{{- end }}
{{- if .displayName }}
displayName: {{ .displayName }}
{{- end }}
Expand Down
8 changes: 8 additions & 0 deletions kubernetes/loculus/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,28 @@ defaultOrganisms:
metadata:
- name: date
type: date
initiallyVisible: true
- name: region
type: string
initiallyVisible: true
generateIndex: true
autocomplete: true
- name: country
initiallyVisible: true
type: string
generateIndex: true
autocomplete: true
- name: division
initiallyVisible: true
type: string
generateIndex: true
autocomplete: true
- name: host
initiallyVisible: true
type: string
autocomplete: true
- name: pango_lineage
initiallyVisible: true
type: pango_lineage
autocomplete: true
required: true
Expand Down Expand Up @@ -273,6 +279,7 @@ defaultOrganisms:
displayName: Collection date
type: date
required: true
initiallyVisible: true
- name: ncbi_release_date
displayName: NCBI release date
type: date
Expand All @@ -281,6 +288,7 @@ defaultOrganisms:
required: true
generateIndex: true
autocomplete: true
initiallyVisible: true
- name: isolate_name
displayName: Isolate name
type: string
Expand Down
97 changes: 97 additions & 0 deletions website/src/components/SearchPage/CustomizeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Dialog, Transition } from '@headlessui/react';

interface CheckboxFieldProps {
label: string;
checked: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
}

const CheckboxField: React.FC<CheckboxFieldProps> = ({ label, checked, onChange, disabled }) => (
<div className='mb-2'>
<label className='flex items-center cursor-pointer'>
<input
type='checkbox'
checked={checked}
onChange={onChange}
className={`form-checkbox h-5 w-5 ${disabled !== true ? 'text-blue-600' : 'text-gray-300'}`}
disabled={disabled}
/>
<span className='ml-2 text-gray-700'>{label}</span>
</label>
</div>
);

interface FieldValue {
name: string;
label: string;
isVisible?: boolean;
notSearchable?: boolean;
}

interface CustomizeModalProps {
isCustomizeModalOpen: boolean;
toggleCustomizeModal: () => void;
alwaysPresentFieldNames: string[];
fieldValues: FieldValue[];
handleFieldVisibilityChange: (fieldName: string, isVisible: boolean) => void;
}

export const CustomizeModal: React.FC<CustomizeModalProps> = ({
isCustomizeModalOpen,
toggleCustomizeModal,
alwaysPresentFieldNames,
fieldValues,
handleFieldVisibilityChange,
}) => {
return (
<Transition appear show={isCustomizeModalOpen}>
<Dialog as='div' className='fixed inset-0 z-10 overflow-y-auto' onClose={toggleCustomizeModal}>
<div className='min-h-screen px-4 text-center'>
<Dialog.Overlay className='fixed inset-0 bg-black opacity-30' />

<span className='inline-block h-screen align-middle' aria-hidden='true'>
&#8203;
</span>

<div className='inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl text-sm'>
<Dialog.Title as='h3' className='text-lg font-medium leading-6 text-gray-900'>
Customize Search Fields
</Dialog.Title>

<div className='mt-4 text-gray-700 text-sm'>Toggle the visibility of search fields</div>

<div className='mt-4'>
{alwaysPresentFieldNames.map((fieldName) => (
<CheckboxField key={fieldName} label={fieldName} checked disabled />
))}

{fieldValues
.filter((field) => field.notSearchable !== true)
.map((field) => (
<CheckboxField
key={field.name}
label={field.label}
checked={field.isVisible !== false}
onChange={(e) => {
handleFieldVisibilityChange(field.name, e.target.checked);
}}
/>
))}
</div>

<div className='mt-6'>
<button
type='button'
className='inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500'
onClick={toggleCustomizeModal}
>
Close
</button>
</div>
</div>
</div>
</Dialog>
</Transition>
);
};
59 changes: 56 additions & 3 deletions website/src/components/SearchPage/SearchForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import type { MetadataFilter } from '../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';

global.ResizeObserver = class FakeResizeObserver {
// This is needed or we get a test failure: https://github.com/tailwindlabs/headlessui/discussions/2414
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, @typescript-eslint/no-empty-function
observe() {}
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, @typescript-eslint/no-empty-function
disconnect() {}
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, @typescript-eslint/no-empty-function
unobserve() {}
};

const searchButtonText = 'Search sequences';

vi.mock('../../config', () => ({
Expand All @@ -20,9 +30,30 @@ vi.mock('../../config', () => ({
const queryClient = new QueryClient();

const defaultSearchFormFilters = [
{ name: 'field1', type: 'string' as const, label: 'Field 1', autocomplete: false, filterValue: '' },
{ name: 'field2', type: 'date' as const, autocomplete: false, filterValue: '', label: 'Field 2' },
{ name: 'field3', type: 'pango_lineage' as const, label: 'Field 3', autocomplete: true, filterValue: '' },
{
name: 'field1',
type: 'string' as const,
label: 'Field 1',
autocomplete: false,
filterValue: '',
initiallyVisible: true,
},
{
name: 'field2',
type: 'date' as const,
autocomplete: false,
filterValue: '',
label: 'Field 2',
initiallyVisible: true,
},
{
name: 'field3',
type: 'pango_lineage' as const,
label: 'Field 3',
autocomplete: true,
filterValue: '',
initiallyVisible: true,
},
];

const defaultReferenceGenomesSequenceNames = {
Expand Down Expand Up @@ -90,6 +121,7 @@ describe('SearchForm', () => {
autocomplete: false,
filterValue: '',
notSearchable: true,
initiallyVisible: true,
},
]);

Expand All @@ -104,6 +136,7 @@ describe('SearchForm', () => {
name: timestampFieldName,
type: 'timestamp' as const,
filterValue: '1706147200',
initiallyVisible: true,
},
]);

Expand All @@ -128,6 +161,7 @@ describe('SearchForm', () => {
name: dateFieldName,
type: 'date' as const,
filterValue: '2024-01-25',
initiallyVisible: true,
},
]);
const dateLabel = screen.getByText('Date field');
Expand All @@ -143,4 +177,23 @@ describe('SearchForm', () => {

expect(window.location.href).toContain(`${dateFieldName}=2025-01-25`);
});

test('toggle field visibility', async () => {
renderSearchForm();

expect(screen.getByLabelText('Field 1')).toBeVisible();

const customizeButton = screen.getByRole('button', { name: 'Customize fields' });
await userEvent.click(customizeButton);

const field1Checkbox = screen.getByRole('checkbox', { name: 'Field 1' });
expect(field1Checkbox).toBeChecked();

await userEvent.click(field1Checkbox);

const closeButton = screen.getByRole('button', { name: 'Close' });
await userEvent.click(closeButton);

expect(screen.queryByLabelText('Field 1')).not.toBeInTheDocument();
});
});
67 changes: 63 additions & 4 deletions website/src/components/SearchPage/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { sentenceCase } from 'change-case';
import { type FC, type FormEventHandler, useMemo, useState, useCallback } from 'react';

import { CustomizeModal } from './CustomizeModal.tsx';
import { AccessionField } from './fields/AccessionField.tsx';
import { AutoCompleteField, type AutoCompleteFieldProps } from './fields/AutoCompleteField';
import { DateField, TimestampField } from './fields/DateField.tsx';
Expand Down Expand Up @@ -50,12 +51,16 @@ export const SearchForm: FC<SearchFormProps> = ({
fieldList.map((filter) => ({
...filter,
label: filter.label ?? filter.displayName ?? sentenceCase(filter.name),
isVisible: filter.initiallyVisible ?? false,
})),
);

const alwaysPresentFieldNames = ['Accession', 'Mutation'];
const [accessionFilter, setAccessionFilter] = useState<AccessionFilter>(initialAccessionFilter);
const [mutationFilter, setMutationFilter] = useState<MutationFilter>(initialMutationFilter);
const [isLoading, setIsLoading] = useState(false);
const { isOpen: isMobileOpen, close: closeOnMobile, toggle: toggleMobileOpen } = useOffCanvas();
const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false);

const handleFieldChange = useCallback(
(metadataName: string, filter: string) => {
Expand Down Expand Up @@ -112,10 +117,15 @@ export const SearchForm: FC<SearchFormProps> = ({
const fields = useMemo(
() =>
fieldValues.map((field) => {
if (field.isVisible !== true) {
return null;
}
if (field.grouped === true) {
return (
<div key={field.name} className='flex flex-col border p-3 mb-3 rounded-md border-gray-300'>
<h3 className='text-gray-500 text-sm mb-1'>{field.label}</h3>
<h3 className='text-gray-500 text-sm mb-1'>
{field.displayName !== undefined ? field.displayName : field.label}
</h3>

{field.groupedFields.map((groupedField) => (
<SearchField
Expand Down Expand Up @@ -145,6 +155,38 @@ export const SearchForm: FC<SearchFormProps> = ({
[fieldValues, handleFieldChange, isLoading, lapisUrl, flattenedFieldValues],
);

const toggleCustomizeModal = () => {
setIsCustomizeModalOpen(!isCustomizeModalOpen);
};

const clearValues = (possiblyGroupedFieldName: string) => {
const fieldInQuestion = fieldValues.find((field) => field.name === possiblyGroupedFieldName);
if (fieldInQuestion === undefined) {
return;
}

if (fieldInQuestion.grouped === true) {
for (const groupedField of fieldInQuestion.groupedFields) {
handleFieldChange(groupedField.name, '');
}
} else {
handleFieldChange(possiblyGroupedFieldName, '');
}
};
const handleFieldVisibilityChange = (fieldName: string, isVisible: boolean) => {
if (isVisible === false) {
clearValues(fieldName);
}
setFieldValues((prev) =>
prev.map((field) => {
if (field.name === fieldName) {
return { ...field, isVisible };
}
return field;
}),
);
};

return (
<QueryClientProvider client={queryClient}>
<div className='text-right -mb-10 md:hidden'>
Expand All @@ -162,13 +204,20 @@ export const SearchForm: FC<SearchFormProps> = ({
<div className='shadow-xl rounded-r-lg px-4 pt-4'>
<div className='flex'>
<h2 className='text-lg font-semibold flex-1 md:hidden'>Search query</h2>
<button className='underline' onClick={resetSearch}>
Reset
</button>
<div className='flex items-center justify-between w-full mb-2 text-primary-700'>
<button className='underline' onClick={toggleCustomizeModal}>
Customize fields
</button>

<button className='underline' onClick={resetSearch}>
Reset
</button>
</div>
<button className='ml-4 md:hidden' onClick={closeOnMobile}>
<SandwichIcon isOpen />
</button>
</div>

<form onSubmit={handleSearch}>
<div className='flex flex-col'>
<AccessionField initialValue={initialAccessionFilter} onChange={setAccessionFilter} />
Expand All @@ -191,6 +240,13 @@ export const SearchForm: FC<SearchFormProps> = ({
</form>
</div>
</div>
<CustomizeModal
isCustomizeModalOpen={isCustomizeModalOpen}
toggleCustomizeModal={toggleCustomizeModal}
alwaysPresentFieldNames={alwaysPresentFieldNames}
fieldValues={fieldValues}
handleFieldVisibilityChange={handleFieldVisibilityChange}
/>
</QueryClientProvider>
);
};
Expand Down Expand Up @@ -267,6 +323,9 @@ const consolidateGroupedFields = (filters: MetadataFilter[]): (MetadataFilter |
groupedFields: [],
type: filter.type,
grouped: true,
displayName: filter.fieldGroupDisplayName,
label: filter.label,
initiallyVisible: filter.initiallyVisible,
};
fieldList.push(fieldForGroup);
groupsMap.set(filter.fieldGroup, fieldForGroup);
Expand Down
Loading

0 comments on commit 73e9073

Please sign in to comment.