From a9a65e9efc2c1592345de19306260406403817a6 Mon Sep 17 00:00:00 2001 From: Diana Nanyanzi Date: Thu, 6 Jun 2024 18:01:19 +0300 Subject: [PATCH 1/2] feat: add support for multi-calendar dates - use UI library's CalendarInput component for date type form inputs - specify calendar type from system settings - add tests --- .../data-entry-cell/entry-field-input.js | 4 + src/data-workspace/inputs/date-input.js | 79 +++++++++++++++++++ src/data-workspace/inputs/date-input.test.js | 77 ++++++++++++++++++ src/data-workspace/inputs/generic-input.js | 1 - src/data-workspace/inputs/index.js | 1 + src/data-workspace/inputs/inputs.module.css | 6 ++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/data-workspace/inputs/date-input.js create mode 100644 src/data-workspace/inputs/date-input.test.js diff --git a/src/data-workspace/data-entry-cell/entry-field-input.js b/src/data-workspace/data-entry-cell/entry-field-input.js index 3ab03bce8..c26230e88 100644 --- a/src/data-workspace/data-entry-cell/entry-field-input.js +++ b/src/data-workspace/data-entry-cell/entry-field-input.js @@ -16,6 +16,7 @@ import { LongText, OptionSet, TrueOnlyCheckbox, + DateInput, } from '../inputs/index.js' function InputComponent({ sharedProps, de }) { @@ -46,6 +47,9 @@ function InputComponent({ sharedProps, de }) { case VALUE_TYPES.TRUE_ONLY: { return } + case VALUE_TYPES.DATE: { + return + } default: { return } diff --git a/src/data-workspace/inputs/date-input.js b/src/data-workspace/inputs/date-input.js new file mode 100644 index 000000000..c2e98e00f --- /dev/null +++ b/src/data-workspace/inputs/date-input.js @@ -0,0 +1,79 @@ +import { useConfig } from '@dhis2/app-runtime' +import { CalendarInput } from '@dhis2/ui' +import React from 'react' +import { useField } from 'react-final-form' +import { useSetDataValueMutation, useUserInfo } from '../../shared/index.js' +import styles from './inputs.module.css' +import { InputPropTypes } from './utils.js' + +export const DateInput = ({ + cocId, + deId, + disabled, + fieldname, + form, + locked, + onFocus, + onKeyDown, +}) => { + const { data: userInfo } = useUserInfo() + const keyUiLocale = userInfo?.settings?.keyUiLocale + + const { systemInfo } = useConfig() + const { calendar } = systemInfo + + const { input, meta } = useField(fieldname, { + subscription: { + value: true, + dirty: true, + valid: true, + data: true, + }, + }) + + const { mutate } = useSetDataValueMutation({ deId, cocId }) + + const syncData = (value) => { + mutate( + // Empty values need an empty string + { value: value || '' }, + { + onSuccess: () => { + form.mutators.setFieldData(fieldname, { + lastSyncedValue: value, + }) + }, + } + ) + } + + const handleChange = (value) => { + // If this value has changed, sync it to server if valid + if (meta.valid && value !== meta.data.lastSyncedValue) { + syncData(value) + } + } + + return ( +
+ { + input.onChange(date?.calendarDateString) + handleChange(date?.calendarDateString) + }} + locale={keyUiLocale} + clearable + /> +
+ ) +} + +DateInput.propTypes = InputPropTypes diff --git a/src/data-workspace/inputs/date-input.test.js b/src/data-workspace/inputs/date-input.test.js new file mode 100644 index 000000000..7da82d790 --- /dev/null +++ b/src/data-workspace/inputs/date-input.test.js @@ -0,0 +1,77 @@ +import { useConfig } from '@dhis2/app-runtime' +import userEvent from '@testing-library/user-event' +import React from 'react' +import { useUserInfo } from '../../shared/index.js' +import { render } from '../../test-utils/index.js' +import { FinalFormWrapper } from '../final-form-wrapper.js' +import { DateInput } from './date-input.js' + +jest.mock('../../shared/use-user-info/use-user-info.js', () => ({ + useUserInfo: jest.fn(), +})) + +jest.mock('@dhis2/app-runtime', () => { + const originalModule = jest.requireActual('@dhis2/app-runtime') + + return { + ...originalModule, + useConfig: jest.fn(), + } +}) + +describe('date input field', () => { + const props = { + cocId: 'HllvX50cXC0', + deId: 'rkAZZFGFEQ7', + disabled: undefined, + fieldname: 'rkAZZFGFEQ7.HllvX50cXC0', + form: {}, + locked: false, + onFocus: jest.fn(), + onKeyDown: jest.fn(), + } + + beforeEach(() => { + useUserInfo.mockImplementation(() => ({ + data: { + settings: { + keyUiLocale: 'en', + }, + }, + })) + }) + + afterEach(jest.clearAllMocks) + + it('renders date input component', async () => { + useConfig.mockImplementation(() => ({ + systemInfo: { calendar: 'gregory' }, + })) + const { getByText, getByRole, getByTestId, queryByTestId } = render( + , + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ) + const calendarInputLabel = getByText('Pick a date') + const clearCalendarInputButton = getByText('Clear') + const calendarInput = getByRole('textbox') + let calendar = queryByTestId('calendar') + + expect(calendarInputLabel).toBeInTheDocument() + expect(clearCalendarInputButton).toBeInTheDocument() + expect(calendarInput.value).toBe('') + expect(calendar).not.toBeInTheDocument() + + await userEvent.click(calendarInput) + calendar = getByTestId('calendar') + + expect(calendar).toBeInTheDocument() + }) + + // todo: test calendar input for different calendars +}) diff --git a/src/data-workspace/inputs/generic-input.js b/src/data-workspace/inputs/generic-input.js index 615e46cf1..bd4f1b4a6 100644 --- a/src/data-workspace/inputs/generic-input.js +++ b/src/data-workspace/inputs/generic-input.js @@ -17,7 +17,6 @@ import { } from './validators.js' const htmlTypeAttrsByValueType = { - [VALUE_TYPES.DATE]: 'date', [VALUE_TYPES.DATETIME]: 'datetime-local', [VALUE_TYPES.EMAIL]: 'email', [VALUE_TYPES.PHONE_NUMBER]: 'tel', diff --git a/src/data-workspace/inputs/index.js b/src/data-workspace/inputs/index.js index 92abb4bc3..4eef13533 100644 --- a/src/data-workspace/inputs/index.js +++ b/src/data-workspace/inputs/index.js @@ -4,6 +4,7 @@ export * from './file-inputs.js' export * from './long-text.js' export * from './option-set.js' export * from './true-only-checkbox.js' +export * from './date-input.js' export { createLessThan, createMoreThan, diff --git a/src/data-workspace/inputs/inputs.module.css b/src/data-workspace/inputs/inputs.module.css index 6122ed073..bafc67e69 100644 --- a/src/data-workspace/inputs/inputs.module.css +++ b/src/data-workspace/inputs/inputs.module.css @@ -100,3 +100,9 @@ margin-left: 0; } } + +.dateInput { + background: none; + border: none; +} + From ad759a33a1dfa2028f1563f462be94f4821b817e Mon Sep 17 00:00:00 2001 From: Diana Nanyanzi Date: Thu, 25 Jul 2024 07:46:06 +0300 Subject: [PATCH 2/2] chore: modify and add test cases for different supported calendars --- src/data-workspace/inputs/date-input.js | 4 +- src/data-workspace/inputs/date-input.test.js | 184 +++++++++++++++++-- 2 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/data-workspace/inputs/date-input.js b/src/data-workspace/inputs/date-input.js index c2e98e00f..c0ffdd340 100644 --- a/src/data-workspace/inputs/date-input.js +++ b/src/data-workspace/inputs/date-input.js @@ -66,8 +66,8 @@ export const DateInput = ({ date={input.value} calendar={calendar} onDateSelect={(date) => { - input.onChange(date?.calendarDateString) - handleChange(date?.calendarDateString) + input.onChange(date ? date?.calendarDateString : '') + handleChange(date ? date?.calendarDateString : '') }} locale={keyUiLocale} clearable diff --git a/src/data-workspace/inputs/date-input.test.js b/src/data-workspace/inputs/date-input.test.js index 7da82d790..dc0589dcb 100644 --- a/src/data-workspace/inputs/date-input.test.js +++ b/src/data-workspace/inputs/date-input.test.js @@ -1,7 +1,7 @@ import { useConfig } from '@dhis2/app-runtime' import userEvent from '@testing-library/user-event' import React from 'react' -import { useUserInfo } from '../../shared/index.js' +import { useSetDataValueMutation, useUserInfo } from '../../shared/index.js' import { render } from '../../test-utils/index.js' import { FinalFormWrapper } from '../final-form-wrapper.js' import { DateInput } from './date-input.js' @@ -10,9 +10,12 @@ jest.mock('../../shared/use-user-info/use-user-info.js', () => ({ useUserInfo: jest.fn(), })) +jest.mock('../../shared/data-value-mutations/data-value-mutations.js', () => ({ + useSetDataValueMutation: jest.fn(), +})) + jest.mock('@dhis2/app-runtime', () => { const originalModule = jest.requireActual('@dhis2/app-runtime') - return { ...originalModule, useConfig: jest.fn(), @@ -31,47 +34,188 @@ describe('date input field', () => { onKeyDown: jest.fn(), } + const mutate = jest.fn() + + const mockDate = new Date('25 July 2024 12:00:00 GMT+0300') + beforeEach(() => { - useUserInfo.mockImplementation(() => ({ + useUserInfo.mockReturnValue({ data: { settings: { keyUiLocale: 'en', }, }, - })) + }) + + useSetDataValueMutation.mockReturnValue({ + mutate, + }) + + // 25th July 2024 (2024-07-25) - gregorian + // 10, Shrawan 2081 (2081-04-10) - nepali + // 18 Hamle 2016 (2016-11-18) - ethiopian + jest.spyOn(Date, 'now').mockImplementation(() => mockDate.getTime()) }) afterEach(jest.clearAllMocks) - it('renders date input component', async () => { - useConfig.mockImplementation(() => ({ - systemInfo: { calendar: 'gregory' }, - })) + it('renders date input component (gregorian calendar)', async () => { + useConfig.mockReturnValue({ + systemInfo: { calendar: 'gregorian' }, + }) + const { getByText, getByRole, getByTestId, queryByTestId } = render( - , - { - wrapper: ({ children }) => ( - - {children} - - ), - } + jest.fn()} + initialValues={{}} + keepDirtyOnReinitialize + > + + ) const calendarInputLabel = getByText('Pick a date') - const clearCalendarInputButton = getByText('Clear') const calendarInput = getByRole('textbox') let calendar = queryByTestId('calendar') expect(calendarInputLabel).toBeInTheDocument() - expect(clearCalendarInputButton).toBeInTheDocument() expect(calendarInput.value).toBe('') expect(calendar).not.toBeInTheDocument() + // open calendar await userEvent.click(calendarInput) calendar = getByTestId('calendar') - expect(calendar).toBeInTheDocument() + + // select today's date: 25th July 2024 + const today = getByText('25') + expect(today.classList.contains('isToday')).toBe(true) + await userEvent.click(today) + + // check that mutate function was called + expect(mutate.mock.calls).toHaveLength(1) + expect(mutate.mock.calls[0][0]).toHaveProperty('value', '2024-07-25') + + expect(calendarInput.value).toBe('2024-07-25') + + // clear date input + const clearCalendarInputButton = getByText('Clear') + await userEvent.click(clearCalendarInputButton) + + // check that mutate function was called again on clicking Clear button + expect(mutate.mock.calls).toHaveLength(2) + expect(mutate.mock.calls[1][0]).toHaveProperty('value', '') + + expect(calendarInput.value).toBe('') }) - // todo: test calendar input for different calendars + it('allows user to navigate calendar component', async () => { + useConfig.mockReturnValue({ + systemInfo: { calendar: 'gregorian' }, + }) + + const { getByText, getByRole, getByTestId, getByLabelText } = render( + jest.fn()} + initialValues={{}} + keepDirtyOnReinitialize + > + + + ) + + const calendarInput = getByRole('textbox') + await userEvent.click(calendarInput) // open calendar + + // previous month and previous year selection + // select 20th June, 2023 + const prevMonthBtn = getByTestId('calendar-previous-month') + await userEvent.click(prevMonthBtn) // June + + const prevYearBtn = getByLabelText('Go to previous year') + await userEvent.click(prevYearBtn) // 2023 + + let dateToSelect = getByText('20') + await userEvent.click(dateToSelect) + + expect(calendarInput.value).toBe('2023-06-20') + + // check that mutate function was called + expect(mutate.mock.calls).toHaveLength(1) + expect(mutate.mock.calls[0][0]).toHaveProperty('value', '2023-06-20') + + // next month and next year selection + // select 18th July, 2024 + await userEvent.click(calendarInput) + + const nextMonthBtn = getByTestId('calendar-next-month') + await userEvent.click(nextMonthBtn) // July + const nextYearBtn = getByLabelText('Go to next year') + await userEvent.click(nextYearBtn) // 2024 + + dateToSelect = getByText('18') + await userEvent.click(dateToSelect) + expect(calendarInput.value).toBe('2024-07-18') + + // check that mutate function was called again + expect(mutate.mock.calls).toHaveLength(2) + expect(mutate.mock.calls[1][0]).toHaveProperty('value', '2024-07-18') + }) + + it('renders system set calendar, i.e. nepali', async () => { + useConfig.mockReturnValue({ + systemInfo: { calendar: 'nepali' }, + }) + + const { getByText, getByRole } = render( + jest.fn()} + initialValues={{}} + keepDirtyOnReinitialize + > + + + ) + + const calendarInput = getByRole('textbox') + expect(calendarInput.value).toBe('') + await userEvent.click(calendarInput) + + const today = getByText('10') + expect(today.classList.contains('isToday')).toBe(true) + await userEvent.click(today) + + expect(calendarInput.value).toBe('2081-04-10') + // check that mutate function was called + expect(mutate.mock.calls).toHaveLength(1) + expect(mutate.mock.calls[0][0]).toHaveProperty('value', '2081-04-10') + }) + + it('renders system set calendar, i.e. ethiopian', async () => { + useConfig.mockReturnValue({ + systemInfo: { calendar: 'ethiopian' }, + }) + + const { getByText, getByRole } = render( + jest.fn()} + initialValues={{}} + keepDirtyOnReinitialize + > + + + ) + + const calendarInput = getByRole('textbox') + expect(calendarInput.value).toBe('') + await userEvent.click(calendarInput) + + const today = getByText('18') + expect(today.classList.contains('isToday')).toBe(true) + await userEvent.click(today) + + expect(calendarInput.value).toBe('2016-11-18') + // check that mutate function was called + expect(mutate.mock.calls).toHaveLength(1) + expect(mutate.mock.calls[0][0]).toHaveProperty('value', '2016-11-18') + }) })