Skip to content

Commit

Permalink
Merge pull request #385 from dhis2/DHIS2-17446/support-multi-calendar…
Browse files Browse the repository at this point in the history
…-dates

feat: add support for multi calendar dates
  • Loading branch information
d-rita authored Jul 29, 2024
2 parents 6cf24bc + ad759a3 commit 55dbcd8
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/data-workspace/data-entry-cell/entry-field-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
LongText,
OptionSet,
TrueOnlyCheckbox,
DateInput,
} from '../inputs/index.js'

function InputComponent({ sharedProps, de }) {
Expand Down Expand Up @@ -46,6 +47,9 @@ function InputComponent({ sharedProps, de }) {
case VALUE_TYPES.TRUE_ONLY: {
return <TrueOnlyCheckbox {...sharedProps} />
}
case VALUE_TYPES.DATE: {
return <DateInput {...sharedProps} />
}
default: {
return <GenericInput {...sharedProps} valueType={de.valueType} />
}
Expand Down
79 changes: 79 additions & 0 deletions src/data-workspace/inputs/date-input.js
Original file line number Diff line number Diff line change
@@ -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 (
<div onClick={onFocus}>
<CalendarInput
{...input}
className={styles.dateInput}
autoComplete="off"
onKeyDown={onKeyDown}
disabled={disabled}
readOnly={locked}
date={input.value}
calendar={calendar}
onDateSelect={(date) => {
input.onChange(date ? date?.calendarDateString : '')
handleChange(date ? date?.calendarDateString : '')
}}
locale={keyUiLocale}
clearable
/>
</div>
)
}

DateInput.propTypes = InputPropTypes
221 changes: 221 additions & 0 deletions src/data-workspace/inputs/date-input.test.js
Original file line number Diff line number Diff line change
@@ -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(
<FinalFormWrapper
onSubmit={() => jest.fn()}
initialValues={{}}
keepDirtyOnReinitialize
>
<DateInput {...props} />
</FinalFormWrapper>
)
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(
<FinalFormWrapper
onSubmit={() => jest.fn()}
initialValues={{}}
keepDirtyOnReinitialize
>
<DateInput {...props} />
</FinalFormWrapper>
)

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(
<FinalFormWrapper
onSubmit={() => jest.fn()}
initialValues={{}}
keepDirtyOnReinitialize
>
<DateInput {...props} />
</FinalFormWrapper>
)

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(
<FinalFormWrapper
onSubmit={() => jest.fn()}
initialValues={{}}
keepDirtyOnReinitialize
>
<DateInput {...props} />
</FinalFormWrapper>
)

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')
})
})
1 change: 0 additions & 1 deletion src/data-workspace/inputs/generic-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/data-workspace/inputs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/data-workspace/inputs/inputs.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,9 @@
margin-left: 0;
}
}

.dateInput {
background: none;
border: none;
}

1 comment on commit 55dbcd8

@dhis2-bot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.