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;
+}
+