diff --git a/CHANGELOG.md b/CHANGELOG.md index b085e2cd..2b3afe33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added * [#708](https://github.com/shlinkio/shlink-web-client/issues/708) Added support for API v3. +* [#717](https://github.com/shlinkio/shlink-web-client/issues/717) Allowed to select time in 10 minute intervals whe configuring "enabled since" and "enabled until" on short URLs. ### Changed * [#713](https://github.com/shlinkio/shlink-web-client/issues/713) Updated dependencies. diff --git a/src/short-urls/ShortUrlForm.tsx b/src/short-urls/ShortUrlForm.tsx index 5385d784..8c3f6635 100644 --- a/src/short-urls/ShortUrlForm.tsx +++ b/src/short-urls/ShortUrlForm.tsx @@ -3,7 +3,7 @@ import { InputType } from 'reactstrap/types/lib/Input'; import { Button, FormGroup, Input, Row } from 'reactstrap'; import { cond, isEmpty, pipe, replace, trim, T } from 'ramda'; import { parseISO } from 'date-fns'; -import { DateInput, DateInputProps } from '../utils/DateInput'; +import { DateTimeInput, DateTimeInputProps } from '../utils/dates/DateTimeInput'; import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features'; import { SimpleCard } from '../utils/SimpleCard'; import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils'; @@ -83,8 +83,8 @@ export const ShortUrlForm = ( /> ); - const renderDateInput = (id: DateFields, placeholder: string, props: Partial = {}) => ( - = {}) => ( + { - const { className, isClearable, selected } = props; + const { className, isClearable, selected, dateFormat } = props; const showCalendarIcon = !isClearable || isNil(selected); const ref = useRef<{ input: HTMLInputElement }>(); @@ -23,7 +24,7 @@ export const DateInput = (props: DateInputProps) => { options: { padding: 24 }, // This prevents the arrow to be placed on the very edge, which looks ugly }, ]} - dateFormat="yyyy-MM-dd" + dateFormat={dateFormat ?? STANDARD_DATE_FORMAT} className={classNames('date-input-container__input form-control', className)} // @ts-expect-error The DatePicker type definition is wrong. It has a ref prop ref={ref} diff --git a/src/utils/dates/DateRangeRow.tsx b/src/utils/dates/DateRangeRow.tsx index 3712fb86..d2cf1b8d 100644 --- a/src/utils/dates/DateRangeRow.tsx +++ b/src/utils/dates/DateRangeRow.tsx @@ -1,5 +1,5 @@ import { endOfDay } from 'date-fns'; -import { DateInput } from '../DateInput'; +import { DateInput } from './DateInput'; import { DateRange } from './types'; interface DateRangeRowProps extends DateRange { diff --git a/src/utils/dates/DateTimeInput.tsx b/src/utils/dates/DateTimeInput.tsx new file mode 100644 index 00000000..ddc45acb --- /dev/null +++ b/src/utils/dates/DateTimeInput.tsx @@ -0,0 +1,15 @@ +import { ReactDatePickerProps } from 'react-datepicker'; +import { FC } from 'react'; +import { DateInput } from './DateInput'; +import { STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date'; + +export type DateTimeInputProps = Omit; + +export const DateTimeInput: FC = (props) => ( + +); diff --git a/src/utils/Time.tsx b/src/utils/dates/Time.tsx similarity index 70% rename from src/utils/Time.tsx rename to src/utils/dates/Time.tsx index fc4c3571..a06381df 100644 --- a/src/utils/Time.tsx +++ b/src/utils/dates/Time.tsx @@ -1,5 +1,5 @@ import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns'; -import { isDateObject } from './helpers/date'; +import { isDateObject, STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date'; export interface TimeProps { date: Date | string; @@ -7,7 +7,7 @@ export interface TimeProps { relative?: boolean; } -export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: TimeProps) => { +export const Time = ({ date, format = STANDARD_DATE_AND_TIME_FORMAT, relative = false }: TimeProps) => { const dateObject = isDateObject(date) ? date : parseISO(date); return ( diff --git a/src/utils/helpers/date.ts b/src/utils/helpers/date.ts index 75c8b766..9e0a73b5 100644 --- a/src/utils/helpers/date.ts +++ b/src/utils/helpers/date.ts @@ -1,6 +1,10 @@ import { format, formatISO, isBefore, isEqual, isWithinInterval, parse, parseISO as stdParseISO } from 'date-fns'; import { OptionalString } from '../utils'; +export const STANDARD_DATE_FORMAT = 'yyyy-MM-dd'; + +export const STANDARD_DATE_AND_TIME_FORMAT = 'yyyy-MM-dd HH:mm'; + export type DateOrString = Date | string; type NullableDate = DateOrString | null; @@ -15,7 +19,10 @@ const formatDateFromFormat = (date?: NullableDate, theFormat?: string): Optional return theFormat ? format(date, theFormat) : formatISO(date); }; -export const formatDate = (theFormat = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, theFormat); +export const formatDate = (theFormat = STANDARD_DATE_FORMAT) => (date?: NullableDate) => formatDateFromFormat( + date, + theFormat, +); export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined); diff --git a/src/visits/ShortUrlVisitsHeader.tsx b/src/visits/ShortUrlVisitsHeader.tsx index 96046d9f..bac9587f 100644 --- a/src/visits/ShortUrlVisitsHeader.tsx +++ b/src/visits/ShortUrlVisitsHeader.tsx @@ -1,7 +1,7 @@ import { UncontrolledTooltip } from 'reactstrap'; import { ExternalLink } from 'react-external-link'; import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; -import { Time } from '../utils/Time'; +import { Time } from '../utils/dates/Time'; import { ShortUrlVisits } from './reducers/shortUrlVisits'; import { VisitsHeader } from './VisitsHeader'; import './ShortUrlVisitsHeader.scss'; diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index 39e30c48..16181b49 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -10,7 +10,7 @@ import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering'; import { prettify } from '../utils/helpers/numbers'; import { supportsBotVisits } from '../utils/helpers/features'; import { SelectedServer } from '../servers/data'; -import { Time } from '../utils/Time'; +import { Time } from '../utils/dates/Time'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { MediaMatcher } from '../utils/types'; import { NormalizedOrphanVisit, NormalizedVisit } from './types'; diff --git a/src/visits/charts/LineChartCard.tsx b/src/visits/charts/LineChartCard.tsx index a6e4b4ba..8554ff71 100644 --- a/src/visits/charts/LineChartCard.tsx +++ b/src/visits/charts/LineChartCard.tsx @@ -31,6 +31,7 @@ import { prettify } from '../../utils/helpers/numbers'; import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../utils/theme'; import './LineChartCard.scss'; +import { STANDARD_DATE_FORMAT } from '../../utils/helpers/date'; interface LineChartCardProps { title: string; @@ -65,10 +66,10 @@ const STEP_TO_DIFF_FUNC_MAP: Record n const STEP_TO_DATE_FORMAT: Record string> = { hourly: (date) => format(date, 'yyyy-MM-dd HH:00'), - daily: (date) => format(date, 'yyyy-MM-dd'), + daily: (date) => format(date, STANDARD_DATE_FORMAT), weekly(date) { - const firstWeekDay = format(startOfISOWeek(date), 'yyyy-MM-dd'); - const lastWeekDay = format(endOfISOWeek(date), 'yyyy-MM-dd'); + const firstWeekDay = format(startOfISOWeek(date), STANDARD_DATE_FORMAT); + const lastWeekDay = format(endOfISOWeek(date), STANDARD_DATE_FORMAT); return `${firstWeekDay} - ${lastWeekDay}`; }, diff --git a/test/utils/DateInput.test.tsx b/test/utils/dates/DateInput.test.tsx similarity index 68% rename from test/utils/DateInput.test.tsx rename to test/utils/dates/DateInput.test.tsx index e036433c..525b0652 100644 --- a/test/utils/DateInput.test.tsx +++ b/test/utils/dates/DateInput.test.tsx @@ -1,7 +1,8 @@ import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { DateInput, DateInputProps } from '../../src/utils/DateInput'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { parseISO } from 'date-fns'; +import { DateInput, DateInputProps } from '../../../src/utils/dates/DateInput'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const setUp = (props: Partial = {}) => renderWithEvents( @@ -30,4 +31,14 @@ describe('', () => { await user.click(screen.getByPlaceholderText('foo')); await waitFor(() => expect(container.querySelector('.react-datepicker')).toBeInTheDocument()); }); + + it.each([ + [undefined, '2022-01-01'], + ['yyyy-MM-dd', '2022-01-01'], + ['yyyy-MM-dd HH:mm', '2022-01-01 15:18'], + ['HH:mm:ss', '15:18:36'], + ])('shows date in expected format', (dateFormat, expectedValue) => { + setUp({ placeholderText: 'foo', selected: parseISO('2022-01-01T15:18:36'), dateFormat }); + expect(screen.getByPlaceholderText('foo')).toHaveValue(expectedValue); + }); }); diff --git a/test/utils/Time.test.tsx b/test/utils/dates/Time.test.tsx similarity index 87% rename from test/utils/Time.test.tsx rename to test/utils/dates/Time.test.tsx index cb1f9c8f..38b38a47 100644 --- a/test/utils/Time.test.tsx +++ b/test/utils/dates/Time.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react'; -import { TimeProps, Time } from '../../src/utils/Time'; -import { parseDate } from '../../src/utils/helpers/date'; +import { TimeProps, Time } from '../../../src/utils/dates/Time'; +import { parseDate } from '../../../src/utils/helpers/date'; describe('