Skip to content

Commit

Permalink
refactor(website): refactor search by extracting re-usable functions (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
theosanderson authored Jul 12, 2024
1 parent a9b19b8 commit 0069ec0
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 230 deletions.
106 changes: 16 additions & 90 deletions website/src/components/SearchPage/SearchFullUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { SearchForm } from './SearchForm';
import { SearchPagination } from './SearchPagination';
import { SeqPreviewModal } from './SeqPreviewModal';
import { Table, type TableSequenceData } from './Table';
import { parseMutationString } from './fields/MutationField.tsx';
import useQueryAsState from './useQueryAsState.js';
import { getLapisUrl } from '../../config.ts';
import { lapisClientHooks } from '../../services/serviceHooks.ts';
Expand All @@ -25,12 +24,15 @@ import {
import { type OrderBy } from '../../types/lapis.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
const orderKey = 'orderBy';
const orderDirectionKey = 'order';

const VISIBILITY_PREFIX = 'visibility_';

const COLUMN_VISIBILITY_PREFIX = 'column_';
import {
getFieldValuesFromQuery,
getColumnVisibilitiesFromQuery,
getFieldVisibilitiesFromQuery,
VISIBILITY_PREFIX,
COLUMN_VISIBILITY_PREFIX,
getLapisSearchParameters,
} from '../../utils/search.ts';
import ErrorBox from '../common/ErrorBox.tsx';

interface InnerSearchFullUIProps {
accessToken?: string;
Expand Down Expand Up @@ -94,39 +96,12 @@ export const InnerSearchFullUI = ({
const [page, setPage] = useState(1);

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]);
return getFieldVisibilitiesFromQuery(schema, state);
}, [schema, 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]);
return getColumnVisibilitiesFromQuery(schema, state);
}, [schema, state]);

const columnsToShow = useMemo(() => {
return schema.metadata
Expand Down Expand Up @@ -155,15 +130,7 @@ export const InnerSearchFullUI = ({
};

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

const values: Record<string, any> = { ...hiddenFieldValues };
for (const key of fieldKeys) {
values[key] = state[key];
}
return values;
return getFieldValuesFromQuery(state, hiddenFieldValues);
}, [state, hiddenFieldValues]);

const setAFieldValue: SetAFieldValue = (fieldName, value) => {
Expand Down Expand Up @@ -207,33 +174,7 @@ export const InnerSearchFullUI = ({
const detailsHook = hooks.useDetails({}, {});

const lapisSearchParameters = useMemo(() => {
const sequenceFilters = Object.fromEntries(
Object.entries(fieldValues).filter(([, value]) => value !== undefined && value !== ''),
);

if (sequenceFilters.accession !== '' && sequenceFilters.accession !== undefined) {
sequenceFilters.accession = textAccessionsToList(sequenceFilters.accession);
}

delete sequenceFilters.mutation;

const mutationFilter = parseMutationString(fieldValues.mutation ?? '', referenceGenomesSequenceNames);

return {
...sequenceFilters,
nucleotideMutations: mutationFilter
.filter((m) => m.baseType === 'nucleotide' && m.mutationType === 'substitutionOrDeletion')
.map((m) => m.text),
aminoAcidMutations: mutationFilter
.filter((m) => m.baseType === 'aminoAcid' && m.mutationType === 'substitutionOrDeletion')
.map((m) => m.text),
nucleotideInsertions: mutationFilter
.filter((m) => m.baseType === 'nucleotide' && m.mutationType === 'insertion')
.map((m) => m.text),
aminoAcidInsertions: mutationFilter
.filter((m) => m.baseType === 'aminoAcid' && m.mutationType === 'insertion')
.map((m) => m.text),
};
return getLapisSearchParameters(fieldValues, referenceGenomesSequenceNames);
}, [fieldValues, referenceGenomesSequenceNames]);

useEffect(() => {
Expand Down Expand Up @@ -338,7 +279,7 @@ export const InnerSearchFullUI = ({
))}
{(detailsHook.isPaused || aggregatedHook.isPaused) &&
(!detailsHook.isSuccess || !aggregatedHook.isSuccess) && (
<div className='bg-red-800'>Connection problem</div>
<ErrorBox title='Connection problem'>Please check your internet connection</ErrorBox>
)}
{!(totalSequences === undefined && oldCount === null) && (
<div
Expand Down Expand Up @@ -450,18 +391,3 @@ export const SearchFullUI = (props: InnerSearchFullUIProps) => {
</QueryClientProvider>
);
};

const textAccessionsToList = (text: string): string[] => {
const accessions = text
.split(/[\t,;\n ]/)
.map((s) => s.trim())
.filter((s) => s !== '')
.map((s) => {
if (s.includes('.')) {
return s.split('.')[0];
}
return s;
});

return accessions;
};
136 changes: 9 additions & 127 deletions website/src/components/SearchPage/fields/MutationField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { type FC, Fragment, useMemo, useState } from 'react';
import * as React from 'react';

import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';
import type { BaseType } from '../../../utils/sequenceTypeHelpers.ts';
import {
parseMutationString,
isValidAminoAcidMutationQuery,
isValidAminoAcidInsertionQuery,
isValidNucleotideInsertionQuery,
isValidNucleotideMutationQuery,
type MutationQuery,
} from '../../../utils/search.ts';
import DisplaySearchDocs from '../DisplaySearchDocs';

interface MutationFieldProps {
Expand All @@ -12,131 +19,6 @@ interface MutationFieldProps {
onChange: (mutationFilter: string) => void;
}

type MutationQuery = {
baseType: BaseType;
mutationType: 'substitutionOrDeletion' | 'insertion';
text: string;
};

const isValidNucleotideMutationQuery = (
text: string,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
): boolean => {
try {
const isMultiSegmented = referenceGenomesSequenceNames.nucleotideSequences.length > 1;
const textUpper = text.toUpperCase();
let mutation = textUpper;
if (isMultiSegmented) {
const [segment, _mutation] = textUpper.split(':');
const existingSegments = new Set(
referenceGenomesSequenceNames.nucleotideSequences.map((n) => n.toUpperCase()),
);
if (!existingSegments.has(segment)) {
return false;
}
mutation = _mutation;
}
return /^[A-Z]?[0-9]+[A-Z-\\.]?$/.test(mutation);
} catch (_) {
return false;
}
};

const isValidAminoAcidMutationQuery = (
text: string,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
): boolean => {
try {
const textUpper = text.toUpperCase();
const [gene, mutation] = textUpper.split(':');
const existingGenes = new Set(referenceGenomesSequenceNames.genes.map((g) => g.toUpperCase()));
if (!existingGenes.has(gene)) {
return false;
}
return /^[A-Z*]?[0-9]+[A-Z-*\\.]?$/.test(mutation);
} catch (_) {
return false;
}
};

const isValidNucleotideInsertionQuery = (
text: string,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
): boolean => {
try {
const isMultiSegmented = referenceGenomesSequenceNames.nucleotideSequences.length > 1;
const textUpper = text.toUpperCase();
if (!textUpper.startsWith('INS_')) {
return false;
}
const query = textUpper.slice(4);
const split = query.split(':');
const [segment, position, insertion] = isMultiSegmented
? split
: ([undefined, ...split] as [undefined | string, string, string]);
if (segment !== undefined) {
const existingSegments = new Set(
referenceGenomesSequenceNames.nucleotideSequences.map((n) => n.toUpperCase()),
);
if (!existingSegments.has(segment)) {
return false;
}
}
if (!Number.isInteger(Number(position))) {
return false;
}
return /^[A-Z*?]+$/.test(insertion);
} catch (_) {
return false;
}
};

const isValidAminoAcidInsertionQuery = (
text: string,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
): boolean => {
try {
const textUpper = text.toUpperCase();
if (!textUpper.startsWith('INS_')) {
return false;
}
const query = textUpper.slice(4);
const [gene, position, insertion] = query.split(':');
const existingGenes = new Set(referenceGenomesSequenceNames.genes.map((g) => g.toUpperCase()));
if (!existingGenes.has(gene) || !Number.isInteger(Number(position))) {
return false;
}
return /^[A-Z*?]+$/.test(insertion);
} catch (_) {
return false;
}
};

export const parseMutationString = (
value: string,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
): MutationQuery[] => {
return value
.split(',')
.map((mutation) => {
const trimmedMutation = mutation.trim();
if (isValidNucleotideMutationQuery(trimmedMutation, referenceGenomesSequenceNames)) {
return { baseType: 'nucleotide', mutationType: 'substitutionOrDeletion', text: trimmedMutation };
}
if (isValidAminoAcidMutationQuery(trimmedMutation, referenceGenomesSequenceNames)) {
return { baseType: 'aminoAcid', mutationType: 'substitutionOrDeletion', text: trimmedMutation };
}
if (isValidNucleotideInsertionQuery(trimmedMutation, referenceGenomesSequenceNames)) {
return { baseType: 'nucleotide', mutationType: 'insertion', text: trimmedMutation };
}
if (isValidAminoAcidInsertionQuery(trimmedMutation, referenceGenomesSequenceNames)) {
return { baseType: 'aminoAcid', mutationType: 'insertion', text: trimmedMutation };
}
return null;
})
.filter(Boolean) as MutationQuery[];
};

const serializeMutationQueries = (selectedOptions: MutationQuery[]): string => {
return selectedOptions.map((option) => option.text).join(', ');
};
Expand All @@ -162,7 +44,7 @@ export const MutationField: FC<MutationFieldProps> = ({ referenceGenomesSequence
{ baseType: 'aminoAcid', mutationType: 'insertion', test: isValidAminoAcidInsertionQuery },
] as const;
tests.forEach(({ baseType, mutationType, test }) => {
if (test(newValue, referenceGenomesSequenceNames)) {
if (test(newValue, referenceGenomesSequenceNames) === true) {
newOptions.push({ baseType, mutationType, text: newValue });
}
});
Expand Down
9 changes: 5 additions & 4 deletions website/src/pages/[organism]/search/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { siloVersionStatuses } from '../../../types/lapis';
import { getAccessToken } from '../../../utils/getAccessToken';
import { getMyGroups } from '../../../utils/getMyGroups';
import { getReferenceGenomesSequenceNames } from '../../../utils/search';
const hiddenFieldValues = {
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[IS_REVOCATION_FIELD]: 'false',
};
const { organism: cleanedOrganism } = cleanOrganism(Astro.params.organism);
Expand Down Expand Up @@ -38,9 +42,6 @@ const referenceGenomeSequenceNames = getReferenceGenomesSequenceNames(cleanedOrg
myGroups={myGroups}
accessToken={accessToken}
referenceGenomesSequenceNames={referenceGenomeSequenceNames}
hiddenFieldValues={{
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[IS_REVOCATION_FIELD]: 'false',
}}
hiddenFieldValues={hiddenFieldValues}
/>
</BaseLayout>
10 changes: 6 additions & 4 deletions website/src/pages/[organism]/submission/[groupId]/released.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const schema = getSchema(cleanedOrganism.key);
const accessToken = getAccessToken(Astro.locals.session);
const referenceGenomeSequenceNames = getReferenceGenomesSequenceNames(cleanedOrganism.key);
const hiddenFieldValues = {
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[GROUP_ID_FIELD]: group.groupId,
};
---

<SubmissionPageWrapper groupsResult={groupsResult} title='Released sequences'>
Expand All @@ -40,9 +45,6 @@ const referenceGenomeSequenceNames = getReferenceGenomesSequenceNames(cleanedOrg
myGroups={[group]}
accessToken={accessToken}
referenceGenomesSequenceNames={referenceGenomeSequenceNames}
hiddenFieldValues={{
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[GROUP_ID_FIELD]: group.groupId,
}}
hiddenFieldValues={hiddenFieldValues}
/>
</SubmissionPageWrapper>
Loading

0 comments on commit 0069ec0

Please sign in to comment.