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..c0ffdd340 --- /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 ? date?.calendarDateString : '') + handleChange(date ? 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..dc0589dcb --- /dev/null +++ b/src/data-workspace/inputs/date-input.test.js @@ -0,0 +1,221 @@ +import { useConfig } from '@dhis2/app-runtime' +import userEvent from '@testing-library/user-event' +import React from 'react' +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' + +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(), + } +}) + +describe('date input field', () => { + const props = { + cocId: 'HllvX50cXC0', + deId: 'rkAZZFGFEQ7', + disabled: undefined, + fieldname: 'rkAZZFGFEQ7.HllvX50cXC0', + form: {}, + locked: false, + onFocus: jest.fn(), + onKeyDown: jest.fn(), + } + + const mutate = jest.fn() + + const mockDate = new Date('25 July 2024 12:00:00 GMT+0300') + + beforeEach(() => { + 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 (gregorian calendar)', async () => { + useConfig.mockReturnValue({ + systemInfo: { calendar: 'gregorian' }, + }) + + const { getByText, getByRole, getByTestId, queryByTestId } = render( + jest.fn()} + initialValues={{}} + keepDirtyOnReinitialize + > + + + ) + const calendarInputLabel = getByText('Pick a date') + const calendarInput = getByRole('textbox') + let calendar = queryByTestId('calendar') + + expect(calendarInputLabel).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('') + }) + + 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') + }) +}) 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; +} +