diff --git a/components/calendar/package.json b/components/calendar/package.json index 54128c44e..4b1d2bfe5 100644 --- a/components/calendar/package.json +++ b/components/calendar/package.json @@ -38,7 +38,7 @@ "@dhis2-ui/input": "10.16.4", "@dhis2-ui/layer": "10.16.4", "@dhis2-ui/popper": "10.16.4", - "@dhis2/multi-calendar-dates": "2.1.2", + "@dhis2/multi-calendar-dates": "3.0.0-alpha.1", "@dhis2/prop-types": "^3.1.2", "@dhis2/ui-constants": "10.16.4", "@dhis2/ui-icons": "10.16.4", diff --git a/components/calendar/src/calendar-input/CLAUDE.md b/components/calendar/src/calendar-input/CLAUDE.md new file mode 100644 index 000000000..bf430fd96 --- /dev/null +++ b/components/calendar/src/calendar-input/CLAUDE.md @@ -0,0 +1,24 @@ +# CalendarInput + +`CalendarInput` wraps the `Calendar` widget (`../calendar/`) with a text input, validation, and a clear button. Both get their date logic — parsing, localization, navigation — from `@dhis2/multi-calendar-dates`'s `useDatePicker`/`useResolvedDirection`/`validateDateString`; this directory has no date-math of its own. + +## Data flow + +Dates cross the public API as calendar-native `YYYY-MM-DD` strings (in whatever `calendar` prop is set — not necessarily Gregorian ISO), never as `Temporal` objects. Treat any change that would leak a `Temporal`/date-engine object through `onDateSelect`, `date`, or similar props as a breaking change. `CalendarInput`'s `onDateSelect` payload also carries a `validation` object that `CalendarInput` computes itself via `validateDateString` — it is *not* part of `multi-calendar-dates`'s types, so it's declared separately in `../../types/index.d.ts` (`CalendarInputValidation` / `CalendarInputOnDateSelectPayload`). If you change how validation is computed or shaped, update that file too. + +## Testing + +Two surfaces, used for different things: +- **Jest** (`__tests__/calendar-input.test.js`, run via `yarn test`) — component behavior and logic: prop handling, validation, callbacks. +- **Cypress + Cucumber e2e** (`../features/*.feature` + step defs, driven off `../__e2e__/calendar-input.e2e.stories.js`, run via `yarn cy:test` at the repo root) — real rendered output and localization across calendars. Heavier; not run by `yarn test`. + +When adding a prop or behavior, add a Jest test. If it's about rendering or localized text for a specific calendar/locale, check whether a `.feature` scenario should exist too. + +Coverage decisions in this component are scoped to the three calendars the project actually supports in practice: **Gregorian, Ethiopic, Nepali**. Ethiopic and Nepali behave differently enough from Gregorian (Ethiopic is a native-ICU calendar with its own era handling; Nepali is a custom, non-ICU calendar implemented in `multi-calendar-dates` via a lookup table, not `Intl`) that "tested for Gregorian" should not be assumed to mean "tested for the other two" — verify explicitly rather than assuming parity. + +## Gotchas + +- **Day-cell tests must assert on visible text, not just the `data-test` attribute.** `data-test` on a day cell is always the full date string (e.g. `2024-10-17`); the *rendered label* is a separate value from `multi-calendar-dates` and can silently regress into something else (it has, historically) without any `data-test`-based assertion catching it. Assert `within(cell).getByText('17')` (or the calendar-appropriate bare day number) in addition to clicking by `data-test`. +- **`useDatePicker` recomputes on every render of `CalendarInput`, regardless of whether the calendar dropdown is open.** Its output must stay properly memoized inside `multi-calendar-dates` — if you find yourself adding expensive work anywhere in this render path (here or upstream), verify it doesn't run on every keystroke. The `performance (LIBS-763)` describe block in the test file guards against this regressing again; keep it green. +- **`inputWidth` is deliberately untested.** It's a passthrough to a `styled-jsx` CSS width — purely cosmetic, and `styled-jsx` output is fragile to assert on in jsdom. Don't assume this is an oversight. +- **`minDate`/`maxDate`/`format`/`strictValidation` get passed to two different functions with different expectations**: `validateDateString` (via `validationOptions`, keyed `minDateString`/`maxDateString`) drives the visible error text, while `useDatePicker` takes the same values under its own key names (`minDate`/`maxDate`) to decide whether to fall back to today internally in strict mode. These used to silently disagree (the `validationOptions` object, keyed for `validateDateString`, was spread straight into `useDatePicker` too) — the widget would keep an out-of-range date selected in strict mode even though the input correctly rejected it. Pass `useDatePicker` its own correctly-named fields explicitly; don't assume an options object built for one of these functions is safe to reuse for the other. diff --git a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js index d4d06469b..b63046684 100644 --- a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js +++ b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js @@ -40,6 +40,86 @@ describe('Calendar Input', () => { jest.useRealTimers() }) + it('allow selection of a date through the calendar widget (ethiopic)', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const onDateSelectMock = jest.fn() + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.focus(dateInput) + + const calendar = await screen.findByTestId('calendar') + expect(calendar).toBeInTheDocument() + + const todayString = '2017-02-12' + const today = within(calendar).getByTestId(todayString) + + // the visible day-cell text must be the bare day number, not the + // full date string used for data-test - regressed silently during + // the multi-calendar-dates 2.2.0-alpha.1 upgrade (see CLAUDE.md) + expect(within(today).getByText('12')).toBeInTheDocument() + + fireEvent.click(today) + + await waitFor(() => { + expect(calendar).not.toBeInTheDocument() + }) + expect(onDateSelectMock).toHaveBeenCalledWith( + expect.objectContaining({ + calendarDateString: todayString, + }) + ) + + jest.useRealTimers() + }) + + it('allow selection of a date through the calendar widget (nepali)', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const onDateSelectMock = jest.fn() + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.focus(dateInput) + + const calendar = await screen.findByTestId('calendar') + expect(calendar).toBeInTheDocument() + + const todayString = '2081-07-06' + const today = within(calendar).getByTestId(todayString) + + // the visible day-cell text must be the bare day number, not the + // full date string used for data-test - regressed silently during + // the multi-calendar-dates 2.2.0-alpha.1 upgrade (see CLAUDE.md) + expect(within(today).getByText('6')).toBeInTheDocument() + + fireEvent.click(today) + + await waitFor(() => { + expect(calendar).not.toBeInTheDocument() + }) + expect(onDateSelectMock).toHaveBeenCalledWith( + expect.objectContaining({ + calendarDateString: todayString, + }) + ) + + jest.useRealTimers() + }) + it('allow selection of a date through the input', async () => { const onDateSelectMock = jest.fn() const screen = render( @@ -61,7 +141,291 @@ describe('Calendar Input', () => { ) }) + it('rejects input whose detected shape does not match the format prop', () => { + // "2024-12-25" is unambiguously YYYY-MM-DD shaped (4-digit group + // first). With format="DD-MM-YYYY" that mismatch is rejected + // rather than reinterpreted. + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.change(dateInput, { target: { value: '2024-12-25' } }) + fireEvent.blur(dateInput) + + expect( + screen.getByText( + 'Date string format does not match the specified format. Expected DD-MM-YYYY but got YYYY-MM-DD.' + ) + ).toBeInTheDocument() + }) + + it('accepts the same typed input without the format prop', () => { + const screen = render() + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.change(dateInput, { target: { value: '2024-12-25' } }) + fireEvent.blur(dateInput) + + expect( + screen.queryByText(/does not match the specified format/) + ).not.toBeInTheDocument() + }) + + it('limits the year selector to the current year when pastOnly is set', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + + const yearSelect = await screen.findByTestId('calendar-year-select') + const years = within(yearSelect) + .getAllByRole('option') + .map((option) => Number(option.value)) + + expect(Math.max(...years)).toBe(2024) + + jest.useRealTimers() + }) + + it('includes future years in the year selector without pastOnly', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + + const yearSelect = await screen.findByTestId('calendar-year-select') + const years = within(yearSelect) + .getAllByRole('option') + .map((option) => Number(option.value)) + + expect(Math.max(...years)).toBeGreaterThan(2024) + + jest.useRealTimers() + }) + + it('sets the calendar wrapper direction from the dir prop', async () => { + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + + const calendar = await screen.findByTestId('calendar') + expect(calendar).toHaveAttribute('dir', 'rtl') + }) + + it('renders day labels using the numberingSystem prop', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + + const calendar = await screen.findByTestId('calendar') + const today = within(calendar).getByTestId('2024-10-22') + + expect(today).toHaveTextContent('٢٢') + + jest.useRealTimers() + }) + + it('renders weekday headers using the weekDayFormat prop', async () => { + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + + const calendar = await screen.findByTestId('calendar') + expect(within(calendar).getByText('Monday')).toBeInTheDocument() + }) + + // Regression coverage for LIBS-763: useDatePicker (in @dhis2/multi-calendar-dates) + // used to rebuild the year/month navigation lists and the day-cell grid on every + // render - including every keystroke while typing here, with the calendar widget + // open - regardless of whether the visible month/calendar/locale had actually + // changed. Fixed upstream in @dhis2/multi-calendar-dates@3.0.0-alpha.1. + describe('performance (LIBS-763)', () => { + // For gregory/ethiopic (native-ICU calendars) the broken memoization showed + // up as ~500 redundant `new Intl.DateTimeFormat(...)` constructions for a + // single keystroke; count is a deterministic, machine-speed-independent + // signal, unlike timing it directly. + const countDateTimeFormatConstructions = (fn) => { + const realResolvedOptions = + Intl.DateTimeFormat.prototype.resolvedOptions + let count = 0 + Intl.DateTimeFormat.prototype.resolvedOptions = function ( + ...args + ) { + count++ + return realResolvedOptions.apply(this, args) + } + try { + fn() + } finally { + Intl.DateTimeFormat.prototype.resolvedOptions = + realResolvedOptions + } + return count + } + + it('does not reconstruct Intl.DateTimeFormat instances while typing (gregory)', async () => { + const screen = render( + + ) + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + await screen.findByTestId('calendar') + + const constructions = countDateTimeFormatConstructions(() => { + fireEvent.change(dateInput, { + target: { value: '2024-10-1' }, + }) + }) + + expect(constructions).toBeLessThan(10) + }) + + it('does not reconstruct Intl.DateTimeFormat instances while typing (ethiopic)', async () => { + const screen = render( + + ) + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + await screen.findByTestId('calendar') + + const constructions = countDateTimeFormatConstructions(() => { + fireEvent.change(dateInput, { + target: { value: '2017-02-0' }, + }) + }) + + expect(constructions).toBeLessThan(10) + }) + + // Nepali is a custom (non-ICU) calendar, so it never goes through + // Intl.DateTimeFormat - the same broken memoization instead showed up as + // redundant custom-calendar date-conversion work, so time it directly. + // 300ms for 10 keystrokes is ~25x the fixed baseline (~13ms) and ~2.5x + // below the broken one (~760ms), leaving headroom in both directions. + // Skipped in CI: unlike the count-based tests above, this asserts on + // wall-clock time, which can be flaky on slower/shared CI runners. + // Uncomment to run locally when touching useDatePicker/useNavigation. + it.skip('keeps typing fast (nepali)', async () => { + const screen = render( + + ) + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + fireEvent.focus(dateInput) + await screen.findByTestId('calendar') + + const chars = '2081-07-01'.split('') + const progressive = chars.map((_, i) => + chars.slice(0, i + 1).join('') + ) + + const start = process.hrtime.bigint() + progressive.forEach((value) => + fireEvent.change(dateInput, { target: { value } }) + ) + const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6 + + expect(elapsedMs).toBeLessThan(300) + }) + }) + describe('validation', () => { + it('should show a warning instead of an error when strictValidation is false', () => { + const onDateSelectMock = jest.fn() + const screen = render( + + ) + + const dateInputString = '2023-10-12' + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.change(dateInput, { target: { value: dateInputString } }) + fireEvent.blur(dateInput) + + expect( + screen.getByText( + 'Date 2023-10-12 is less than the minimum allowed date 2024-01-01.' + ) + ).toBeInTheDocument() + expect(onDateSelectMock).toHaveBeenCalledWith( + expect.objectContaining({ + validation: expect.objectContaining({ + warning: true, + valid: true, + }), + }) + ) + }) it('should validate minimum date', () => { const onDateSelectMock = jest.fn() const screen = render( @@ -87,6 +451,83 @@ describe('Calendar Input', () => { ).toBeInTheDocument() expect(onDateSelectMock).toHaveBeenCalledTimes(1) }) + + // Regression test: minDate/maxDate used to reach validateDateString + // (as minDateString/maxDateString) but not useDatePicker itself, due to a + // key mismatch - so in strict mode the input rejected an out-of-range + // date while the widget still selected it. Warning mode is unaffected + // (see the test below) since useDatePicker only falls back to today when + // strictValidation produces an `error`. + it('calendar widget falls back to today for a date the input has flagged as below the minimum (strict mode)', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.change(dateInput, { target: { value: '2020-01-15' } }) + fireEvent.blur(dateInput) + + expect( + screen.getByText( + 'Date 2020-01-15 is less than the minimum allowed date 2024-01-01.' + ) + ).toBeInTheDocument() + + fireEvent.focus(dateInput) + const calendar = await screen.findByTestId('calendar') + + // expected: the widget falls back to today, like useDatePicker's own + // getInvalidDateResult does for a date it recognizes as invalid + expect( + within(calendar).getByTestId('2024-10-22').querySelector('button') + ).toHaveClass('isToday') + + jest.useRealTimers() + }) + + it('calendar widget correctly still selects an out-of-range date in warning mode', async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + + const screen = render( + + ) + + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + fireEvent.change(dateInput, { target: { value: '2020-01-15' } }) + fireEvent.blur(dateInput) + + expect( + screen.getByText( + 'Date 2020-01-15 is less than the minimum allowed date 2024-01-01.' + ) + ).toBeInTheDocument() + + fireEvent.focus(dateInput) + const calendar = await screen.findByTestId('calendar') + + // warning mode means "flag it, but still let them pick it" - the + // widget showing the out-of-range date as selected is intentional + expect( + within(calendar).getByTestId('2020-01-15').querySelector('button') + ).toHaveClass('isSelected') + + jest.useRealTimers() + }) + it('should validate maximum date', () => { const { getByTestId, getByText } = render( { ) ).toBeInTheDocument() }) - // Temporarily disabled - it.skip('should validate date in ethiopic calendar', () => { + it('should validate date in ethiopic calendar', () => { const onDateSelectMock = jest.fn() const { getByTestId, getByText, queryByText } = render( { getByText('Invalid date in specified calendar') ).toBeInTheDocument() }) - // ToDo: these scenarios seem to work but they timeout on CI sporadically - ticket: https://dhis2.atlassian.net/browse/LIBS-763 it('should validate date in nepali calendar', () => { const onDateSelectMock = jest.fn() const { getByTestId, getByText, queryByText } = render( diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index fd722cf43..90f38bb4e 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -92,7 +92,10 @@ export const CalendarInput = ({ setOpen(false) }, date, - ...validationOptions, + minDate, + maxDate, + format, + strictValidation, options: useDatePickerOptions, }) diff --git a/components/calendar/types/index.d.ts b/components/calendar/types/index.d.ts index 9fc7c65d7..8f505cbac 100644 --- a/components/calendar/types/index.d.ts +++ b/components/calendar/types/index.d.ts @@ -1,5 +1,5 @@ import * as React from 'react' -import { useDatePicker } from '@dhis2/multi-calendar-dates' +import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates' import { InputFieldProps } from '@dhis2-ui/input' export type CalendarDir = 'ltr' | 'rtl' @@ -7,6 +7,25 @@ export type CalendarDir = 'ltr' | 'rtl' type CalendarPickerParam = Parameters[0] type CalendarPickerOptions = CalendarPickerParam['options'] +/** + * The `validation` object CalendarInput attaches to its `onDateSelect` + * payload. Not produced by the bare `Calendar` widget - CalendarInput + * computes it itself via `validateDateString` (whose `error`/`warning`/ + * `valid` combination depends on the `strictValidation` prop), except + * when the date is cleared, in which case it's just `{ valid: true }`. + */ +export type CalendarInputValidation = ReturnType + +export type CalendarInputOnDateSelectPayload = + | { + calendarDateString: string + validation: CalendarInputValidation + } + | { + calendarDateString: null + validation: { valid: true } + } + export interface CalendarProps { /** * the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts @@ -62,7 +81,14 @@ export interface CalendarProps { export const Calendar: React.FC export type CalendarInputProps = Omit & - CalendarProps & { + Omit & { + /** + * Called with `{ calendarDateString: null, validation: { valid: true } }` + * when the date is cleared, otherwise with `{ calendarDateString: string, validation }` + * where `validation` reflects the result of validating against `minDate`/`maxDate`/ + * `format` (see `CalendarInputValidation`), influenced by `strictValidation`. + */ + onDateSelect: (payload: CalendarInputOnDateSelectPayload) => void /** * Optional format for the date. Determines how the date is displayed * or processed. If not provided it supports both formats diff --git a/yarn.lock b/yarn.lock index 2ba6210c5..79d487c40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -188,7 +188,16 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.29.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/code-frame@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== @@ -254,7 +263,18 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.29.7", "@babel/generator@^7.7.2": +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.7.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" + integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== + dependencies: + "@babel/parser" "^7.26.2" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/generator@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== @@ -394,12 +414,22 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== -"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.24.7", "@babel/helper-validator-identifier@^7.29.7": +"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.24.7": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-identifier@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== -"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.29.7": +"@babel/helper-validator-option@^7.24.7": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-validator-option@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== @@ -413,7 +443,15 @@ "@babel/traverse" "^7.29.7" "@babel/types" "^7.29.7" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.29.7": +"@babel/helpers@^7.12.5": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" + +"@babel/helpers@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== @@ -431,7 +469,14 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.17.0", "@babel/parser@^7.18.8", "@babel/parser@^7.20.7", "@babel/parser@^7.29.7", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": +"@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.17.0", "@babel/parser@^7.18.8", "@babel/parser@^7.20.7", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" + integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== + dependencies: + "@babel/types" "^7.26.0" + +"@babel/parser@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== @@ -1347,7 +1392,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.12.7", "@babel/template@^7.29.7", "@babel/template@^7.3.3": +"@babel/template@^7.12.7", "@babel/template@^7.25.9", "@babel/template@^7.29.7", "@babel/template@^7.3.3": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== @@ -1356,7 +1401,20 @@ "@babel/parser" "^7.29.7" "@babel/types" "^7.29.7" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9", "@babel/traverse@^7.29.7", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.1.6", "@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== @@ -1377,7 +1435,7 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.25.2", "@babel/types@^7.29.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.25.2", "@babel/types@^7.26.0", "@babel/types@^7.29.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== @@ -2132,13 +2190,13 @@ resolved "https://registry.yarnpkg.com/@dhis2/data-engine/-/data-engine-3.17.3.tgz#0347416e9919efbf4d9739c4141fa543f89669ad" integrity sha512-hLXt7LFrFitR7QgKfGQ3ComTLrY5IAdtERonhdo/SIrsRYWoeVaMiCOkUUzC48pEaeo1/BL5qwA7Tw7jZgROQw== -"@dhis2/multi-calendar-dates@2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-2.1.2.tgz#c3dd01e358c3dda4354c6722bb72e077b3ddd9b8" - integrity sha512-7/EuYZFi266QwyRpK+s79iEiwdOUVBwM+QjKwNUHN3zdYMDmQcWLTo5TujJFg1XnnQ8UhdiRqESq5rgoJkM2fQ== +"@dhis2/multi-calendar-dates@3.0.0-alpha.1": + version "3.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-3.0.0-alpha.1.tgz#2e43e2d00e59ce8b3b7a1d93e5ad8077bea6267b" + integrity sha512-Q2L9UnB04jZLRKH8uBjUG8gDi36orixvgaGZJzAIbFXhZ2FVW98yT1lJ05IKbQDZR0rYT+MY6bSo1vE7ARg+yg== dependencies: "@dhis2/d2-i18n" "^1.1.3" - "@js-temporal/polyfill" "0.4.3" + "@js-temporal/polyfill" "0.5.1" classnames "^2.3.2" "@dhis2/prop-types@^3.1.2": @@ -3338,13 +3396,12 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@js-temporal/polyfill@0.4.3": - version "0.4.3" - resolved "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.4.3.tgz" - integrity sha512-6Fmjo/HlkyVCmJzAPnvtEWlcbQUSRhi8qlN9EtJA/wP7FqXsevLLrlojR44kzNzrRkpf7eDJ+z7b4xQD/Ycypw== +"@js-temporal/polyfill@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@js-temporal/polyfill/-/polyfill-0.5.1.tgz#c9726fdb7fbdbc6419292ba94294b10a08852c32" + integrity sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ== dependencies: - jsbi "^4.1.0" - tslib "^2.3.1" + jsbi "^4.3.0" "@juggle/resize-observer@^3.3.1": version "3.4.0" @@ -12613,10 +12670,10 @@ js2xmlparser@^4.0.2: dependencies: xmlcreate "^2.0.4" -jsbi@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz" - integrity sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g== +jsbi@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-4.3.2.tgz#8a4d05d4e09907d73042135b6aa55a6d161ee955" + integrity sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew== jsbn@~0.1.0: version "0.1.1" @@ -18467,7 +18524,7 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.6.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==