diff --git a/cypress/component/DateRangeWidget.cy.tsx b/cypress/component/DateRangeWidget.cy.tsx index 6b8f0fa..cbd071b 100644 --- a/cypress/component/DateRangeWidget.cy.tsx +++ b/cypress/component/DateRangeWidget.cy.tsx @@ -38,15 +38,17 @@ describe('', () => { dataset: 'fake', inputs: { date_range_start: '2023-09-30', - date_range_end: '2025-02-10' + date_range_end: '2023-10-10' } }) ) + const configuration = getDateRangeWidgetConfiguration() + cy.mount(
diff --git a/src/common/DateField.tsx b/src/common/DateField.tsx index c112403..fb96f8b 100644 --- a/src/common/DateField.tsx +++ b/src/common/DateField.tsx @@ -25,7 +25,6 @@ import { import { CalendarDate } from '@internationalized/date' import { Error, ReservedSpace } from '../widgets/Widget' -import { SingleSelect } from './Select' interface DateFieldProps { name: string @@ -39,6 +38,8 @@ interface DateFieldProps { error?: string disabled?: boolean required?: boolean + years?: number[] + months?: number[] } const DateField = ({ name, @@ -51,7 +52,9 @@ const DateField = ({ isDateUnavailable, error, disabled, - required + required, + months, + years }: DateFieldProps) => { return ( - - - - - - - + {months && months.length > 0 && years && years.length > 0 ? ( + + ) : ( + + )} {date => } @@ -116,58 +122,76 @@ const Months = [ ] interface DateSelectsProps { - name: string - selectedYear: number - selectedMonth: number + value: CalendarDate years: number[] months: number[] - onYearChange(year: number): void - onMonthChange(month: number): void -} -const DateSelects = ({ - name, - selectedYear, - selectedMonth, - years, - months, - onYearChange, - onMonthChange -}: DateSelectsProps) => { - const yearOptions = React.useMemo( - () => years.map(y => ({ id: y.toString(), label: y.toString() })), - [years] - ) - - const monthOptions = React.useMemo( - () => - months.map(m => ({ - id: m.toString(), - label: Months[m] - })), - [months] - ) - - return ( - - onMonthChange(parseInt(value))} - options={monthOptions} - placeholder='Select month' - /> - onYearChange(parseInt(value))} - options={yearOptions} - placeholder='Select year' - /> - - ) + onDateChange(date: CalendarDate): void } +const DateSelects = React.memo( + ({ value, years, months, onDateChange }: DateSelectsProps) => { + const yearOptions = React.useMemo(() => { + const baseYears = years.map(y => ({ + id: y.toString(), + label: y.toString(), + disabled: false + })) + if (!baseYears.find(({ id }) => value.year.toString() === id)) { + return baseYears.concat({ + id: value.year.toString(), + label: value.year.toString(), + disabled: true + }) + } + return baseYears + }, [years, value]) + + const monthOptions = React.useMemo( + () => + months.map((m, i) => ({ + id: (m + 1).toString(), + label: Months[m] + })), + [months] + ) + + const handleChange = + (key: 'month' | 'year') => + ({ + target: { value: selectValue } + }: React.ChangeEvent) => { + const newDate = value.set({ [key]: parseInt(selectValue) }) + console.log(selectValue, newDate.month) + onDateChange(newDate) + } + + return ( + + + + + ) + } +) const Row = styled.div` width: 100%; diff --git a/src/widgets/DateRangeWidget.tsx b/src/widgets/DateRangeWidget.tsx index d50fc8c..913cef1 100644 --- a/src/widgets/DateRangeWidget.tsx +++ b/src/widgets/DateRangeWidget.tsx @@ -2,7 +2,13 @@ import React from 'react' import styled from 'styled-components' import { DateValue } from 'react-aria-components' -import { parseDate } from '@internationalized/date' +import { + Calendar, + CalendarDate, + parseDate, + maxDate, + minDate +} from '@internationalized/date' import { Error, @@ -13,11 +19,154 @@ import { WidgetHeader, WidgetTitle } from './Widget' -import { DateField } from '../index' -import { WidgetTooltip } from '../index' +import { DateField } from '../common/DateField' +import { WidgetTooltip } from '../common/WidgetTooltip' import { useBypassRequired } from '../utils' import { useReadLocalStorage } from 'usehooks-ts' +type ValidateDateFn = ( + startDate: CalendarDate, + endDate: CalendarDate, + minStart: string, + maxEnd: string, + isDateUnavailable: (date: DateValue) => boolean +) => string | undefined +const getStartDateErrors: ValidateDateFn = ( + startDate, + endDate, + minStart, + maxEnd, + isDateUnavailable +) => { + const fMinDate = parseDate(minStart), + fMaxDate = parseDate(maxEnd) + + if (!startDate) { + return 'Date is no valid' + } + + if (!endDate) { + return '' + } + + if (!startDate) { + return 'Start date is required' + } + + if (startDate.compare(endDate) > 0) { + return 'Start date should be above to End date' + } + + if (startDate.compare(fMaxDate) > 0) { + return `Start date cannot exceed the deadline (${fMaxDate.toString()})` + } + + if (startDate.compare(fMinDate) < 0) { + return `Start date cannot be set earlier than the minimum date (${fMinDate.toString()}) ` + } + + if (isDateUnavailable(startDate)) { + return `Date is not valid` + } +} + +const getEndDateErrors: ValidateDateFn = ( + startDate, + endDate, + minStart, + maxEnd, + isDateUnavailable +) => { + const fMinDate = parseDate(minStart), + fMaxDate = parseDate(maxEnd) + + if (!endDate) { + return 'Date is no valid' + } + + if (!startDate) { + return '' + } + + if (!endDate) { + return 'End date is required' + } + + if (endDate.compare(startDate) < 0) { + return 'End date cannot be earlier than Start date' + } + + if (endDate.compare(fMaxDate) > 0) { + return `End date cannot exceed the deadline (${fMaxDate.toString()})` + } + + if (endDate.compare(fMinDate) < 0) { + return `End date cannot be set earlier than the deadline (${fMinDate.toString()}) ` + } + + if (isDateUnavailable(startDate)) { + return `Date is not valid` + } +} + +const getDateLimits = ( + startDate: CalendarDate, + endDate: CalendarDate, + minStart: string, + maxEnd: string +) => { + const fMinStart = parseDate(minStart), + fMaxEnd = parseDate(maxEnd) + + let startMinDate = fMinStart, + startMaxDate = endDate, + endMinDate = startDate, + endMaxDate = fMaxEnd + + if (startMinDate.compare(startMaxDate) > 0) { + startMaxDate = maxDate(startDate, startMinDate) + } + + if (endMaxDate.compare(endMinDate) < 0) { + endMaxDate = minDate(endDate, endMaxDate) + } + + return { + startMinDate, + endMinDate, + startMaxDate, + endMaxDate + } +} + +const getAvailableYears = (minDate: CalendarDate, maxDate: CalendarDate) => { + const delta = maxDate.year - minDate.year + if (delta <= 0) { + return [minDate.year] + } + + return new Array(delta).fill(0).map((_, i) => minDate.year + i) +} + +const getAvailableMonths = () => + // date: CalendarDate, + // minDate: CalendarDate, + // maxDate: CalendarDate + { + // if (date.year !== minDate.year && date.year !== maxDate.year) { + // if (date.year === minDate.year && date.year === maxDate.year) { + // const delta = maxDate.month - minDate.month; + // return new Array(delta).map((_, i) => minDate.month + i); + // } + + // if (date.year === minDate.year) { + + // } + // } + + return new Array(12).fill(1).map((_, i) => i) + } + interface DateRangeWidgetConfiguration { type: 'DateRangeWidget' help: string | null @@ -94,8 +243,6 @@ const DateRangeWidget = ({ setEndDate(d => endDate ?? d) }, [configuration]) - console.log(persistedSelection) - const isDateUnavailable = React.useCallback( (date: DateValue) => { return Boolean(constraints?.find(d => parseDate(d).compare(date) === 0)) @@ -103,71 +250,49 @@ const DateRangeWidget = ({ [constraints] ) - const startDateError = React.useMemo(() => { - const fMinDate = parseDate(configuration.details.minStart), - fMaxDate = parseDate(configuration.details.maxEnd) - - if (!startDate) { - return 'Date is no valid' - } - - if (!endDate) { - return '' - } - - if (!startDate) { - return 'Start date is required' - } - - if (startDate.compare(endDate) > 0) { - return 'Start date should be above to End date' - } - - if (startDate.compare(fMaxDate) > 0) { - return `Start date cannot exceed the deadline (${fMaxDate.toString()})` - } - - if (startDate.compare(fMinDate) < 0) { - return `Start date cannot be set earlier than the minimum date (${fMinDate.toString()}) ` - } - - if (isDateUnavailable(startDate)) { - return `Date is not valid` - } - }, [startDate, endDate, configuration.details, isDateUnavailable]) - - const endDateError = React.useMemo(() => { - const fMinDate = parseDate(configuration.details.minStart), - fMaxDate = parseDate(configuration.details.maxEnd) - - if (!endDate) { - return 'Date is no valid' - } + const startDateError = React.useMemo( + () => + getStartDateErrors( + startDate, + endDate, + configuration.details.minStart, + configuration.details.maxEnd, + isDateUnavailable + ), + [startDate, endDate, configuration.details, isDateUnavailable] + ) - if (!startDate) { - return '' - } + const endDateError = React.useMemo( + () => + getEndDateErrors( + startDate, + endDate, + configuration.details.minStart, + configuration.details.maxEnd, + isDateUnavailable + ), + [startDate, endDate, configuration.details, isDateUnavailable] + ) - if (!endDate) { - return 'End date is required' - } + const { startMinDate, startMaxDate, endMinDate, endMaxDate } = + React.useMemo(() => { + return getDateLimits( + startDate, + endDate, + configuration.details.minStart, + configuration.details.maxEnd + ) + }, [startDate, endDate, configuration.details]) - if (endDate.compare(startDate) < 0) { - return 'End date cannot be earlier than Start date' - } + const startYears = React.useMemo(() => { + return getAvailableYears(startMinDate, endDate) + }, [startMinDate, endDate.year]) - if (endDate.compare(fMaxDate) > 0) { - return `End date cannot exceed the deadline (${fMaxDate.toString()})` - } - - if (endDate.compare(fMinDate) < 0) { - return `End date cannot be set earlier than the deadline (${fMinDate.toString()}) ` - } + const endYears = React.useMemo(() => { + return getAvailableYears(startDate, startMaxDate) + }, [startDate.year, startMaxDate]) - if (isDateUnavailable(startDate)) { - return `Date is not valid` - } - }, [startDate, endDate, configuration.details, isDateUnavailable]) + const months = React.useMemo(() => getAvailableMonths(), []) return ( @@ -196,11 +321,13 @@ const DateRangeWidget = ({ label='Start date' error={startDateError} defaultValue={parseDate(configuration.details.defaultStart)} - minStart={parseDate(configuration.details.minStart)} - maxEnd={endDate} + minStart={startMinDate} + maxEnd={startMaxDate} isDateUnavailable={isDateUnavailable} disabled={disabled} required={configuration.required} + years={startYears} + months={months} />