Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(website): allow customizing columns #1993

Merged
merged 10 commits into from
Jun 19, 2024
13 changes: 11 additions & 2 deletions website/src/components/SearchPage/CustomizeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Dialog, Transition } from '@headlessui/react';

const titleCaseWords = (str: string) => {
return str
.split(' ')
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(' ');
};

interface CheckboxFieldProps {
label: string;
checked: boolean;
Expand Down Expand Up @@ -29,6 +36,7 @@ interface CustomizeModalProps {
visibilities: Map<string, boolean>;
setAVisibility: (fieldName: string, isVisible: boolean) => void;
nameToLabelMap: Record<string, string>;
thingToCustomize: string;
}

export const CustomizeModal: React.FC<CustomizeModalProps> = ({
Expand All @@ -38,6 +46,7 @@ export const CustomizeModal: React.FC<CustomizeModalProps> = ({
visibilities,
setAVisibility,
nameToLabelMap,
thingToCustomize,
}) => {
return (
<Transition appear show={isCustomizeModalOpen}>
Expand All @@ -51,10 +60,10 @@ export const CustomizeModal: React.FC<CustomizeModalProps> = ({

<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
Customize {titleCaseWords(thingToCustomize)}s
</Dialog.Title>

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

<div className='mt-4'>
{alwaysPresentFieldNames.map((fieldName) => (
Expand Down
15 changes: 8 additions & 7 deletions website/src/components/SearchPage/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ interface SearchFormProps {
fieldValues: FieldValues;
setAFieldValue: SetAFieldValue;
lapisUrl: string;
visibilities: Map<string, boolean>;
setAVisibility: (fieldName: string, value: boolean) => void;
searchVisibilities: Map<string, boolean>;
setASearchVisibility: (fieldName: string, value: boolean) => void;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
lapisSearchParameters: Record<string, any>;
}
Expand All @@ -33,12 +33,12 @@ export const SearchForm = ({
fieldValues,
setAFieldValue,
lapisUrl,
visibilities,
setAVisibility,
searchVisibilities,
setASearchVisibility,
referenceGenomesSequenceNames,
lapisSearchParameters,
}: SearchFormProps) => {
const visibleFields = consolidatedMetadataSchema.filter((field) => visibilities.get(field.name));
const visibleFields = consolidatedMetadataSchema.filter((field) => searchVisibilities.get(field.name));

const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false);
const { isOpen: isMobileOpen, close: closeOnMobile, toggle: toggleMobileOpen } = useOffCanvas();
Expand Down Expand Up @@ -78,11 +78,12 @@ export const SearchForm = ({
</div>{' '}
</div>
<CustomizeModal
thingToCustomize='search field'
isCustomizeModalOpen={isCustomizeModalOpen}
toggleCustomizeModal={toggleCustomizeModal}
alwaysPresentFieldNames={[]}
visibilities={visibilities}
setAVisibility={setAVisibility}
visibilities={searchVisibilities}
setAVisibility={setASearchVisibility}
nameToLabelMap={consolidatedMetadataSchema.reduce(
(acc, field) => {
acc[field.name] = field.displayName ?? field.label ?? sentenceCase(field.name);
Expand Down
121 changes: 94 additions & 27 deletions website/src/components/SearchPage/SearchFullUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { sentenceCase } from 'change-case';
import { useEffect, useMemo, useState } from 'react';

import { CustomizeModal } from './CustomizeModal.tsx';
import { DownloadDialog } from './DownloadDialog/DownloadDialog.tsx';
import { RecentSequencesBanner } from './RecentSequencesBanner.tsx';
import { SearchForm } from './SearchForm';
Expand All @@ -23,6 +24,8 @@ const orderDirectionKey = 'order';

const VISIBILITY_PREFIX = 'visibility_';

const COLUMN_VISIBILITY_PREFIX = 'column_';

interface InnerSearchFullUIProps {
accessToken?: string;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
Expand Down Expand Up @@ -50,6 +53,8 @@ export const InnerSearchFullUI = ({
}
const metadataSchema = schema.metadata;

const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);

const metadataSchemaWithExpandedRanges = useMemo(() => {
const result = [];
for (const field of metadataSchema) {
Expand Down Expand Up @@ -82,7 +87,52 @@ export const InnerSearchFullUI = ({
const [state, setState] = useQueryAsState({});
const [page, setPage] = useState(1);

const orderByField = state.orderBy ?? schema.defaultOrderBy ?? schema.primaryKey;
const searchVisibilities = useMemo(() => {
const visibilities = new Map<string, boolean>();
schema.metadata.forEach((field) => {
if (field.hideOnSequenceDetailsPage === true) {
return;
}
visibilities.set(field.name, field.initiallyVisible === true);
});

const visibilityKeys = Object.keys(state).filter((key) => key.startsWith(VISIBILITY_PREFIX));

for (const key of visibilityKeys) {
visibilities.set(key.slice(VISIBILITY_PREFIX.length), state[key] === 'true');
}
return visibilities;
}, [schema.metadata, state]);

const columnVisibilities = useMemo(() => {
const visibilities = new Map<string, boolean>();
schema.metadata.forEach((field) => {
if (field.hideOnSequenceDetailsPage === true) {
return;
}
visibilities.set(field.name, schema.tableColumns.includes(field.name));
});

const visibilityKeys = Object.keys(state).filter((key) => key.startsWith(COLUMN_VISIBILITY_PREFIX));

for (const key of visibilityKeys) {
visibilities.set(key.slice(COLUMN_VISIBILITY_PREFIX.length), state[key] === 'true');
}

return visibilities;
}, [schema.metadata, schema.tableColumns, state]);

const columnsToShow = useMemo(() => {
return schema.metadata
.filter((field) => columnVisibilities.get(field.name) === true)
.map((field) => field.name);
}, [schema.metadata, columnVisibilities]);

let orderByField = state.orderBy ?? schema.defaultOrderBy ?? schema.primaryKey;
if (!columnsToShow.includes(orderByField)) {
orderByField = schema.primaryKey;
}

const orderDirection = state.order ?? schema.defaultOrder ?? 'ascending';

const setOrderByField = (field: string) => {
Expand All @@ -98,23 +148,9 @@ export const InnerSearchFullUI = ({
}));
};

const visibilities = useMemo(() => {
const visibilities = new Map<string, boolean>();
schema.metadata.forEach((field) => {
visibilities.set(field.name, field.initiallyVisible === true);
});

const visibilityKeys = Object.keys(state).filter((key) => key.startsWith(VISIBILITY_PREFIX));

for (const key of visibilityKeys) {
visibilities.set(key.slice(VISIBILITY_PREFIX.length), state[key] === 'true');
}
return visibilities;
}, [schema.metadata, state]);

const fieldValues = useMemo(() => {
const fieldKeys = Object.keys(state)
.filter((key) => !key.startsWith(VISIBILITY_PREFIX))
.filter((key) => !key.startsWith(VISIBILITY_PREFIX) && !key.startsWith(COLUMN_VISIBILITY_PREFIX))
.filter((key) => key !== orderKey && key !== orderDirectionKey);

const values: Record<string, any> = { ...hiddenFieldValues };
Expand All @@ -138,7 +174,7 @@ export const InnerSearchFullUI = ({
setPage(1);
};

const setAVisibility = (fieldName: string, visible: boolean) => {
const setASearchVisibility = (fieldName: string, visible: boolean) => {
setState((prev: any) => ({
...prev,
[`${VISIBILITY_PREFIX}${fieldName}`]: visible ? 'true' : 'false',
Expand All @@ -149,6 +185,13 @@ export const InnerSearchFullUI = ({
}
};

const setAColumnVisibility = (fieldName: string, visible: boolean) => {
setState((prev: any) => ({
...prev,
[`${COLUMN_VISIBILITY_PREFIX}${fieldName}`]: visible ? 'true' : 'false',
}));
};

const lapisUrl = getLapisUrl(clientConfig, organism);

const consolidatedMetadataSchema = consolidateGroupedFields(metadataSchemaWithExpandedRanges);
Expand Down Expand Up @@ -201,7 +244,7 @@ export const InnerSearchFullUI = ({
// @ts-expect-error because the hooks don't accept OrderBy
detailsHook.mutate({
...lapisSearchParameters,
fields: [...schema.tableColumns, schema.primaryKey],
fields: [...columnsToShow, schema.primaryKey],
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: OrderByList,
Expand All @@ -228,6 +271,21 @@ export const InnerSearchFullUI = ({

return (
<div className='flex flex-col md:flex-row gap-8 md:gap-4'>
<CustomizeModal
thingToCustomize='column'
isCustomizeModalOpen={isColumnModalOpen}
toggleCustomizeModal={() => setIsColumnModalOpen(!isColumnModalOpen)}
alwaysPresentFieldNames={[]}
visibilities={columnVisibilities}
setAVisibility={setAColumnVisibility}
nameToLabelMap={consolidatedMetadataSchema.reduce(
(acc, field) => {
acc[field.name] = field.displayName ?? field.label ?? sentenceCase(field.name);
return acc;
},
{} as Record<string, string>,
)}
/>
<SeqPreviewModal
seqId={previewedSeqId ?? ''}
accessToken={accessToken}
Expand All @@ -248,8 +306,8 @@ export const InnerSearchFullUI = ({
setAFieldValue={setAFieldValue}
consolidatedMetadataSchema={consolidatedMetadataSchema}
lapisUrl={lapisUrl}
visibilities={visibilities}
setAVisibility={setAVisibility}
searchVisibilities={searchVisibilities}
setASearchVisibility={setASearchVisibility}
lapisSearchParameters={lapisSearchParameters}
/>
</div>
Expand Down Expand Up @@ -296,13 +354,21 @@ export const InnerSearchFullUI = ({
<span className='loading loading-spinner loading-xs ml-3 appearSlowly'></span>
) : null}
</div>

<DownloadDialog
lapisUrl={lapisUrl}
lapisSearchParameters={lapisSearchParameters}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
hiddenFieldValues={hiddenFieldValues}
/>
<div className='flex'>
<button
className='text-gray-800 hover:text-gray-600 mr-4 underline text-primary-700 hover:text-primary-500'
onClick={() => setIsColumnModalOpen(true)}
>
Customize columns
</button>

<DownloadDialog
lapisUrl={lapisUrl}
lapisSearchParameters={lapisSearchParameters}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
hiddenFieldValues={hiddenFieldValues}
/>
</div>
</div>

<Table
Expand All @@ -322,6 +388,7 @@ export const InnerSearchFullUI = ({
}
setOrderByField={setOrderByField}
setOrderDirection={setOrderDirection}
columnsToShow={columnsToShow}
/>

<div className='mt-4 flex justify-center'>
Expand Down
4 changes: 3 additions & 1 deletion website/src/components/SearchPage/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type TableProps = {
orderBy: OrderBy;
setOrderByField: (field: string) => void;
setOrderDirection: (direction: 'ascending' | 'descending') => void;
columnsToShow: string[];
};

export const Table: FC<TableProps> = ({
Expand All @@ -30,12 +31,13 @@ export const Table: FC<TableProps> = ({
orderBy,
setOrderByField,
setOrderDirection,
columnsToShow,
}) => {
const primaryKey = schema.primaryKey;

const maxLengths = Object.fromEntries(schema.metadata.map((m) => [m.name, m.truncateColumnDisplayTo ?? 100]));

const columns = schema.tableColumns.map((field) => ({
const columns = columnsToShow.map((field) => ({
field,
headerName: schema.metadata.find((m) => m.name === field)?.displayName ?? capitalCase(field),
maxLength: maxLengths[field],
Expand Down
Loading