diff --git a/website/src/components/pageStateSelectors/ApplyFilterButton.browser.spec.tsx b/website/src/components/pageStateSelectors/ApplyFilterButton.browser.spec.tsx index 128a0927..4fd115d1 100644 --- a/website/src/components/pageStateSelectors/ApplyFilterButton.browser.spec.tsx +++ b/website/src/components/pageStateSelectors/ApplyFilterButton.browser.spec.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import { act } from '@testing-library/react'; +import React, { useState } from 'react'; import { describe, expect } from 'vitest'; -import { render } from 'vitest-browser-react'; +import { render, renderHook, type RenderResult } from 'vitest-browser-react'; import { ApplyFilterButton } from './ApplyFilterButton'; import { it } from '../../../test-extend'; @@ -29,69 +30,91 @@ const urlTooLongMessage = /URL is too long/i; describe('ApplyFilterButton', () => { const handler = new DummyPageStateHandler(); - it('should render enabled link for short URL', () => { + it('should render enabled link for short URL', async () => { const shortState: DummyPageState = { data: 'short' }; - const { getByRole, container } = render( - , + const { result: state } = renderHook(() => useState({ data: 'initial' })); + + const { getByRole } = render( + , ); - const link = getByRole('button'); - expect(link.element()).toBeInTheDocument(); - expect(link.element()).toHaveAttribute('href', '/test?data=short'); - expect(container.textContent).not.toMatch(urlTooLongMessage); + await clickApply(getByRole); + + expect(state.current[0].data).toEqual('short'); }); - it('should render disabled span and error message for long URL', () => { + it('should render disabled span and error message for long URL', async () => { const longData = 'x'.repeat(2000); const longState: DummyPageState = { data: longData }; + const { result: state } = renderHook(() => useState({ data: 'initial' })); + const { getByRole, container } = render( - , + , ); - const button = getByRole('button'); - expect(button.element()).not.toHaveAttribute('href'); expect(container.textContent).toMatch(urlTooLongMessage); + expect(getByRole('button', { name: 'Apply filters' }).element()).not.toHaveAttribute('onClick'); + + await clickApply(getByRole); + expect(state.current[0].data).toEqual('initial'); }); - it('should update when state changes from short to long', () => { + it('should update when state changes from short to long', async () => { const shortState: DummyPageState = { data: 'short' }; + const { result: state } = renderHook(() => useState({ data: 'initial' })); + const { getByRole, container, rerender } = render( - , + , ); - const button = getByRole('button'); - expect(button.element()).toHaveAttribute('href', '/test?data=short'); + await clickApply(getByRole); + expect(state.current[0].data).toEqual('short'); expect(container.textContent).not.toMatch(urlTooLongMessage); const longData = 'x'.repeat(2000); const longState: DummyPageState = { data: longData }; - rerender(); + rerender( + , + ); - const updatedButton = getByRole('button'); - expect(updatedButton.element()).not.toHaveAttribute('href'); expect(container.textContent).toMatch(urlTooLongMessage); + expect(getByRole('button', { name: 'Apply filters' }).element()).not.toHaveAttribute('onClick'); + + await clickApply(getByRole); + expect(state.current[0].data).toEqual('short'); }); - it('should update when state changes from long to short', () => { + it('should update when state changes from long to short', async () => { const longData = 'x'.repeat(2000); const longState: DummyPageState = { data: longData }; + const { result: state } = renderHook(() => useState({ data: 'initial' })); + const { getByRole, container, rerender } = render( - , + , ); - const button = getByRole('button'); - expect(button.element()).not.toHaveAttribute('href'); + expect(getByRole('button', { name: 'Apply filters' }).element()).not.toHaveAttribute('onClick'); expect(container.textContent).toMatch(urlTooLongMessage); + await clickApply(getByRole); + expect(state.current[0].data).toEqual('initial'); const shortState: DummyPageState = { data: 'short' }; - rerender(); + rerender( + , + ); - const updatedButton = getByRole('button'); - expect(updatedButton.element()).toHaveAttribute('href', '/test?data=short'); expect(container.textContent).not.toMatch(urlTooLongMessage); + await clickApply(getByRole); + expect(state.current[0].data).toEqual('short'); }); }); + +async function clickApply(getByRole: RenderResult['getByRole']) { + await act(async () => { + await getByRole('button', { name: 'Apply filters' }).click(); + }); +} diff --git a/website/src/components/pageStateSelectors/ApplyFilterButton.tsx b/website/src/components/pageStateSelectors/ApplyFilterButton.tsx index d01b61b8..d3d7b573 100644 --- a/website/src/components/pageStateSelectors/ApplyFilterButton.tsx +++ b/website/src/components/pageStateSelectors/ApplyFilterButton.tsx @@ -1,3 +1,5 @@ +import type { Dispatch, SetStateAction } from 'react'; + import type { WithClassName } from '../../types/WithClassName.ts'; import type { PageStateHandler } from '../../views/pageStateHandlers/PageStateHandler.ts'; @@ -8,15 +10,21 @@ const MAX_URL_LENGTH = 2000; export function ApplyFilterButton({ pageStateHandler, newPageState, + setPageState, className = '', }: WithClassName<{ pageStateHandler: PageStateHandler; newPageState: PageState; + setPageState: Dispatch>; }>) { const url = pageStateHandler.toUrl(newPageState); const fullUrl = `${window.location.origin}${url}`; const urlTooLong = fullUrl.length > MAX_URL_LENGTH; + const applyFilters = () => { + setPageState(newPageState); + }; + return urlTooLong ? ( <> @@ -30,8 +38,8 @@ export function ApplyFilterButton({ ) : ( - + ); } diff --git a/website/src/components/pageStateSelectors/CompareSideBySidePageStateSelector.tsx b/website/src/components/pageStateSelectors/CompareSideBySidePageStateSelector.tsx index a71cfc05..f63e329c 100644 --- a/website/src/components/pageStateSelectors/CompareSideBySidePageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/CompareSideBySidePageStateSelector.tsx @@ -1,37 +1,38 @@ -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'; import { ApplyFilterButton } from './ApplyFilterButton.tsx'; import { BaselineSelector } from './BaselineSelector.tsx'; import { SelectorHeadline } from './SelectorHeadline.tsx'; import { makeVariantFilterConfig, VariantSelector } from './VariantSelector.tsx'; -import type { OrganismsConfig } from '../../config.ts'; import { Inset } from '../../styles/Inset.tsx'; +import { BaseView } from '../../views/BaseView.ts'; +import type { OrganismConstants } from '../../views/OrganismConstants.ts'; import type { CompareSideBySideData } from '../../views/View.ts'; -import { type OrganismViewKey, Routing } from '../../views/routing.ts'; -import type { compareSideBySideViewKey } from '../../views/viewKeys.ts'; +import { CompareSideBySideStateHandler } from '../../views/pageStateHandlers/CompareSideBySidePageStateHandler.ts'; export function CompareSideBySidePageStateSelector({ + view, filterId, - initialPageState, - organismViewKey, - organismsConfig, + pageState, + setPageState, enableAdvancedQueryFilter, }: { + view: BaseView; filterId: number; - initialPageState: CompareSideBySideData; - organismViewKey: OrganismViewKey & `${string}.${typeof compareSideBySideViewKey}`; - organismsConfig: OrganismsConfig; + pageState: CompareSideBySideData; + setPageState: Dispatch>; enableAdvancedQueryFilter: boolean; }) { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); + const [draftPageState, setDraftPageState] = useState(pageState); + useEffect(() => setDraftPageState(pageState), [pageState]); + const variantFilterConfig = useMemo( () => makeVariantFilterConfig(view.organismConstants), [view.organismConstants], ); - const [pageState, setPageState] = useState(initialPageState); const { filterOfCurrentId, currentLapisFilter } = useMemo(() => { - const filterOfCurrentId = pageState.filters.get(filterId) ?? { + const filterOfCurrentId = draftPageState.filters.get(filterId) ?? { datasetFilter: { locationFilters: {}, dateFilters: {}, @@ -47,7 +48,7 @@ export function CompareSideBySidePageStateSelector({ filterOfCurrentId.variantFilter, ), }; - }, [pageState, filterId, view.pageStateHandler]); + }, [draftPageState, filterId, view.pageStateHandler]); return (
@@ -60,8 +61,8 @@ export function CompareSideBySidePageStateSelector({ lapisFilter={currentLapisFilter} datasetFilter={filterOfCurrentId.datasetFilter} setDatasetFilter={(newDatasetFilter) => { - setPageState((previousState) => { - const updatedFilters = new Map(initialPageState.filters); + setDraftPageState((previousState) => { + const updatedFilters = new Map(pageState.filters); updatedFilters.set(filterId, { ...filterOfCurrentId, datasetFilter: newDatasetFilter, @@ -78,8 +79,8 @@ export function CompareSideBySidePageStateSelector({ { - setPageState((previousState) => { - const updatedFilters = new Map(initialPageState.filters); + setDraftPageState((previousState) => { + const updatedFilters = new Map(pageState.filters); updatedFilters.set(filterId, { ...filterOfCurrentId, variantFilter: newVariantFilter, @@ -96,7 +97,11 @@ export function CompareSideBySidePageStateSelector({
- +
); diff --git a/website/src/components/pageStateSelectors/CompareVariantsPageStateSelector.tsx b/website/src/components/pageStateSelectors/CompareVariantsPageStateSelector.tsx index cbfd106a..0d03db9c 100644 --- a/website/src/components/pageStateSelectors/CompareVariantsPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/CompareVariantsPageStateSelector.tsx @@ -1,39 +1,38 @@ -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'; import { ApplyFilterButton } from './ApplyFilterButton.tsx'; import { BaselineSelector } from './BaselineSelector.tsx'; import { SelectorHeadline } from './SelectorHeadline.tsx'; import { makeVariantFilterConfig } from './VariantSelector.tsx'; import { VariantsSelector } from './VariantsSelector.tsx'; -import { type OrganismsConfig } from '../../config.ts'; import { Inset } from '../../styles/Inset.tsx'; +import { GenericCompareVariantsView } from '../../views/BaseView.ts'; +import type { OrganismConstants } from '../../views/OrganismConstants.ts'; import type { CompareVariantsData, Id, VariantFilter } from '../../views/View.ts'; -import { type OrganismViewKey, Routing } from '../../views/routing.ts'; -import type { compareVariantsViewKey } from '../../views/viewKeys.ts'; export function CompareVariantsPageStateSelector({ - organismViewKey, - organismsConfig, - initialPageState, + view, + pageState, + setPageState, enableAdvancedQueryFilter, }: { - organismViewKey: OrganismViewKey & `${string}.${typeof compareVariantsViewKey}`; - organismsConfig: OrganismsConfig; - initialPageState: CompareVariantsData; + view: GenericCompareVariantsView; + pageState: CompareVariantsData; + setPageState: Dispatch>; enableAdvancedQueryFilter: boolean; }) { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); - const [pageState, setPageState] = useState(initialPageState); + const [draftPageState, setDraftPageState] = useState(pageState); + useEffect(() => setDraftPageState(pageState), [pageState]); const variantFilterConfigs = useMemo(() => { return new Map( - pageState.variants.entries().map(([id]) => [id, makeVariantFilterConfig(view.organismConstants)]), + draftPageState.variants.entries().map(([id]) => [id, makeVariantFilterConfig(view.organismConstants)]), ); - }, [pageState.variants, view.organismConstants]); + }, [draftPageState.variants, view.organismConstants]); const currentLapisFilter = useMemo(() => { - return view.pageStateHandler.datasetFilterToLapisFilter(pageState.datasetFilter); - }, [pageState, view.pageStateHandler]); + return view.pageStateHandler.datasetFilterToLapisFilter(draftPageState.datasetFilter); + }, [draftPageState, view.pageStateHandler]); return (
@@ -43,9 +42,9 @@ export function CompareVariantsPageStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, datasetFilter: newDatasetFilter, })); @@ -58,10 +57,10 @@ export function CompareVariantsPageStateSelector({ Variant Filters ) => { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, variants: newVariantFilters, })); @@ -75,7 +74,8 @@ export function CompareVariantsPageStateSelector({
diff --git a/website/src/components/pageStateSelectors/CompareVariantsToBaselineStateSelector.tsx b/website/src/components/pageStateSelectors/CompareVariantsToBaselineStateSelector.tsx index 5aa4cb49..d57cb6bc 100644 --- a/website/src/components/pageStateSelectors/CompareVariantsToBaselineStateSelector.tsx +++ b/website/src/components/pageStateSelectors/CompareVariantsToBaselineStateSelector.tsx @@ -1,29 +1,28 @@ -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'; import { ApplyFilterButton } from './ApplyFilterButton.tsx'; import { BaselineSelector } from './BaselineSelector.tsx'; import { SelectorHeadline } from './SelectorHeadline.tsx'; import { makeVariantFilterConfig, VariantSelector } from './VariantSelector.tsx'; import { VariantsSelector } from './VariantsSelector.tsx'; -import { type OrganismsConfig } from '../../config.ts'; import { Inset } from '../../styles/Inset.tsx'; +import { GenericCompareToBaselineView } from '../../views/BaseView.ts'; +import type { OrganismConstants } from '../../views/OrganismConstants.ts'; import type { CompareToBaselineData, Id, VariantFilter } from '../../views/View.ts'; -import { type OrganismViewKey, Routing } from '../../views/routing.ts'; -import { type compareToBaselineViewKey } from '../../views/viewKeys.ts'; export function CompareVariantsToBaselineStateSelector({ - organismViewKey, - organismsConfig, - initialPageState, + view, + pageState, + setPageState, enableAdvancedQueryFilter, }: { - organismViewKey: OrganismViewKey & `${string}.${typeof compareToBaselineViewKey}`; - organismsConfig: OrganismsConfig; - initialPageState: CompareToBaselineData; + view: GenericCompareToBaselineView; + pageState: CompareToBaselineData; + setPageState: Dispatch>; enableAdvancedQueryFilter: boolean; }) { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); - const [pageState, setPageState] = useState(initialPageState); + const [draftPageState, setDraftPageState] = useState(pageState); + useEffect(() => setDraftPageState(pageState), [pageState]); const variantFilterConfig = useMemo( () => makeVariantFilterConfig(view.organismConstants), @@ -31,12 +30,12 @@ export function CompareVariantsToBaselineStateSelector({ ); const variantFilterConfigs = useMemo(() => { - return new Map(pageState.variants.entries().map(([id]) => [id, { ...variantFilterConfig }])); - }, [pageState.variants, variantFilterConfig]); + return new Map(draftPageState.variants.entries().map(([id]) => [id, { ...variantFilterConfig }])); + }, [draftPageState.variants, variantFilterConfig]); const currentLapisFilter = useMemo(() => { - return view.pageStateHandler.baselineFilterToLapisFilter(pageState); - }, [pageState, view.pageStateHandler]); + return view.pageStateHandler.baselineFilterToLapisFilter(draftPageState); + }, [draftPageState, view.pageStateHandler]); return (
@@ -47,9 +46,9 @@ export function CompareVariantsToBaselineStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, datasetFilter: newDatasetFilter, })); @@ -64,13 +63,13 @@ export function CompareVariantsToBaselineStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, baselineFilter: newVariantFilter, })); }} variantFilterConfig={variantFilterConfig} - variantFilter={pageState.baselineFilter} + variantFilter={draftPageState.baselineFilter} lapisFilter={currentLapisFilter} enableAdvancedQueryFilter={enableAdvancedQueryFilter} /> @@ -81,10 +80,10 @@ export function CompareVariantsToBaselineStateSelector({ Variant Filters ) => { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, variants: newVariantFilters, })); @@ -98,7 +97,8 @@ export function CompareVariantsToBaselineStateSelector({
diff --git a/website/src/components/pageStateSelectors/SequencingEffortsPageStateSelector.tsx b/website/src/components/pageStateSelectors/SequencingEffortsPageStateSelector.tsx index ee58b8bc..e095837a 100644 --- a/website/src/components/pageStateSelectors/SequencingEffortsPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/SequencingEffortsPageStateSelector.tsx @@ -1,33 +1,31 @@ -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'; import { ApplyFilterButton } from './ApplyFilterButton.tsx'; import { BaselineSelector } from './BaselineSelector.tsx'; import { LineageFilterInput } from './LineageFilterInput.tsx'; import { SelectorHeadline } from './SelectorHeadline.tsx'; -import { type OrganismsConfig } from '../../config.ts'; import { Inset } from '../../styles/Inset.tsx'; +import { GenericSequencingEffortsView } from '../../views/BaseView.ts'; +import type { OrganismConstants } from '../../views/OrganismConstants.ts'; import type { DatasetAndVariantData } from '../../views/View.ts'; -import { type OrganismViewKey, Routing } from '../../views/routing.ts'; -import type { sequencingEffortsViewKey } from '../../views/viewKeys.ts'; export function SequencingEffortsPageStateSelector({ - organismViewKey, - organismsConfig, - initialPageState, + view, + pageState, + setPageState, enableAdvancedQueryFilter, }: { - organismViewKey: OrganismViewKey & `${string}.${typeof sequencingEffortsViewKey}`; - organismsConfig: OrganismsConfig; - initialPageState: DatasetAndVariantData; + view: GenericSequencingEffortsView; + pageState: DatasetAndVariantData; + setPageState: Dispatch>; enableAdvancedQueryFilter: boolean; }) { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); - - const [pageState, setPageState] = useState(initialPageState); + const [draftPageState, setDraftPageState] = useState(pageState); + useEffect(() => setDraftPageState(pageState), [pageState]); const currentLapisFilter = useMemo(() => { - return view.pageStateHandler.toLapisFilter(pageState); - }, [pageState, view.pageStateHandler]); + return view.pageStateHandler.toLapisFilter(draftPageState); + }, [draftPageState, view.pageStateHandler]); return (
@@ -37,9 +35,9 @@ export function SequencingEffortsPageStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, datasetFilter: newDatasetFilter, })); @@ -50,7 +48,7 @@ export function SequencingEffortsPageStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, variantFilter: { lineages: { @@ -62,7 +60,7 @@ export function SequencingEffortsPageStateSelector({ }} key={lineageFilterConfig.lapisField} lapisFilter={currentLapisFilter} - value={pageState.variantFilter.lineages?.[lineageFilterConfig.lapisField]} + value={draftPageState.variantFilter.lineages?.[lineageFilterConfig.lapisField]} /> ))} @@ -71,7 +69,8 @@ export function SequencingEffortsPageStateSelector({
diff --git a/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.browser.spec.tsx b/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.browser.spec.tsx index b3e4d6b0..ad9f50fa 100644 --- a/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.browser.spec.tsx +++ b/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.browser.spec.tsx @@ -1,40 +1,51 @@ +import { act } from '@testing-library/react'; +import { useState } from 'react'; import { describe, expect } from 'vitest'; -import { render } from 'vitest-browser-react'; +import { render, renderHook } from 'vitest-browser-react'; import { SingleVariantPageStateSelector } from './SingleVariantPageStateSelector.tsx'; import { DUMMY_LAPIS_URL, testOrganismsConfig } from '../../../routeMocker.ts'; import { it } from '../../../test-extend'; +import type { CovidVariantData } from '../../views/covid.ts'; +import { Routing } from '../../views/routing.ts'; describe('SingleVariantPageStateSelector', () => { - it('should remember the covid collection id', ({ routeMockers }) => { + it('should remember the covid collection id', async ({ routeMockers }) => { routeMockers.lapis.mockPostAggregated({}, { data: [] }); routeMockers.lapis.mockReferenceGenome({ nucleotideSequences: [{ name: 'main', sequence: 'ATGC' }], genes: [], }); + routeMockers.lapis.mockLineageDefinition('nextcladePangoLineage', {}); - const initialPageState = { - datasetFilter: { - locationFilters: {}, - dateFilters: {}, - textFilters: {}, - numberFilters: {}, - }, - variantFilter: {}, - collectionId: 5, - }; + const { result } = renderHook(() => + useState({ + datasetFilter: { + locationFilters: {}, + dateFilters: {}, + textFilters: {}, + numberFilters: {}, + }, + variantFilter: {}, + collectionId: 5, + }), + ); const { getByRole } = render( , ); - expect(getByRole('button').element()).toHaveAttribute('href', expect.stringContaining('collectionId=5')); + await act(async () => { + await getByRole('button', { name: 'Apply filters' }).click(); + }); + + expect(result.current[0].collectionId).equals(5); }); }); diff --git a/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.tsx b/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.tsx index d9a75c96..cc40f5e0 100644 --- a/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/SingleVariantPageStateSelector.tsx @@ -1,38 +1,38 @@ -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'; import { ApplyFilterButton } from './ApplyFilterButton.tsx'; import { BaselineSelector } from './BaselineSelector.tsx'; import { SelectorHeadline } from './SelectorHeadline.tsx'; import { makeVariantFilterConfig, VariantSelector } from './VariantSelector.tsx'; -import { type OrganismsConfig } from '../../config.ts'; import { Inset } from '../../styles/Inset.tsx'; +import type { GenericSingleVariantView } from '../../views/BaseView.ts'; +import type { OrganismConstants } from '../../views/OrganismConstants.ts'; import { type DatasetAndVariantData } from '../../views/View.ts'; -import { type OrganismViewKey, Routing } from '../../views/routing.ts'; -import type { singleVariantViewKey } from '../../views/viewKeys.ts'; export function SingleVariantPageStateSelector({ - organismViewKey, - organismsConfig, - initialPageState, + view, + pageState, + setPageState, enableAdvancedQueryFilter, }: { - organismViewKey: OrganismViewKey & `${string}.${typeof singleVariantViewKey}`; - organismsConfig: OrganismsConfig; - initialPageState: DatasetAndVariantData; + view: GenericSingleVariantView; + pageState: DatasetAndVariantData; + setPageState: Dispatch>; enableAdvancedQueryFilter: boolean; }) { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); const variantFilterConfig = useMemo( () => makeVariantFilterConfig(view.organismConstants), [view.organismConstants], ); - const [pageState, setPageState] = useState(initialPageState); + + const [draftPageState, setDraftPageState] = useState(pageState); + useEffect(() => setDraftPageState(pageState), [pageState]); const baselineFilterConfigs = view.organismConstants.baselineFilterConfigs; const currentLapisFilter = useMemo(() => { - return view.pageStateHandler.toLapisFilter(pageState); - }, [pageState, view.pageStateHandler]); + return view.pageStateHandler.toLapisFilter(draftPageState); + }, [draftPageState, view.pageStateHandler]); return (
@@ -42,9 +42,9 @@ export function SingleVariantPageStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, datasetFilter: newDatasetFilter, })); @@ -59,13 +59,13 @@ export function SingleVariantPageStateSelector({ { - setPageState((previousState) => ({ + setDraftPageState((previousState) => ({ ...previousState, variantFilter: newVariantFilter, })); }} variantFilterConfig={variantFilterConfig} - variantFilter={pageState.variantFilter} + variantFilter={draftPageState.variantFilter} lapisFilter={currentLapisFilter} enableAdvancedQueryFilter={enableAdvancedQueryFilter} /> @@ -76,7 +76,8 @@ export function SingleVariantPageStateSelector({
diff --git a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx index 811c2133..62d18f6d 100644 --- a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { useState } from 'react'; +import { type Dispatch, type SetStateAction, useState } from 'react'; import { ApplyFilterButton } from '../ApplyFilterButton'; import { DynamicDateFilter } from '../DynamicDateFilter'; @@ -34,11 +34,13 @@ export function WasapPageStateSelector({ pageStateHandler, initialBaseFilterState, initialAnalysisFilterState, + setPageState, }: { config: WasapPageConfig; pageStateHandler: PageStateHandler; initialBaseFilterState: WasapBaseFilter; initialAnalysisFilterState: WasapAnalysisFilter; + setPageState: Dispatch>; }) { const [baseFilterState, setBaseFilterState] = useState(initialBaseFilterState); @@ -201,7 +203,11 @@ export function WasapPageStateSelector({ } })()} - + ); } diff --git a/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx b/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx index 0af04924..bd6d4b1e 100644 --- a/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx +++ b/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx @@ -1,15 +1,12 @@ import { useQuery } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; +import { type Dispatch, type SetStateAction, useState } from 'react'; import { z } from 'zod'; import { getClientLogger } from '../../../clientLogger.ts'; -import type { OrganismsConfig } from '../../../config.ts'; import { type CovidVariantData } from '../../../views/covid.ts'; -import { Routing } from '../../../views/routing.ts'; import { useErrorToast } from '../../ErrorReportInstruction.tsx'; -import { withQueryProvider } from '../../subscriptions/backendApi/withQueryProvider.tsx'; -export const collectionVariantClassName = 'border bg-white px-4 py-2 hover:bg-cyan border-gray-200'; +export const collectionVariantClassName = 'border bg-white px-4 py-2 hover:bg-cyan border-gray-200 cursor-pointer'; type CollectionVariant = { name: string; @@ -25,12 +22,11 @@ type Collection = { type CollectionsListProps = { initialCollectionId?: number; - organismsConfig: OrganismsConfig; + pageState: CovidVariantData; + setPageState: Dispatch>; }; -export const CollectionsList = withQueryProvider(CollectionsListInner); - -function CollectionsListInner({ initialCollectionId, organismsConfig }: CollectionsListProps) { +export function CollectionsList({ initialCollectionId, pageState, setPageState }: CollectionsListProps) { const [selectedCollectionId, setSelectedCollectionId] = useState(initialCollectionId ?? 1); const query = useQuery({ @@ -56,7 +52,8 @@ function CollectionsListInner({ initialCollectionId, organismsConfig }: Collecti /> c.id === selectedCollectionId) ?? query.data[0]} - organismsConfig={organismsConfig} + pageState={pageState} + setPageState={setPageState} /> ); @@ -96,10 +93,11 @@ const querySchema = z.object({ type CollectionVariantListProps = { collection: Collection; - organismsConfig: OrganismsConfig; + pageState: CovidVariantData; + setPageState: Dispatch>; }; -function CollectionVariantList({ collection, organismsConfig }: CollectionVariantListProps) { +function CollectionVariantList({ collection, pageState, setPageState }: CollectionVariantListProps) { const variants = collection.variants; return ( @@ -108,8 +106,9 @@ function CollectionVariantList({ collection, organismsConfig }: CollectionVarian ))} @@ -118,32 +117,37 @@ function CollectionVariantList({ collection, organismsConfig }: CollectionVarian type VariantLinkProps = { variant: CollectionVariant; - organismsConfig: OrganismsConfig; collectionId: number; + pageState: CovidVariantData; + setPageState: Dispatch>; }; -function VariantLink({ variant, organismsConfig, collectionId }: VariantLinkProps) { - const variantLink = useVariantLink(organismsConfig, collectionId, variant); +function VariantLink({ variant, collectionId, pageState, setPageState }: VariantLinkProps) { + const newPageState = useVariantLink(pageState, collectionId, variant); + + const applyFilters = () => { + if (newPageState === undefined) { + return; + } + setPageState(newPageState); + }; return ( - + ); } const logger = getClientLogger('CollectionList'); -function useVariantLink(organismsConfig: OrganismsConfig, collectionId: number, variant: CollectionVariant) { - const routing = useMemo(() => new Routing(organismsConfig), [organismsConfig]); - +function useVariantLink( + currentPageState: CovidVariantData, + collectionId: number, + variant: CollectionVariant, +): CovidVariantData | undefined { const { showErrorToast } = useErrorToast(logger); - const currentPageState = routing - .getOrganismView('covid.singleVariantView') - .pageStateHandler.parsePageStateFromUrl(new URL(window.location.href)); - let newPageState: CovidVariantData; - const queryParseResult = querySchema.safeParse(JSON.parse(variant.query)); if (!queryParseResult.success) { @@ -158,7 +162,7 @@ function useVariantLink(organismsConfig: OrganismsConfig, collectionId: number, const query = queryParseResult.data; if (query.variantQuery !== undefined) { - newPageState = { + return { ...currentPageState, collectionId: collectionId, variantFilter: { @@ -168,7 +172,7 @@ function useVariantLink(organismsConfig: OrganismsConfig, collectionId: number, }, }; } else { - newPageState = { + return { ...currentPageState, collectionId: collectionId, variantFilter: { @@ -184,5 +188,4 @@ function useVariantLink(organismsConfig: OrganismsConfig, collectionId: number, }, }; } - return routing.getOrganismView('covid.singleVariantView').pageStateHandler.toUrl(newPageState); } diff --git a/website/src/components/views/analyzeSingleVariant/CovidSingleVariantDataDisplay.tsx b/website/src/components/views/analyzeSingleVariant/CovidSingleVariantDataDisplay.tsx index b07241b5..8ee87831 100644 --- a/website/src/components/views/analyzeSingleVariant/CovidSingleVariantDataDisplay.tsx +++ b/website/src/components/views/analyzeSingleVariant/CovidSingleVariantDataDisplay.tsx @@ -1,17 +1,13 @@ import { views } from '@genspectrum/dashboard-components/util'; -import { type FC, useMemo } from 'react'; +import { type Dispatch, type FC, type SetStateAction } from 'react'; import { CollectionsList } from './CollectionsList.tsx'; import { SelectVariant } from './SelectVariant.tsx'; -import type { OrganismsConfig } from '../../../config.ts'; -import type { Organisms } from '../../../types/Organism.ts'; import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; import { hasOnlyUndefinedValues } from '../../../util/hasOnlyUndefinedValues.ts'; -import type { CovidVariantData } from '../../../views/covid.ts'; +import { CovidAnalyzeSingleVariantView, type CovidVariantData } from '../../../views/covid.ts'; import { getLocationSubdivision } from '../../../views/locationHelpers.ts'; import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl.ts'; -import { Routing } from '../../../views/routing.ts'; -import type { singleVariantViewKey } from '../../../views/viewKeys.ts'; import { ComponentsGrid } from '../../ComponentsGrid.tsx'; import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; import { GsMutations } from '../../genspectrum/GsMutations.tsx'; @@ -21,18 +17,16 @@ import { GsRelativeGrowthAdvantage } from '../../genspectrum/GsRelativeGrowthAdv import { GsStatistics } from '../../genspectrum/GsStatistics.tsx'; export type CovidSingleVariantDataDisplayProps = { - organismViewKey: `${typeof Organisms.covid}.${typeof singleVariantViewKey}`; - organismsConfig: OrganismsConfig; + view: CovidAnalyzeSingleVariantView; pageState: CovidVariantData; + setPageState: Dispatch>; }; export const CovidSingleVariantDataDisplay: FC = ({ - organismViewKey, - organismsConfig, + view, pageState, + setPageState, }) => { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); - const variantFilter = view.pageStateHandler.toLapisFilter(pageState); const datasetLapisFilter = view.pageStateHandler.toLapisFilterWithoutVariant(pageState); const timeGranularity = chooseGranularityBasedOnDateRange({ @@ -55,7 +49,8 @@ export const CovidSingleVariantDataDisplay: FC diff --git a/website/src/components/views/analyzeSingleVariant/CovidSingleVariantReactPage.tsx b/website/src/components/views/analyzeSingleVariant/CovidSingleVariantReactPage.tsx index 35b12bad..438ea46b 100644 --- a/website/src/components/views/analyzeSingleVariant/CovidSingleVariantReactPage.tsx +++ b/website/src/components/views/analyzeSingleVariant/CovidSingleVariantReactPage.tsx @@ -10,6 +10,7 @@ import { type OrganismViewKey, Routing } from '../../../views/routing.ts'; import { SelectorHeadline } from '../../pageStateSelectors/SelectorHeadline.tsx'; import { SingleVariantPageStateSelector } from '../../pageStateSelectors/SingleVariantPageStateSelector.tsx'; import { sanitizeForFilename } from '../compareSideBySide/toDownloadLink.ts'; +import { usePageState } from '../usePageState.ts'; export type CovidSingleVariantReactPageProps = { organismsConfig: OrganismsConfig; @@ -20,7 +21,7 @@ export const CovidSingleVariantReactPage: FC = const organismViewKey: OrganismViewKey = 'covid.singleVariantView'; const view = useMemo(() => new Routing(organismsConfig).getOrganismView(organismViewKey), [organismsConfig]); - const pageState = view.pageStateHandler.parsePageStateFromUrl(new URL(window.location.href)); + const { pageState, setPageState } = usePageState(view.pageStateHandler); const variantFilter = view.pageStateHandler.toLapisFilter(pageState); @@ -51,9 +52,9 @@ export const CovidSingleVariantReactPage: FC = filters={ <>
@@ -61,17 +62,14 @@ export const CovidSingleVariantReactPage: FC = Collections } dataDisplay={ - + } /> ); diff --git a/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx index 8dbdf386..0bd266e2 100644 --- a/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx +++ b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx @@ -1,16 +1,15 @@ import { views } from '@genspectrum/dashboard-components/util'; -import { type FC, useMemo } from 'react'; +import { type Dispatch, type FC, type SetStateAction } from 'react'; import { QuickstartLinks } from './QuickstartLinks.tsx'; import { SelectVariant } from './SelectVariant.tsx'; -import type { OrganismsConfig } from '../../../config.ts'; import { Organisms } from '../../../types/Organism.ts'; import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; import { hasOnlyUndefinedValues } from '../../../util/hasOnlyUndefinedValues.ts'; import type { DatasetAndVariantData } from '../../../views/View.ts'; import { getLocationSubdivision } from '../../../views/locationHelpers.ts'; import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl.ts'; -import { type OrganismViewKey, Routing, type SingleVariantOrganism } from '../../../views/routing.ts'; +import { type SingleVariantOrganism, type ViewsMap } from '../../../views/routing.ts'; import type { singleVariantViewKey } from '../../../views/viewKeys.ts'; import { ComponentsGrid } from '../../ComponentsGrid.tsx'; import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; @@ -20,19 +19,16 @@ import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx import { GsStatistics } from '../../genspectrum/GsStatistics.tsx'; export type GenericAnalyseSingleVariantDataDisplayProps = { - organismViewKey: OrganismViewKey & - `${Exclude}.${typeof singleVariantViewKey}`; - organismsConfig: OrganismsConfig; + view: ViewsMap[Exclude][typeof singleVariantViewKey]; pageState: DatasetAndVariantData; + setPageState: Dispatch>; }; export const GenericAnalyseSingleVariantDataDisplay: FC = ({ - organismViewKey, - organismsConfig, + view, pageState, + setPageState, }) => { - const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); - const variantLapisFilter = view.pageStateHandler.toLapisFilter(pageState); const datasetLapisFilter = view.pageStateHandler.toLapisFilterWithoutVariant(pageState); const timeGranularity = chooseGranularityBasedOnDateRange({ @@ -52,7 +48,7 @@ export const GenericAnalyseSingleVariantDataDisplay: FC {noVariantSelected && ( - + )} diff --git a/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantReactPage.tsx b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantReactPage.tsx index 3a9c770f..477e9a5a 100644 --- a/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantReactPage.tsx +++ b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantReactPage.tsx @@ -10,6 +10,7 @@ import { type OrganismViewKey, type OrganismWithViewKey, Routing } from '../../. import { singleVariantViewKey } from '../../../views/viewKeys'; import { SingleVariantPageStateSelector } from '../../pageStateSelectors/SingleVariantPageStateSelector'; import { sanitizeForFilename } from '../compareSideBySide/toDownloadLink'; +import { usePageState } from '../usePageState.ts'; export type GenericAnalyseSingleVariantReactPageProps = { organism: Exclude, typeof Organisms.covid>; @@ -28,7 +29,7 @@ export const GenericAnalyseSingleVariantReactPage: FC } dataDisplay={ - + } /> ); diff --git a/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx b/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx index 9745d57d..86eac39e 100644 --- a/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx +++ b/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx @@ -1,8 +1,8 @@ -import type { FC } from 'react'; +import type { Dispatch, FC, SetStateAction } from 'react'; import { collectionVariantClassName } from './CollectionsList'; import { Organisms } from '../../../types/Organism'; -import type { DatasetFilter } from '../../../views/View.ts'; +import type { DatasetAndVariantData, DatasetFilter } from '../../../views/View.ts'; import { toDisplayName } from '../../../views/pageStateHandlers/PageStateHandler'; import type { SingleVariantOrganism, ViewsMap } from '../../../views/routing'; import { singleVariantViewKey } from '../../../views/viewKeys'; @@ -10,9 +10,10 @@ import { singleVariantViewKey } from '../../../views/viewKeys'; export type QuickstartLinksProps = { view: ViewsMap[Exclude][typeof singleVariantViewKey]; datasetFilter: DatasetFilter; + setPageState: Dispatch>; }; -export const QuickstartLinks: FC = ({ view, datasetFilter }) => { +export const QuickstartLinks: FC = ({ view, datasetFilter, setPageState }) => { const variants = view.organismConstants.predefinedVariants; return ( @@ -21,13 +22,17 @@ export const QuickstartLinks: FC = ({ view, datasetFilter
{variants.map((variant) => { - const href = view.pageStateHandler.toUrl({ - datasetFilter: datasetFilter, - variantFilter: variant, - }); + const applyFilters = () => { + setPageState({ + datasetFilter: datasetFilter, + variantFilter: variant, + }); + }; + + const displayName = toDisplayName(variant); return ( - - {toDisplayName(variant)} + + {displayName} ); })} diff --git a/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx b/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx index 53921b47..76e5da97 100644 --- a/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx +++ b/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx @@ -1,36 +1,28 @@ -import { type FC, useMemo } from 'react'; +import { type FC } from 'react'; -import type { OrganismsConfig } from '../../../config.ts'; import { Organisms } from '../../../types/Organism.ts'; import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; -import { ComponentHeight } from '../../../views/OrganismConstants.ts'; -import type { DatasetAndVariantData } from '../../../views/View.ts'; +import { BaseView } from '../../../views/BaseView.ts'; +import { ComponentHeight, type OrganismConstants } from '../../../views/OrganismConstants.ts'; +import type { CompareSideBySideData, DatasetAndVariantData } from '../../../views/View.ts'; +import { CompareSideBySideStateHandler } from '../../../views/pageStateHandlers/CompareSideBySidePageStateHandler.ts'; import { toLapisFilterWithoutVariant } from '../../../views/pageStateHandlers/toLapisFilterWithoutVariant.ts'; -import { type OrganismWithViewKey, Routing } from '../../../views/routing.ts'; -import { compareSideBySideViewKey } from '../../../views/viewKeys.ts'; import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; import { GsMutations } from '../../genspectrum/GsMutations.tsx'; import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx'; import { GsRelativeGrowthAdvantage } from '../../genspectrum/GsRelativeGrowthAdvantage.tsx'; export type GenericCompareSideBySideDataDisplayProps = { - organismViewKey: `${OrganismWithViewKey}.${typeof compareSideBySideViewKey}`; - organismsConfig: OrganismsConfig; + view: BaseView; datasetAndVariantData: DatasetAndVariantData; hideMutationComponents?: boolean; }; export const GenericCompareSideBySideDataDisplay: FC = ({ - organismViewKey, - organismsConfig, + view, datasetAndVariantData, hideMutationComponents, }) => { - const view = useMemo( - () => new Routing(organismsConfig).getOrganismView(organismViewKey), - [organismsConfig, organismViewKey], - ); - const { datasetFilter, variantFilter } = datasetAndVariantData; const datasetLapisFilter = toLapisFilterWithoutVariant(datasetFilter, view.organismConstants.additionalFilters); diff --git a/website/src/components/views/compareSideBySide/GenericCompareSideBySideReactPage.tsx b/website/src/components/views/compareSideBySide/GenericCompareSideBySideReactPage.tsx index 20e72eed..e4f3b5a2 100644 --- a/website/src/components/views/compareSideBySide/GenericCompareSideBySideReactPage.tsx +++ b/website/src/components/views/compareSideBySide/GenericCompareSideBySideReactPage.tsx @@ -7,6 +7,7 @@ import { OrganismViewPageLayout } from '../../../layouts/OrganismPage/OrganismVi import { type OrganismViewKey, type OrganismWithViewKey, Routing } from '../../../views/routing'; import { compareSideBySideViewKey } from '../../../views/viewKeys'; import { CompareSideBySidePageStateSelector } from '../../pageStateSelectors/CompareSideBySidePageStateSelector'; +import { usePageState } from '../usePageState.ts'; export type GenericCompareSideBySideReactPageProps = { organism: OrganismWithViewKey; @@ -28,7 +29,7 @@ export const GenericCompareSideBySideReactPage: FC )}
diff --git a/website/src/components/views/compareToBaseline/GenericCompareToBaselineDataDisplay.tsx b/website/src/components/views/compareToBaseline/GenericCompareToBaselineDataDisplay.tsx index 39dc1bb0..f2f7163e 100644 --- a/website/src/components/views/compareToBaseline/GenericCompareToBaselineDataDisplay.tsx +++ b/website/src/components/views/compareToBaseline/GenericCompareToBaselineDataDisplay.tsx @@ -1,31 +1,19 @@ -import { type FC, useMemo } from 'react'; +import { type FC } from 'react'; import { SelectBaseline } from './SelectBaseline.tsx'; -import type { OrganismsConfig } from '../../../config.ts'; import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; -import { ComponentHeight } from '../../../views/OrganismConstants.ts'; +import { GenericCompareToBaselineView } from '../../../views/BaseView.ts'; +import { ComponentHeight, type OrganismConstants } from '../../../views/OrganismConstants.ts'; import type { CompareToBaselineData } from '../../../views/View.ts'; -import { type OrganismWithViewKey, Routing } from '../../../views/routing.ts'; -import { compareToBaselineViewKey } from '../../../views/viewKeys.ts'; import { ComponentsGrid } from '../../ComponentsGrid.tsx'; import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx'; export type GenericCompareToBaselineDisplayProps = { - organismViewKey: `${OrganismWithViewKey}.${typeof compareToBaselineViewKey}`; - organismsConfig: OrganismsConfig; + view: GenericCompareToBaselineView; pageState: CompareToBaselineData; }; -export const GenericCompareToBaselineDataDisplay: FC = ({ - organismViewKey, - organismsConfig, - pageState, -}) => { - const view = useMemo( - () => new Routing(organismsConfig).getOrganismView(organismViewKey), - [organismsConfig, organismViewKey], - ); - +export const GenericCompareToBaselineDataDisplay: FC = ({ view, pageState }) => { const baselineLapisFilter = view.pageStateHandler.baselineFilterToLapisFilter(pageState); const timeGranularity = chooseGranularityBasedOnDateRange({ earliestDate: new Date(view.organismConstants.earliestDate), diff --git a/website/src/components/views/compareToBaseline/GenericCompareToBaselineReactPage.tsx b/website/src/components/views/compareToBaseline/GenericCompareToBaselineReactPage.tsx index b1afb1c0..b6b71b46 100644 --- a/website/src/components/views/compareToBaseline/GenericCompareToBaselineReactPage.tsx +++ b/website/src/components/views/compareToBaseline/GenericCompareToBaselineReactPage.tsx @@ -7,6 +7,7 @@ import { type OrganismViewKey, type OrganismWithViewKey, Routing } from '../../. import { compareToBaselineViewKey } from '../../../views/viewKeys'; import { CompareVariantsToBaselineStateSelector } from '../../pageStateSelectors/CompareVariantsToBaselineStateSelector'; import { sanitizeForFilename } from '../compareSideBySide/toDownloadLink'; +import { usePageState } from '../usePageState.ts'; export type GenericCompareToBaselineReactPageProps = { organism: OrganismWithViewKey; @@ -25,7 +26,7 @@ export const GenericCompareToBaselineReactPage: FC - } - dataDisplay={ - } + dataDisplay={} /> ); }; diff --git a/website/src/components/views/compareVariants/GenericCompareVariantsDataDisplay.tsx b/website/src/components/views/compareVariants/GenericCompareVariantsDataDisplay.tsx index 2b57434c..6610477b 100644 --- a/website/src/components/views/compareVariants/GenericCompareVariantsDataDisplay.tsx +++ b/website/src/components/views/compareVariants/GenericCompareVariantsDataDisplay.tsx @@ -1,33 +1,22 @@ -import { type FC, useMemo } from 'react'; +import { type FC } from 'react'; import { SelectVariants } from './SelectVariants.tsx'; -import type { OrganismsConfig } from '../../../config.ts'; import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; +import { GenericCompareVariantsView } from '../../../views/BaseView.ts'; +import type { OrganismConstants } from '../../../views/OrganismConstants.ts'; import type { CompareVariantsData } from '../../../views/View.ts'; -import { type OrganismWithViewKey, Routing } from '../../../views/routing.ts'; -import { compareVariantsViewKey } from '../../../views/viewKeys.ts'; import { ComponentsGrid } from '../../ComponentsGrid.tsx'; import { GsMutationComparison } from '../../genspectrum/GsMutationComparison.tsx'; import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx'; export type GenericCompareVariantsDataDisplayProps = { - organismViewKey: `${OrganismWithViewKey}.${typeof compareVariantsViewKey}`; - organismsConfig: OrganismsConfig; + view: GenericCompareVariantsView; pageState: CompareVariantsData; }; const componentHeight = '540px'; // prevalence over time table with 10 rows -export const GenericCompareVariantsDataDisplay: FC = ({ - organismViewKey, - organismsConfig, - pageState, -}) => { - const view = useMemo( - () => new Routing(organismsConfig).getOrganismView(organismViewKey), - [organismsConfig, organismViewKey], - ); - +export const GenericCompareVariantsDataDisplay: FC = ({ view, pageState }) => { const datasetLapisFilter = view.pageStateHandler.datasetFilterToLapisFilter(pageState.datasetFilter); const timeGranularity = chooseGranularityBasedOnDateRange({ earliestDate: new Date(view.organismConstants.earliestDate), diff --git a/website/src/components/views/compareVariants/GenericCompareVariantsReactPage.tsx b/website/src/components/views/compareVariants/GenericCompareVariantsReactPage.tsx index d1b97048..cffaa97f 100644 --- a/website/src/components/views/compareVariants/GenericCompareVariantsReactPage.tsx +++ b/website/src/components/views/compareVariants/GenericCompareVariantsReactPage.tsx @@ -7,6 +7,7 @@ import { type OrganismViewKey, type OrganismWithViewKey, Routing } from '../../. import { compareVariantsViewKey } from '../../../views/viewKeys'; import { CompareVariantsPageStateSelector } from '../../pageStateSelectors/CompareVariantsPageStateSelector'; import { sanitizeForFilename } from '../compareSideBySide/toDownloadLink'; +import { usePageState } from '../usePageState.ts'; export type GenericCompareVariantsReactPageProps = { organism: OrganismWithViewKey; @@ -25,7 +26,7 @@ export const GenericCompareVariantsReactPage: FC - } - dataDisplay={ - } + dataDisplay={} /> ); }; diff --git a/website/src/components/views/sequencingEfforts/GenericSequencingEffortsReactPage.tsx b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsReactPage.tsx index 8dea17a2..865a0647 100644 --- a/website/src/components/views/sequencingEfforts/GenericSequencingEffortsReactPage.tsx +++ b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsReactPage.tsx @@ -6,6 +6,7 @@ import { SingleVariantOrganismPageLayout } from '../../../layouts/OrganismPage/S import { type OrganismViewKey, type OrganismWithViewKey, Routing } from '../../../views/routing'; import { sequencingEffortsViewKey } from '../../../views/viewKeys'; import { SequencingEffortsPageStateSelector } from '../../pageStateSelectors/SequencingEffortsPageStateSelector'; +import { usePageState } from '../usePageState.ts'; export type GenericSequencingEffortsReactPageProps = { organism: OrganismWithViewKey; @@ -24,7 +25,7 @@ export const GenericSequencingEffortsReactPage: FC } diff --git a/website/src/components/views/usePageState.ts b/website/src/components/views/usePageState.ts new file mode 100644 index 00000000..fe72b8a0 --- /dev/null +++ b/website/src/components/views/usePageState.ts @@ -0,0 +1,27 @@ +import { type Dispatch, type SetStateAction, useCallback, useMemo, useState } from 'react'; + +import type { PageStateHandler } from '../../views/pageStateHandlers/PageStateHandler.ts'; + +export function usePageState>(pageStateHandler: StateHandler) { + type PageState = StateHandler extends PageStateHandler ? PS : never; + + const [pageState, setPageStateRaw] = useState( + () => pageStateHandler.parsePageStateFromUrl(new URL(window.location.href)) as PageState, + ); + + const setPageState: Dispatch> = useCallback( + (newPageStateOrUpdater) => { + setPageStateRaw((prevState) => { + const newPageState = + typeof newPageStateOrUpdater === 'function' + ? newPageStateOrUpdater(prevState) + : newPageStateOrUpdater; + window.history.pushState(undefined, '', pageStateHandler.toUrl(newPageState)); + return newPageState; + }); + }, + [pageStateHandler], + ); + + return useMemo(() => ({ pageState, setPageState }), [pageState, setPageState]); +} diff --git a/website/src/components/views/wasap/Wasap.astro b/website/src/components/views/wasap/Wasap.astro index 4c24593d..4535ed13 100644 --- a/website/src/components/views/wasap/Wasap.astro +++ b/website/src/components/views/wasap/Wasap.astro @@ -9,10 +9,8 @@ type Props = { const { wastewaterOrganism } = Astro.props; const { name } = wastewaterOrganismConfigs[wastewaterOrganism]; - -const url = Astro.url; --- - + diff --git a/website/src/components/views/wasap/WasapPage.tsx b/website/src/components/views/wasap/WasapPage.tsx index 03f93c72..b829f9b8 100644 --- a/website/src/components/views/wasap/WasapPage.tsx +++ b/website/src/components/views/wasap/WasapPage.tsx @@ -21,20 +21,19 @@ import { WasapPageStateHandler } from '../../../views/pageStateHandlers/WasapPag import { GsMutationsOverTime } from '../../genspectrum/GsMutationsOverTime'; import { WasapPageStateSelector } from '../../pageStateSelectors/wasap/WasapPageStateSelector'; import { withQueryProvider } from '../../subscriptions/backendApi/withQueryProvider'; +import { usePageState } from '../usePageState.ts'; export type WasapPageProps = { wastewaterOrganism: WastewaterOrganismName; - currentUrl: URL; }; -export const WasapPageInner: FC = ({ wastewaterOrganism, currentUrl }) => { +export const WasapPageInner: FC = ({ wastewaterOrganism }) => { const config = wastewaterOrganismConfigs[wastewaterOrganism]; // initialize page state from the URL const pageStateHandler = useMemo(() => new WasapPageStateHandler(config), [config]); - const { base, analysis } = useMemo( - () => pageStateHandler.parsePageStateFromUrl(currentUrl), - [pageStateHandler, currentUrl], - ); + + const { pageState, setPageState } = usePageState(pageStateHandler); + const { base, analysis } = pageState; // fetch which mutations should be analyzed const { data, isPending, isError } = useWasapPageData(config, analysis); @@ -90,6 +89,7 @@ export const WasapPageInner: FC = ({ wastewaterOrganism, current pageStateHandler={pageStateHandler} initialBaseFilterState={base} initialAnalysisFilterState={analysis} + setPageState={setPageState} /> {isError ? ( diff --git a/website/src/views/OrganismConstants.ts b/website/src/views/OrganismConstants.ts index e3c32330..b6c34951 100644 --- a/website/src/views/OrganismConstants.ts +++ b/website/src/views/OrganismConstants.ts @@ -24,6 +24,7 @@ type AggregatedVisualizations = { export interface OrganismConstants { readonly organism: Organism; + readonly earliestDate: string; readonly dataOrigins: DataOrigin[]; readonly accessionDownloadFields: string[]; readonly mainDateField: string; diff --git a/website/src/views/pageStateHandlers/CompareSideBySidePageStateHandler.spec.ts b/website/src/views/pageStateHandlers/CompareSideBySidePageStateHandler.spec.ts index a369acc5..99e178fb 100644 --- a/website/src/views/pageStateHandlers/CompareSideBySidePageStateHandler.spec.ts +++ b/website/src/views/pageStateHandlers/CompareSideBySidePageStateHandler.spec.ts @@ -9,6 +9,7 @@ const mockDateRangeOption = { label: 'Last 7 Days', dateFrom: '2024-11-22', date const mockConstants: OrganismConstants = { organism: Organisms.covid, + earliestDate: 'earliestDate', dataOrigins: [], locationFields: ['country', 'region'], mainDateField: 'date', diff --git a/website/src/views/pageStateHandlers/CompareToBaselinePageStateHandler.spec.ts b/website/src/views/pageStateHandlers/CompareToBaselinePageStateHandler.spec.ts index 17492378..29f131f6 100644 --- a/website/src/views/pageStateHandlers/CompareToBaselinePageStateHandler.spec.ts +++ b/website/src/views/pageStateHandlers/CompareToBaselinePageStateHandler.spec.ts @@ -9,6 +9,7 @@ const mockDateRangeOption = { label: 'Last 7 Days', dateFrom: '2024-11-22', date const mockConstants: OrganismConstants = { organism: Organisms.covid, + earliestDate: 'earliestDate', dataOrigins: [], locationFields: ['country', 'region'], mainDateField: 'date', diff --git a/website/src/views/pageStateHandlers/CompareVariantsPageStateHandler.spec.ts b/website/src/views/pageStateHandlers/CompareVariantsPageStateHandler.spec.ts index 5d05f6de..fbdce1b4 100644 --- a/website/src/views/pageStateHandlers/CompareVariantsPageStateHandler.spec.ts +++ b/website/src/views/pageStateHandlers/CompareVariantsPageStateHandler.spec.ts @@ -9,6 +9,7 @@ const mockDateRangeOption = { label: 'Last 7 Days', dateFrom: '2024-11-22', date const mockConstants: OrganismConstants = { organism: Organisms.covid, + earliestDate: 'earliestDate', dataOrigins: [], locationFields: ['country', 'region'], mainDateField: 'date', diff --git a/website/src/views/pageStateHandlers/SingleVariantPageStateHandler.spec.ts b/website/src/views/pageStateHandlers/SingleVariantPageStateHandler.spec.ts index cc9f7ce3..78598639 100644 --- a/website/src/views/pageStateHandlers/SingleVariantPageStateHandler.spec.ts +++ b/website/src/views/pageStateHandlers/SingleVariantPageStateHandler.spec.ts @@ -9,6 +9,7 @@ const mockDateRangeOption = { label: 'Last 7 Days', dateFrom: '2024-11-22', date const mockConstants: OrganismConstants = { organism: Organisms.covid, + earliestDate: 'earliestDate', dataOrigins: [], locationFields: ['country', 'region'], mainDateField: 'date',