Skip to content

Commit

Permalink
fixup! feat(components): gs-date-range-selector: add attributes `init…
Browse files Browse the repository at this point in the history
…ialDateFrom`, `initialDateTo` #245
  • Loading branch information
fengelniederhammer committed May 28, 2024
1 parent 12b8302 commit 5528222
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest';

import { computeInitialValues } from './computeInitialValues';
import { PRESET_VALUE_CUSTOM, PRESET_VALUE_LAST_3_MONTHS, PRESET_VALUE_LAST_6_MONTHS } from './selectableOptions';

const today = new Date();
const earliestDate = '1900-01-01';

describe('computeInitialValues', () => {
it('should compute for initial value if initial "from" and "to" are unset', () => {
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, undefined, earliestDate, []);

const expectedFrom = new Date();
expectedFrom.setMonth(today.getMonth() - 3);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_3_MONTHS);
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
expectDateMatches(result.initialSelectedDateTo, today);
});

it('should fall back to default when initial value is unknown', () => {
const result = computeInitialValues('not a known value', undefined, undefined, earliestDate, []);

const expectedFrom = new Date();
expectedFrom.setMonth(today.getMonth() - 6);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_6_MONTHS);
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
expectDateMatches(result.initialSelectedDateTo, today);
});

it('should overwrite initial value if initial "from" is set', () => {
const initialDateFrom = '2020-01-01';
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, initialDateFrom, undefined, earliestDate, []);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
expectDateMatches(result.initialSelectedDateTo, today);
});

it('should overwrite initial value if initial "to" is set', () => {
const initialDateTo = '2020-01-01';
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, initialDateTo, earliestDate, []);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
});

it('should overwrite initial value if initial "to" and "from" are set', () => {
const initialDateFrom = '2020-01-01';
const initialDateTo = '2022-01-01';
const result = computeInitialValues(
PRESET_VALUE_LAST_3_MONTHS,
initialDateFrom,
initialDateTo,
earliestDate,
[],
);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
});

it('should set initial "to" to "from" if "from" is after "to"', () => {
const initialDateFrom = '2020-01-01';
const initialDateTo = '1900-01-01';
const result = computeInitialValues(
PRESET_VALUE_LAST_3_MONTHS,
initialDateFrom,
initialDateTo,
earliestDate,
[],
);

expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateFrom));
});

it('should throw if initial "from" is not a valid date', () => {
expect(() =>
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, 'not a date', undefined, earliestDate, []),
).toThrowError('Invalid initialDateFrom');
});

it('should throw if initial "to" is not a valid date', () => {
expect(() =>
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, 'not a date', earliestDate, []),
).toThrowError('Invalid initialDateTo');
});

function expectDateMatches(actual: Date, expected: Date) {
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
}
});
73 changes: 73 additions & 0 deletions components/src/preact/dateRangeSelector/computeInitialValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
type CustomSelectOption,
getDatesForSelectorValue,
getSelectableOptions,
PRESET_VALUE_CUSTOM,
PRESET_VALUE_LAST_6_MONTHS,
type PresetOptionValues,
} from './selectableOptions';
import { UserFacingError } from '../components/error-display';

export function computeInitialValues<CustomLabel extends string>(
initialValue: PresetOptionValues | CustomLabel | undefined,
initialDateFrom: string | undefined,
initialDateTo: string | undefined,
earliestDate: string,
customSelectOptions: CustomSelectOption<CustomLabel>[],
): {
initialSelectedDateRange: CustomLabel | PresetOptionValues;
initialSelectedDateFrom: Date;
initialSelectedDateTo: Date;
} {
if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
const selectableOptions = getSelectableOptions(customSelectOptions);
const initialSelectedDateRange =
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
? initialValue
: PRESET_VALUE_LAST_6_MONTHS;

const { dateFrom, dateTo } = getDatesForSelectorValue(
initialSelectedDateRange,
customSelectOptions,
earliestDate,
);

return {
initialSelectedDateRange,
initialSelectedDateFrom: dateFrom,
initialSelectedDateTo: dateTo,
};
}

const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
? new Date(earliestDate)
: new Date(initialDateFrom);
let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);

if (isNaN(initialSelectedDateFrom.getTime())) {
throw new UserFacingError(
'Invalid initialDateFrom',
`Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
);
}
if (isNaN(initialSelectedDateTo.getTime())) {
throw new UserFacingError(
'Invalid initialDateTo',
`Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
);
}

if (initialSelectedDateFrom > initialSelectedDateTo) {
initialSelectedDateTo = initialSelectedDateFrom;
}

return {
initialSelectedDateRange: PRESET_VALUE_CUSTOM,
initialSelectedDateFrom,
initialSelectedDateTo,
};
}

function isUndefinedOrEmpty(value: string | undefined): value is undefined | '' {
return value === undefined || value === '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import { type Meta, type StoryObj } from '@storybook/preact';
import { expect, waitFor, within } from '@storybook/test';
import dayjs from 'dayjs/esm';

import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
import {
DateRangeSelector,
type DateRangeSelectorProps,
PRESET_VALUE_ALL_TIMES,
PRESET_VALUE_CUSTOM,
PRESET_VALUE_LAST_2_MONTHS,
PRESET_VALUE_LAST_2_WEEKS,
PRESET_VALUE_LAST_3_MONTHS,
PRESET_VALUE_LAST_6_MONTHS,
PRESET_VALUE_LAST_MONTH,
} from './date-range-selector';
} from './selectableOptions';
import { LAPIS_URL } from '../../constants';
import { LapisUrlContext } from '../LapisUrlContext';

Expand Down
152 changes: 7 additions & 145 deletions components/src/preact/dateRangeSelector/date-range-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import flatpickr from 'flatpickr';
import 'flatpickr/dist/flatpickr.min.css';
import { useEffect, useRef, useState } from 'preact/hooks';

import { computeInitialValues } from './computeInitialValues';
import { toYYYYMMDD } from './dateConversion';
import {
type CustomSelectOption,
getDatesForSelectorValue,
getSelectableOptions,
type PresetOptionValues,
} from './selectableOptions';
import { ErrorBoundary } from '../components/error-boundary';
import { UserFacingError } from '../components/error-display';
import { ResizeContainer } from '../components/resize-container';
import { Select } from '../components/select';
import type { ScaleType } from '../shared/charts/getYAxisScale';

export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };

export interface DateRangeSelectorProps<CustomLabel extends string> extends DateRangeSelectorPropsInner<CustomLabel> {
width: string;
}
Expand All @@ -24,26 +28,6 @@ export interface DateRangeSelectorPropsInner<CustomLabel extends string> {
dateColumn: string;
}

export const PRESET_VALUE_CUSTOM = 'custom';
export const PRESET_VALUE_ALL_TIMES = 'allTimes';
export const PRESET_VALUE_LAST_2_WEEKS = 'last2Weeks';
export const PRESET_VALUE_LAST_MONTH = 'lastMonth';
export const PRESET_VALUE_LAST_2_MONTHS = 'last2Months';
export const PRESET_VALUE_LAST_3_MONTHS = 'last3Months';
export const PRESET_VALUE_LAST_6_MONTHS = 'last6Months';

export const presets = {
[PRESET_VALUE_CUSTOM]: { label: 'Custom' },
[PRESET_VALUE_ALL_TIMES]: { label: 'All times' },
[PRESET_VALUE_LAST_2_WEEKS]: { label: 'Last 2 weeks' },
[PRESET_VALUE_LAST_MONTH]: { label: 'Last month' },
[PRESET_VALUE_LAST_2_MONTHS]: { label: 'Last 2 months' },
[PRESET_VALUE_LAST_3_MONTHS]: { label: 'Last 3 months' },
[PRESET_VALUE_LAST_6_MONTHS]: { label: 'Last 6 months' },
};

export type PresetOptionValues = keyof typeof presets;

export const DateRangeSelector = <CustomLabel extends string>({
customSelectOptions,
earliestDate = '1900-01-01',
Expand Down Expand Up @@ -226,125 +210,3 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
</div>
);
};

function computeInitialValues<CustomLabel extends string>(
initialValue: PresetOptionValues | CustomLabel | undefined,
initialDateFrom: string | undefined,
initialDateTo: string | undefined,
earliestDate: string,
customSelectOptions: CustomSelectOption<CustomLabel>[],
): {
initialSelectedDateRange: CustomLabel | PresetOptionValues;
initialSelectedDateFrom: Date;
initialSelectedDateTo: Date;
} {
if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
const selectableOptions = getSelectableOptions(customSelectOptions);
const initialSelectedDateRange =
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
? initialValue
: PRESET_VALUE_LAST_6_MONTHS;

const { dateFrom, dateTo } = getDatesForSelectorValue(
initialSelectedDateRange,
customSelectOptions,
earliestDate,
);

return {
initialSelectedDateRange,
initialSelectedDateFrom: dateFrom,
initialSelectedDateTo: dateTo,
};
}

const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
? new Date(earliestDate)
: new Date(initialDateFrom);
let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);

if (isNaN(initialSelectedDateFrom.getTime())) {
throw new UserFacingError(
'Invalid initialDateFrom',
`Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
);
}
if (isNaN(initialSelectedDateTo.getTime())) {
throw new UserFacingError(
'Invalid initialDateTo',
`Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
);
}

if (initialSelectedDateFrom > initialSelectedDateTo) {
initialSelectedDateTo = initialSelectedDateFrom;
}

return {
initialSelectedDateRange: PRESET_VALUE_CUSTOM,
initialSelectedDateFrom,
initialSelectedDateTo,
};
}

function isUndefinedOrEmpty(value: string | undefined): value is undefined | '' {
return value === undefined || value === '';
}

const getSelectableOptions = <Label extends string>(customSelectOptions: CustomSelectOption<Label>[]) => {
const presetOptions = Object.entries(presets).map(([key, value]) => {
return { label: value.label, value: key };
});

const customOptions = customSelectOptions.map((customSelectOption) => {
return { label: customSelectOption.label, value: customSelectOption.label };
});

return [...presetOptions, ...customOptions];
};

const getDatesForSelectorValue = <Label extends string>(
selectorValue: string,
customSelectOptions: CustomSelectOption<Label>[],
earliestDate: string,
) => {
const today = new Date();

const customSelectOption = customSelectOptions.find((option) => option.label === selectorValue);
if (customSelectOption) {
return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
}

switch (selectorValue) {
case PRESET_VALUE_LAST_2_WEEKS: {
const twoWeeksAgo = new Date(today);
twoWeeksAgo.setDate(today.getDate() - 14);
return { dateFrom: twoWeeksAgo, dateTo: today };
}
case PRESET_VALUE_LAST_MONTH: {
const lastMonth = new Date(today);
lastMonth.setMonth(today.getMonth() - 1);
return { dateFrom: lastMonth, dateTo: today };
}
case PRESET_VALUE_LAST_2_MONTHS: {
const twoMonthsAgo = new Date(today);
twoMonthsAgo.setMonth(today.getMonth() - 2);
return { dateFrom: twoMonthsAgo, dateTo: today };
}
case PRESET_VALUE_LAST_3_MONTHS: {
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
return { dateFrom: threeMonthsAgo, dateTo: today };
}
case PRESET_VALUE_LAST_6_MONTHS: {
const sixMonthsAgo = new Date(today);
sixMonthsAgo.setMonth(today.getMonth() - 6);
return { dateFrom: sixMonthsAgo, dateTo: today };
}
case PRESET_VALUE_ALL_TIMES: {
return { dateFrom: new Date(earliestDate), dateTo: today };
}
default:
return { dateFrom: today, dateTo: today };
}
};
Loading

0 comments on commit 5528222

Please sign in to comment.