Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reason for delayed registration in v2 birth #8533

Merged
merged 14 commits into from
Feb 7, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
getConditionalActionsForField,
getDependentFields,
handleInitialValue,
hasInitialValueDependencyInfo
hasInitialValueDependencyInfo,
makeDatesFormatted
} from './utils'
import { Errors, getValidationErrorsForForm } from './validation'

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

return (
<section>
Expand Down Expand Up @@ -536,7 +534,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 All @@ -554,36 +552,6 @@ function makeFormikFieldIdsOpenCRVSCompatible<T>(data: Record<string, T>) {
])
)
}
/**
*
* @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
*/
function getValuesWithFormattedDate(
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 = value
.split('-')
.map((d: string) => d.padStart(2, '0'))
.join('-')
acc[fieldId] = formattedDate
}
}
return acc
},
{ ...values }
)
}

export const FormFieldGenerator: React.FC<ExposedProps> = (props) => {
const intl = useIntl()
Expand All @@ -606,7 +574,7 @@ export const FormFieldGenerator: React.FC<ExposedProps> = (props) => {
getValidationErrorsForForm(
props.fields,
makeFormikFieldIdsOpenCRVSCompatible(
getValuesWithFormattedDate(props.fields, values)
makeDatesFormatted(props.fields, values)
),
props.requiredErrorMessage
)
Expand Down
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 @@ -43,6 +43,7 @@ import { selectFieldToString } from '@client/v2-events/features/events/registere
import { selectCountryFieldToString } from '@client/v2-events/features/events/registered-fields/SelectCountry'
import { ILocation } from '@client/v2-events/features/events/registered-fields/Location'
import { checkboxToString } from '@client/v2-events/features/events/registered-fields/Checkbox'
import { FIELD_SEPARATOR } from './FormFieldGenerator'

export function handleInitialValue(
field: FieldConfig,
Expand Down Expand Up @@ -183,3 +184,35 @@ export function fieldValueToString({
throw new Error(`Field type ${fieldConfig.type} configuration missing.`)
}
}

/**
*
* @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)
}

function formatDateFieldValue(value: string) {
return value
.split('-')
.map((d: string) => d.padStart(2, '0'))
.join('-')
}
166 changes: 107 additions & 59 deletions packages/toolkit/src/conditionals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,30 @@ export function eventHasAction(type: ActionDocument['type']) {
}
}

type dateBoundary = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.
*/
isAfter: (days: number) => FieldAPI
isBefore: () => dateBoundary
isAfter: () => dateBoundary
isEqualTo: (value: string) => FieldAPI
isUndefined: () => FieldAPI
not: {
isBeforeNow: () => FieldAPI
isBefore: () => dateBoundary
isAfter: () => dateBoundary
inArray: (values: string[]) => FieldAPI
equalTo: (value: string) => FieldAPI
}
Expand Down Expand Up @@ -140,49 +153,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'
format: 'date',
[clause]: date
}
},
required: ['$form', '$now']
}),
isAfter: (days: number) =>
addCondition({
required: [fieldId]
}
},
required: ['$form']
})

const getNegativeDateRange = (
date: string,
clause: 'formatMinimum' | 'formatMaximum'
) => ({
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
[fieldId]: {
type: 'string',
format: 'date',
formatMinimum: new Date(Date.now() - days * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0]
}
},
required: [fieldId]
[fieldId]: {
type: 'string',
not: {
format: 'date',
[clause]: date
}
}
},
required: ['$form']
required: [fieldId]
}
},
required: ['$form']
})

const api: FieldAPI = {
isAfter: () => ({
days: (days: number) => ({
inPast: () =>
addCondition(getDateRange(getDateFromNow(days), 'formatMinimum')),
inFuture: () =>
addCondition(getDateRange(getDateFromNow(-days), 'formatMinimum'))
}),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Really like the abstraction effort! Is the isAfter nested with inPast and inFuture bit odd? could they be flattened or what do yout hink?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

isAfter and isBefore are the boundaries, whereas inPast and inFuture indicates if some boundary is n days in the past (n days before now) or n days in the future (n days after now).
We could ditch inPast and inFuture to make it refer to the past date by default, and future when days is -n .

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) =>
addCondition({
type: 'object',
Expand Down Expand Up @@ -230,30 +270,38 @@ export function field(fieldId: string) {
required: ['$form']
}),
not: {
isBeforeNow: () =>
addCondition({
type: 'object',
properties: {
$form: {
type: 'object',
properties: {
[fieldId]: {
type: 'string',
not: {
format: 'date',
formatMaximum: { $data: '2/$now' }
}
}
},
required: [fieldId]
},
$now: {
type: 'string',
format: 'date'
}
},
required: ['$form', '$now']
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
Loading