Skip to content

Commit

Permalink
feat(website): Change data use terms in bulk (#3322)
Browse files Browse the repository at this point in the history
* Add TODO

* Add dialog stub

* More stub progress

* Factor out BaseDialog

* Factor out BaseDialog in new Dialog

* Add todos

* Add download parameters

* clean up

* Add some notes

* Rename

* refactoring SequenceFilter

* add docs

* partial test fixes

* Test fix

* More test fix

* test fix

* WIP

* Data can be downloaded!

* Remove dead code

* Can edit just selected sequences now

* Remove TODO

* Update todo

* WIP

* Add more skeleton stuff

* Remove TODOs

* progress

* a bit of cleanup

* Add button

* Layout progress

* WIP

* Refactor DataUseTermsSelector

* Add DataUseTermsSelector to EditDataUseTermsModal

* format

* Getting close!

* Fix a timing issue

* remove ':' from active filters for consistency

* Show button only on 'released' page

* set min date in popup

* rename, cleanup

* rename, cleanup

* rename, cleanup

* remove debug statement

* Add unit test stub

* Better 'not logged in' handling

* Add stub test

* Add DataUseTermsSelector tests

* Update website/src/components/DataUseTerms/EditDataUseTermsModal.tsx

Co-authored-by: Theo Sanderson <[email protected]>

* don't select anything by default

* Update website/src/components/DataUseTerms/EditDataUseTermsModal.tsx

Co-authored-by: Theo Sanderson <[email protected]>

* Make 'Open Data Use Terms' a link to the data use terms page

* fix mobile layout: use flex-col for screens<md

* move text below selection

* add calendar description option; replace 'cyan' with primary color for date picker

* Add info about edit successful

* Re-use hook toast handlers

* rename type->option

* format

* Show data use terms column when editing data use terms is possible

* Only update date where date needs to be updated

* Disable button if no sequences would get updated

---------

Co-authored-by: Theo Sanderson <[email protected]>
  • Loading branch information
fhennig and theosanderson authored Dec 10, 2024
1 parent 5b78356 commit 272d34b
Show file tree
Hide file tree
Showing 27 changed files with 1,009 additions and 463 deletions.
140 changes: 140 additions & 0 deletions website/src/components/DataUseTerms/DataUseTermsSelector.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { DateTime } from 'luxon';
import { describe, expect, test, vi } from 'vitest';

import DataUseTermsSelector from './DataUseTermsSelector';
import { openDataUseTermsOption, restrictedDataUseTermsOption } from '../../types/backend.ts';

describe('DataUseTermsSelector', () => {
test('calls setDataUseTerms when an input is clicked', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.now().plus({ days: 30 });

render(
<DataUseTermsSelector
initialDataUseTermsOption={openDataUseTermsOption}
maxRestrictedUntil={maxRestrictedUntil}
setDataUseTerms={mockSetDataUseTerms}
/>,
);

// Restricted radio input
const restrictedInput = screen.getByLabelText('Restricted');
fireEvent.click(restrictedInput);

expect(mockSetDataUseTerms).toHaveBeenCalledWith({
type: restrictedDataUseTermsOption,
restrictedUntil: maxRestrictedUntil.toFormat('yyyy-MM-dd'),
});

// Open radio input
const openInput = screen.getByLabelText('Open');
fireEvent.click(openInput);

expect(mockSetDataUseTerms).toHaveBeenCalledWith({ type: openDataUseTermsOption });
});

test('opens the modal when calendarUseModal is true and "Change date" button is clicked', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.now().plus({ days: 30 });

render(
<DataUseTermsSelector
initialDataUseTermsOption={restrictedDataUseTermsOption}
maxRestrictedUntil={maxRestrictedUntil}
calendarUseModal
setDataUseTerms={mockSetDataUseTerms}
/>,
);

const changeDateButton = screen.getByText('Change date');
fireEvent.click(changeDateButton);

expect(screen.getByText('Change date until which sequences are restricted')).toBeInTheDocument();
});

test('does not use the modal when calendarUseModal is false and renders the inline datepicker instead', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.now().plus({ days: 30 });

render(
<DataUseTermsSelector
initialDataUseTermsOption={restrictedDataUseTermsOption}
maxRestrictedUntil={maxRestrictedUntil}
calendarUseModal={false}
setDataUseTerms={mockSetDataUseTerms}
/>,
);

expect(screen.queryByText('Mon')).toBeInTheDocument();
expect(screen.queryByText('Tue')).toBeInTheDocument();
expect(screen.queryByText('Wed')).toBeInTheDocument();
expect(screen.queryByText('Change date')).not.toBeInTheDocument();
});

test('updates the date when a date is clicked in the inline datepicker', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.fromISO('2077-07-15');

render(
<DataUseTermsSelector
initialDataUseTermsOption={restrictedDataUseTermsOption}
maxRestrictedUntil={maxRestrictedUntil}
calendarUseModal={false}
setDataUseTerms={mockSetDataUseTerms}
/>,
);

const dateButton = screen.getByText('14');
fireEvent.click(dateButton);

expect(mockSetDataUseTerms).toHaveBeenCalledWith({
type: restrictedDataUseTermsOption,
restrictedUntil: '2077-07-14',
});
});

test('updates the date via modal when "Change date" is clicked, a date is selected, and submitted', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.fromISO('2077-07-15');

render(
<DataUseTermsSelector
initialDataUseTermsOption={restrictedDataUseTermsOption}
maxRestrictedUntil={maxRestrictedUntil}
calendarUseModal
setDataUseTerms={mockSetDataUseTerms}
/>,
);

// Open the modal
const changeDateButton = screen.getByText('Change date');
fireEvent.click(changeDateButton);

// Select a date in the modal
const dateButton = screen.getByText('14');
fireEvent.click(dateButton);

// Submit the modal
const submitButton = screen.getByText('Save');
fireEvent.click(submitButton);

expect(mockSetDataUseTerms).toHaveBeenCalledWith({
type: restrictedDataUseTermsOption,
restrictedUntil: '2077-07-14',
});
});

test('renders with no radio input selected when initialDataUseTermsType is not set', () => {
const mockSetDataUseTerms = vi.fn();
const maxRestrictedUntil = DateTime.fromISO('2077-07-15');

render(<DataUseTermsSelector maxRestrictedUntil={maxRestrictedUntil} setDataUseTerms={mockSetDataUseTerms} />);

const openInput = screen.getByLabelText('Open');
const restrictedInput = screen.getByLabelText('Restricted');

expect(openInput).not.toBeChecked();
expect(restrictedInput).not.toBeChecked();
});
});
118 changes: 107 additions & 11 deletions website/src/components/DataUseTerms/DataUseTermsSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,85 @@
import { type FC } from 'react';
import { Datepicker } from 'flowbite-react';
import { DateTime } from 'luxon';
import { useState, type FC } from 'react';

import { DateChangeModal, datePickerTheme } from './DateChangeModal.tsx';
import { getClientLogger } from '../../clientLogger.ts';
import { routes } from '../../routes/routes.ts';
import { type DataUseTermsType, openDataUseTermsType, restrictedDataUseTermsType } from '../../types/backend.ts';
import {
type DataUseTermsOption,
openDataUseTermsOption,
restrictedDataUseTermsOption,
type DataUseTerms,
} from '../../types/backend.ts';
import Locked from '~icons/fluent-emoji-high-contrast/locked';
import Unlocked from '~icons/fluent-emoji-high-contrast/unlocked';

const logger = getClientLogger('DatauseTermsSelector');

type DataUseTermsSelectorProps = {
dataUseTermsType: DataUseTermsType;
setDataUseTermsType: (dataUseTermsType: DataUseTermsType) => void;
initialDataUseTermsOption?: DataUseTermsOption | null;
maxRestrictedUntil: DateTime;
calendarUseModal?: boolean;
calendarDescription?: React.ReactNode;
setDataUseTerms: (dataUseTerms: DataUseTerms) => void;
};

const DataUseTermsSelector: FC<DataUseTermsSelectorProps> = ({ dataUseTermsType, setDataUseTermsType }) => {
const DataUseTermsSelector: FC<DataUseTermsSelectorProps> = ({
initialDataUseTermsOption = null,
maxRestrictedUntil,
calendarUseModal = false,
setDataUseTerms,
calendarDescription = null,
}) => {
const setDataUseTermsWithValues = (newOption: DataUseTermsOption, newDate: DateTime) => {
switch (newOption) {
case openDataUseTermsOption:
setDataUseTerms({ type: openDataUseTermsOption });
break;
case restrictedDataUseTermsOption:
setDataUseTerms({
type: restrictedDataUseTermsOption,
restrictedUntil: newDate.toFormat('yyyy-MM-dd'),
});
break;
}
};

const [selectedOption, setSelectedOptionInternal] = useState<DataUseTermsOption | null>(initialDataUseTermsOption);
const [selectedDate, setSelectedDateInternal] = useState<DateTime>(maxRestrictedUntil);

const setSelectedOption = (newOption: DataUseTermsOption) => {
setSelectedOptionInternal(newOption);
setDataUseTermsWithValues(newOption, selectedDate);
};

const setSelectedDate = (newDate: DateTime) => {
setSelectedOptionInternal(restrictedDataUseTermsOption);
setSelectedDateInternal(newDate);
setDataUseTermsWithValues(restrictedDataUseTermsOption, newDate);
};

const [dateChangeModalOpen, setDateChangeModalOpen] = useState(false);

return (
<>
<div>
{dateChangeModalOpen && (
<DateChangeModal
title='Change date until which sequences are restricted'
description={calendarDescription}
restrictedUntil={selectedDate}
setRestrictedUntil={setSelectedDate}
setDateChangeModalOpen={setDateChangeModalOpen}
maxDate={maxRestrictedUntil}
/>
)}
<div className='flex-1'>
<input
id='data-use-open'
name='data-use'
onChange={() => setDataUseTermsType(openDataUseTermsType)}
onChange={() => setSelectedOption(openDataUseTermsOption)}
type='radio'
checked={dataUseTermsType === openDataUseTermsType}
checked={selectedOption === openDataUseTermsOption}
className='h-4 w-4 p-2 border-gray-300 text-iteal-600 focus:ring-iteal-600 inline-block'
/>
<label htmlFor='data-use-open' className='ml-2 h-4 p-2 text-sm font-medium leading-6 text-gray-900'>
Expand All @@ -35,13 +95,13 @@ const DataUseTermsSelector: FC<DataUseTermsSelectorProps> = ({ dataUseTermsType,
.
</div>
</div>
<div>
<div className='flex-1'>
<input
id='data-use-restricted'
name='data-use'
onChange={() => setDataUseTermsType(restrictedDataUseTermsType)}
onChange={() => setSelectedOption(restrictedDataUseTermsOption)}
type='radio'
checked={dataUseTermsType === restrictedDataUseTermsType}
checked={selectedOption === restrictedDataUseTermsOption}
className='h-4 w-4 border-gray-300 text-iteal-600 focus:ring-iteal-600 inline-block'
/>
<label
Expand All @@ -59,6 +119,42 @@ const DataUseTermsSelector: FC<DataUseTermsSelectorProps> = ({ dataUseTermsType,
</a>
.
</div>
{selectedOption === restrictedDataUseTermsOption && !calendarUseModal && (
<>
{calendarDescription !== null && (
<p className='ml-8 text-xs text-gray-500 mb-4'>{calendarDescription}</p>
)}
<Datepicker
className='ml-8'
defaultValue={selectedDate.toJSDate()}
showClearButton={false}
showTodayButton={false}
minDate={new Date()}
maxDate={maxRestrictedUntil.toJSDate()}
theme={datePickerTheme}
onChange={(date: Date | null) => {
if (date !== null) {
setSelectedDate(DateTime.fromJSDate(date));
} else {
void logger.warn(
"Datepicker onChange received a null value, this shouldn't happen!",
);
}
}}
inline
/>
</>
)}
{selectedOption === restrictedDataUseTermsOption && (
<span className='py-4 text-sm ml-8'>
Data use will be restricted until <b>{selectedDate.toFormat('yyyy-MM-dd')}</b>.{' '}
{calendarUseModal && (
<button className='border rounded px-2 py-1' onClick={() => setDateChangeModalOpen(true)}>
Change date
</button>
)}
</span>
)}
</div>
</>
);
Expand Down
Loading

0 comments on commit 272d34b

Please sign in to comment.