Skip to content

Commit

Permalink
feat: Reason for delayed registration in v2 birth (#8533)
Browse files Browse the repository at this point in the history
* feat: add `isBefore`

* fix: rename `isBefore` to `isAfter`

* feat: add not.beforeNow()

* fix: unexpected validation errors when days or months are single digit

* chore: upgrade toolkit

* fix: apply suggested changes

* fix: refine date range

* test: add unit test for `formatDateFieldValue` function
  • Loading branch information
jamil314 authored Feb 7, 2025
1 parent f6786bf commit 0c73bae
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
getConditionalActionsForField,
getDependentFields,
handleInitialValue,
hasInitialValueDependencyInfo
hasInitialValueDependencyInfo,
makeDatesFormatted
} from './utils'
import { Errors, getValidationErrorsForForm } from './validation'

Expand Down Expand Up @@ -486,6 +487,7 @@ class FormSectionComponent extends React.Component<AllProps> {
...field,
id: field.id.replaceAll('.', FIELD_SEPARATOR)
}))
const valuesWithFormattedDate = makeDatesFormatted(fieldsWithDotIds, values)

return (
<section>
Expand All @@ -501,7 +503,9 @@ class FormSectionComponent extends React.Component<AllProps> {
const conditionalActions: string[] = getConditionalActionsForField(
field,
{
$form: makeFormikFieldIdsOpenCRVSCompatible(values),
$form: makeFormikFieldIdsOpenCRVSCompatible(
valuesWithFormattedDate
),
$now: formatISO(new Date(), { representation: 'date' })
}
)
Expand Down Expand Up @@ -554,7 +558,7 @@ class FormSectionComponent extends React.Component<AllProps> {
* Because our form field ids can have dots in them, we temporarily transform those dots
* to a different character before passing the data to Formik. This function unflattens
*/
const FIELD_SEPARATOR = '____'
export const FIELD_SEPARATOR = '____'
function makeFormFieldIdsFormikCompatible<T>(data: Record<string, T>) {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
Expand Down Expand Up @@ -593,7 +597,9 @@ export const FormFieldGenerator: React.FC<ExposedProps> = (props) => {
validate={(values) =>
getValidationErrorsForForm(
props.fields,
makeFormikFieldIdsOpenCRVSCompatible(values),
makeFormikFieldIdsOpenCRVSCompatible(
makeDatesFormatted(props.fields, values)
),
props.requiredErrorMessage
)
}
Expand Down
43 changes: 43 additions & 0 deletions packages/client/src/v2-events/components/forms/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

import { formatDateFieldValue } from './utils'

describe('Verify formatDateFieldValue', () => {
it('Formats date properly', () => {
const cases = [
{
input: '--',
expectedOutput: '00-00-00'
},
{
input: '01-02-',
expectedOutput: '01-02-00'
},
{
input: '1-2-3',
expectedOutput: '01-02-03'
},
{
input: '01-2-32',
expectedOutput: '01-02-32'
},
{
input: '2025-2-3',
expectedOutput: '2025-02-03'
}
]

cases.forEach(({ input, expectedOutput }) =>
expect(formatDateFieldValue(input)).toBe(expectedOutput)
)
})
})
33 changes: 33 additions & 0 deletions packages/client/src/v2-events/components/forms/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
validate
} from '@opencrvs/commons/client'
import { DependencyInfo } from '@client/forms'
import { FIELD_SEPARATOR } from './FormFieldGenerator'

export function handleInitialValue(
field: FieldConfig,
Expand Down Expand Up @@ -106,3 +107,35 @@ export function setEmptyValuesForFields(fields: FieldConfig[]) {
export interface Stringifiable {
toString(): string
}

/**
*
* @param fields field config in OpenCRVS format (separated with `.`)
* @param values form values in formik format (separated with `FIELD_SEPARATOR`)
* @returns adds 0 before single digit days and months to make them 2 digit
* because ajv's `formatMaximum` and `formatMinimum` does not allow single digit day or months
*/
export function makeDatesFormatted(
fields: FieldConfig[],
values: Record<string, FieldValue>
) {
return fields.reduce((acc, field) => {
const fieldId = field.id.replaceAll('.', FIELD_SEPARATOR)

if (field.type === 'DATE' && fieldId in values) {
const value = values[fieldId as keyof typeof values]
if (typeof value === 'string') {
const formattedDate = formatDateFieldValue(value)
return { ...acc, [fieldId]: formattedDate }
}
}
return acc
}, values)
}

export function formatDateFieldValue(value: string) {
return value
.split('-')
.map((d: string) => d.padStart(2, '0'))
.join('-')
}
2 changes: 1 addition & 1 deletion packages/toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencrvs/toolkit",
"version": "0.0.37-ml",
"version": "0.0.38-jr",
"description": "OpenCRVS toolkit for building country configurations",
"license": "MPL-2.0",
"exports": {
Expand Down
133 changes: 115 additions & 18 deletions packages/toolkit/src/conditionals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,30 @@ export function eventHasAction(type: ActionType): AjvJSONSchema {
}
}

type DateBoundary = {
now: () => FieldAPI
date: (
// date should be in yyyy-mm-dd format
date: `${number}${number}${number}${number}-${number}${number}-${number}${number}`
) => FieldAPI
days: (days: number) => {
inPast: () => FieldAPI
inFuture: () => FieldAPI
}
}

export type FieldAPI = {
inArray: (values: string[]) => FieldAPI
isBeforeNow: () => FieldAPI
/**
* Checks if the date is within `days` days in the past from now.
*/
isBefore: () => DateBoundary
isAfter: () => DateBoundary
isEqualTo: (value: string | boolean) => FieldAPI
isUndefined: () => FieldAPI
not: {
isBefore: () => DateBoundary
isAfter: () => DateBoundary
inArray: (values: string[]) => FieldAPI
equalTo: (value: string | boolean) => FieldAPI
}
Expand Down Expand Up @@ -152,29 +170,76 @@ export function field(fieldId: string) {
return api
}

const api: FieldAPI = {
isBeforeNow: () =>
addCondition({
const getDateFromNow = (days: number) =>
new Date(Date.now() - days * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0]

const getDateRange = (
date: string,
clause: 'formatMinimum' | 'formatMaximum'
) => ({
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
[fieldId]: {
type: 'string',
format: 'date',
formatMaximum: { $data: '2/$now' }
}
},
required: [fieldId]
},
$now: {
[fieldId]: {
type: 'string',
format: 'date',
[clause]: date
}
},
required: [fieldId]
}
},
required: ['$form']
})

const getNegativeDateRange = (
date: string,
clause: 'formatMinimum' | 'formatMaximum'
) => ({
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
[fieldId]: {
type: 'string',
format: 'date'
not: {
format: 'date',
[clause]: date
}
}
},
required: ['$form', '$now']
required: [fieldId]
}
},
required: ['$form']
})

const api: FieldAPI = {
isAfter: () => ({
days: (days: number) => ({
inPast: () =>
addCondition(getDateRange(getDateFromNow(days), 'formatMinimum')),
inFuture: () =>
addCondition(getDateRange(getDateFromNow(-days), 'formatMinimum'))
}),
date: (date: string) => addCondition(getDateRange(date, 'formatMinimum')),
now: () => addCondition(getDateRange(getDateFromNow(0), 'formatMinimum'))
}),
isBefore: () => ({
days: (days: number) => ({
inPast: () =>
addCondition(getDateRange(getDateFromNow(days), 'formatMaximum')),
inFuture: () =>
addCondition(getDateRange(getDateFromNow(-days), 'formatMaximum'))
}),
date: (date: string) => addCondition(getDateRange(date, 'formatMaximum')),
now: () => addCondition(getDateRange(getDateFromNow(0), 'formatMaximum'))
}),
isEqualTo: (value: string | boolean) =>
addCondition({
type: 'object',
Expand Down Expand Up @@ -228,6 +293,38 @@ export function field(fieldId: string) {
required: ['$form']
}),
not: {
isAfter: () => ({
days: (days: number) => ({
inPast: () =>
addCondition(
getNegativeDateRange(getDateFromNow(days), 'formatMinimum')
),
inFuture: () =>
addCondition(
getNegativeDateRange(getDateFromNow(-days), 'formatMinimum')
)
}),
date: (date: string) =>
addCondition(getNegativeDateRange(date, 'formatMinimum')),
now: () =>
addCondition(getNegativeDateRange(getDateFromNow(0), 'formatMinimum'))
}),
isBefore: () => ({
days: (days: number) => ({
inPast: () =>
addCondition(
getNegativeDateRange(getDateFromNow(days), 'formatMaximum')
),
inFuture: () =>
addCondition(
getNegativeDateRange(getDateFromNow(-days), 'formatMaximum')
)
}),
date: (date: string) =>
addCondition(getNegativeDateRange(date, 'formatMaximum')),
now: () =>
addCondition(getNegativeDateRange(getDateFromNow(0), 'formatMaximum'))
}),
inArray: (values: string[]) =>
addCondition({
type: 'object',
Expand Down

0 comments on commit 0c73bae

Please sign in to comment.