Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} />,
const { result: state } = renderHook(() => useState<DummyPageState>({ data: 'initial' }));

const { getByRole } = render(
<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} setPageState={state.current[1]} />,
);

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<DummyPageState>({ data: 'initial' }));

const { getByRole, container } = render(
<ApplyFilterButton pageStateHandler={handler} newPageState={longState} />,
<ApplyFilterButton pageStateHandler={handler} newPageState={longState} setPageState={state.current[1]} />,
);

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<DummyPageState>({ data: 'initial' }));

const { getByRole, container, rerender } = render(
<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} />,
<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} setPageState={state.current[1]} />,
);

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(<ApplyFilterButton pageStateHandler={handler} newPageState={longState} />);
rerender(
<ApplyFilterButton pageStateHandler={handler} newPageState={longState} setPageState={state.current[1]} />,
);

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<DummyPageState>({ data: 'initial' }));

const { getByRole, container, rerender } = render(
<ApplyFilterButton pageStateHandler={handler} newPageState={longState} />,
<ApplyFilterButton pageStateHandler={handler} newPageState={longState} setPageState={state.current[1]} />,
);

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(<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} />);
rerender(
<ApplyFilterButton pageStateHandler={handler} newPageState={shortState} setPageState={state.current[1]} />,
);

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();
});
}
12 changes: 10 additions & 2 deletions website/src/components/pageStateSelectors/ApplyFilterButton.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,15 +10,21 @@ const MAX_URL_LENGTH = 2000;
export function ApplyFilterButton<PageState extends object>({
pageStateHandler,
newPageState,
setPageState,
className = '',
}: WithClassName<{
pageStateHandler: PageStateHandler<PageState>;
newPageState: PageState;
setPageState: Dispatch<SetStateAction<PageState>>;
}>) {
const url = pageStateHandler.toUrl(newPageState);
const fullUrl = `${window.location.origin}${url}`;
const urlTooLong = fullUrl.length > MAX_URL_LENGTH;

const applyFilters = () => {
setPageState(newPageState);
};

return urlTooLong ? (
<>
<span role='button' className={`btn btn-primary btn-disabled ${className}`}>
Expand All @@ -30,8 +38,8 @@ export function ApplyFilterButton<PageState extends object>({
</div>
</>
) : (
<a role='button' href={url} className={`btn btn-primary ${className}`}>
<button type='button' onClick={applyFilters} className={`btn btn-primary ${className}`}>
Apply filters
</a>
</button>
);
}
Original file line number Diff line number Diff line change
@@ -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<CompareSideBySideData, OrganismConstants, CompareSideBySideStateHandler>;
filterId: number;
initialPageState: CompareSideBySideData;
organismViewKey: OrganismViewKey & `${string}.${typeof compareSideBySideViewKey}`;
organismsConfig: OrganismsConfig;
pageState: CompareSideBySideData;
setPageState: Dispatch<SetStateAction<CompareSideBySideData>>;
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: {},
Expand All @@ -47,7 +48,7 @@ export function CompareSideBySidePageStateSelector({
filterOfCurrentId.variantFilter,
),
};
}, [pageState, filterId, view.pageStateHandler]);
}, [draftPageState, filterId, view.pageStateHandler]);

return (
<div className='flex flex-col gap-4 p-2 shadow-lg'>
Expand All @@ -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,
Expand All @@ -78,8 +79,8 @@ export function CompareSideBySidePageStateSelector({
<Inset className='p-2'>
<VariantSelector
onVariantFilterChange={(newVariantFilter) => {
setPageState((previousState) => {
const updatedFilters = new Map(initialPageState.filters);
setDraftPageState((previousState) => {
const updatedFilters = new Map(pageState.filters);
updatedFilters.set(filterId, {
...filterOfCurrentId,
variantFilter: newVariantFilter,
Expand All @@ -96,7 +97,11 @@ export function CompareSideBySidePageStateSelector({
</div>
</div>
<div className='flex justify-end'>
<ApplyFilterButton pageStateHandler={view.pageStateHandler} newPageState={pageState} />
<ApplyFilterButton
pageStateHandler={view.pageStateHandler}
newPageState={draftPageState}
setPageState={setPageState}
/>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OrganismConstants>;
pageState: CompareVariantsData;
setPageState: Dispatch<SetStateAction<CompareVariantsData>>;
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 (
<div className='flex flex-col gap-4'>
Expand All @@ -43,9 +42,9 @@ export function CompareVariantsPageStateSelector({
<BaselineSelector
baselineFilterConfigs={view.organismConstants.baselineFilterConfigs}
lapisFilter={currentLapisFilter}
datasetFilter={pageState.datasetFilter}
datasetFilter={draftPageState.datasetFilter}
setDatasetFilter={(newDatasetFilter) => {
setPageState((previousState) => ({
setDraftPageState((previousState) => ({
...previousState,
datasetFilter: newDatasetFilter,
}));
Expand All @@ -58,10 +57,10 @@ export function CompareVariantsPageStateSelector({
<SelectorHeadline>Variant Filters</SelectorHeadline>
<Inset className='p-2'>
<VariantsSelector
variantFilters={pageState.variants}
variantFilters={draftPageState.variants}
variantFilterConfigs={variantFilterConfigs}
setVariantFilters={(newVariantFilters: Map<Id, VariantFilter>) => {
setPageState((previousState) => ({
setDraftPageState((previousState) => ({
...previousState,
variants: newVariantFilters,
}));
Expand All @@ -75,7 +74,8 @@ export function CompareVariantsPageStateSelector({
<ApplyFilterButton
className='w-full'
pageStateHandler={view.pageStateHandler}
newPageState={pageState}
newPageState={draftPageState}
setPageState={setPageState}
/>
</div>
</div>
Expand Down
Loading
Loading