Skip to content

Commit

Permalink
feat(website): add download dialog to search page (#847)
Browse files Browse the repository at this point in the history
  • Loading branch information
chaoran-chen authored Jan 30, 2024
1 parent c68096f commit 09744a6
Show file tree
Hide file tree
Showing 8 changed files with 564 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { FC } from 'react';

import type { FilterValue, MutationFilter } from '../../../types/config.ts';

type ActiveDownloadFiltersProps = {
metadataFilter: FilterValue[];
mutationFilter: MutationFilter;
};

export const ActiveDownloadFilters: FC<ActiveDownloadFiltersProps> = ({ metadataFilter, mutationFilter }) => {
const filterValues: FilterValue[] = metadataFilter.filter((f) => f.filterValue.length > 0);
[
{ name: 'nucleotideMutations', value: mutationFilter.nucleotideMutationQueries },
{ name: 'aminoAcidMutations', value: mutationFilter.aminoAcidMutationQueries },
{ name: 'nucleotideInsertion', value: mutationFilter.nucleotideInsertionQueries },
{ name: 'aminoAcidInsertions', value: mutationFilter.aminoAcidInsertionQueries },
].forEach(({ name, value }) => {
if (value !== undefined) {
filterValues.push({ name, filterValue: value.join(', ') });
}
});

if (filterValues.length === 0) {
return undefined;
}

return (
<div className='mb-4'>
<h4 className='font-bold mb-2'>Active filters:</h4>
<div className='flex flex-row flex-wrap gap-4'>
{filterValues.map(({ name, filterValue }) => (
<div key={name} className='border-black border rounded-full px-2 py-1 text-sm'>
{name}: {filterValue}
</div>
))}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeAll, describe, expect, test, vi } from 'vitest';

import { DownloadDialog } from './DownloadDialog.tsx';
import type { FilterValue, MutationFilter } from '../../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';

const defaultReferenceGenome: ReferenceGenomesSequenceNames = {
nucleotideSequences: ['main'],
genes: ['gene1', 'gene2'],
};

const defaultLapisUrl = 'https://lapis';

async function renderDialog(
metadataFilter: FilterValue[] = [],
mutationFilter: MutationFilter = {},
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames = defaultReferenceGenome,
lapisUrl: string = defaultLapisUrl,
) {
render(
<DownloadDialog
metadataFilter={metadataFilter}
mutationFilter={mutationFilter}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
lapisUrl={lapisUrl}
/>,
);

// Open the panel
const button = screen.getByRole('button', { name: 'Download' });
await userEvent.click(button);
}

describe('DownloadDialog', () => {
beforeAll(() => {
// Vitest does not seem to support showModal, yet.
// Workaround from https://github.com/jsdom/jsdom/issues/3294#issuecomment-1268330372
HTMLDialogElement.prototype.showModal = vi.fn(function mock(this: HTMLDialogElement) {
this.open = true;
});

HTMLDialogElement.prototype.close = vi.fn(function mock(this: HTMLDialogElement) {
this.open = false;
});
});

test('should display active filters if there are some', async () => {
await renderDialog([{ name: 'field1', filterValue: 'value1' }], {
nucleotideMutationQueries: ['A123T', 'G234C'],
});
expect(screen.queryByText(/Active filters/)).toBeInTheDocument();
expect(screen.queryByText('field1: value1')).toBeInTheDocument();
expect(screen.queryByText(/A123T, G234C/)).toBeInTheDocument();
});

test('should not display active filters if there are none', async () => {
await renderDialog();
expect(screen.queryByText(/Active filters/)).not.toBeInTheDocument();
expect(screen.queryByText('field1: value1')).not.toBeInTheDocument();
expect(screen.queryByText(/A123T, G234C/)).not.toBeInTheDocument();
});

test('should activate download button only after agreeing to the terms', async () => {
await renderDialog();

const downloadButton = screen.getByRole('link', { name: 'Download' });
expect(downloadButton).toHaveClass('btn-disabled');
expect(getDownloadHref()).not.toMatch(new RegExp(`^${defaultLapisUrl}`));

await checkAgreement();
expect(downloadButton).not.toHaveClass('btn-disabled');
expect(getDownloadHref()).toMatch(new RegExp(`^${defaultLapisUrl}`));
});

test('should generate the right download link', async () => {
await renderDialog([{ name: 'field1', filterValue: 'value1' }]);
await checkAgreement();

expect(getDownloadHref()).toBe(
`${defaultLapisUrl}/sample/details?versionStatus=LATEST_VERSION&isRevocation=false&dataFormat=tsv&field1=value1`,
);

await userEvent.click(screen.getByLabelText(/Yes, include older versions/));
await userEvent.click(screen.getByLabelText(/Raw nucleotide sequences/));
expect(getDownloadHref()).toBe(`${defaultLapisUrl}/sample/unalignedNucleotideSequences?field1=value1`);
});
});

async function checkAgreement() {
const agreementCheckbox = screen.getByLabelText('I agree to the data use terms.');
await userEvent.click(agreementCheckbox);
}

function getDownloadHref() {
const downloadButton = screen.getByRole('link', { name: 'Download' });
return downloadButton.getAttribute('href');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { type FC, useMemo, useRef, useState } from 'react';

import { ActiveDownloadFilters } from './ActiveDownloadFilters.tsx';
import { DownloadForm } from './DownloadForm.tsx';
import { type DownloadOption, generateDownloadUrl } from './generateDownloadUrl.ts';
import type { FilterValue, MutationFilter } from '../../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';

type DownloadDialogProps = {
metadataFilter: FilterValue[];
mutationFilter: MutationFilter;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
lapisUrl: string;
};

export const DownloadDialog: FC<DownloadDialogProps> = ({
metadataFilter,
mutationFilter,
referenceGenomesSequenceNames,
lapisUrl,
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const [downloadOption, setDownloadOption] = useState<DownloadOption | undefined>();
const [agreedToDataUseTerms, setAgreedToDataUseTerms] = useState(false);

const openDialog = () => {
if (dialogRef.current) {
dialogRef.current.showModal();
}
};

const closeDialog = () => {
if (dialogRef.current) {
dialogRef.current.close();
}
};

const downloadUrl = useMemo(() => {
if (downloadOption === undefined || !agreedToDataUseTerms) {
return '#';
}
return generateDownloadUrl(metadataFilter, mutationFilter, downloadOption, lapisUrl);
}, [downloadOption, lapisUrl, metadataFilter, mutationFilter, agreedToDataUseTerms]);

return (
<>
<button className='btn' onClick={openDialog}>
Download
</button>

<dialog ref={dialogRef} className='modal'>
<div className='modal-box max-w-5xl'>
<form method='dialog'>
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'></button>
</form>

<h3 className='font-bold text-2xl mb-4'>Download</h3>

<ActiveDownloadFilters metadataFilter={metadataFilter} mutationFilter={mutationFilter} />
<DownloadForm
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
onChange={setDownloadOption}
/>

<div className='mb-4'>
<label className='label justify-start'>
<input
type='checkbox'
name='data-use-terms-agreement'
className='checkbox mr-2'
checked={agreedToDataUseTerms}
onChange={() => setAgreedToDataUseTerms(!agreedToDataUseTerms)}
/>
<span className='label-text'>
I agree to the {/* TODO(862) */}
<a href='#' className='underline'>
data use terms
</a>
.
</span>
</label>
</div>

<a
className={`btn loculusGreen ${!agreedToDataUseTerms ? 'btn-disabled' : ''}`}
href={downloadUrl}
target='_blank'
onClick={closeDialog}
>
Download
</a>
</div>
</dialog>
</>
);
};
Loading

0 comments on commit 09744a6

Please sign in to comment.