Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ HTTP input now accepts `field('..')` references in the HTTP body definition.
#### Jurisdiction

- Elasticsearch now stores location IDs as a full administrative hierarchy, with the leaf representing the actual event location. This enables searching events by any jurisdiction level (district, province, office, health facility etc.).
- Added configurable placeOfEvent in EventConfig, allowing multiple location fields to be defined, with only one becoming the active place of event per document (based on conditionals), enabling jurisdiction-specific search by event location (e.g., birth location, child’s home address, death location).

### Improvements

- Refactor the tRPC context to allow defining public procedures that don't require authentication.
- Remove legacy mongo migration status outputs and skip typecheck which reduced the migration service startup time by 66%.
- The postgres migration files now get restored to their original state (i.e. without the environment variables being replaced) regardless of the migration passing or not
- Added experimental ALPHA_HIDDEN form field type, allowing configurable default/derived values and conditional inclusion in form submissions.

## 1.9.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,27 @@ export function FormSectionComponent({
)

const firstNonFalsyValue = compact(
referencesToOtherFields.map((reference) =>
resolveFieldReferenceValue(reference, fieldValues)
)
referencesToOtherFields.map((reference) => {
const referenceFieldConfig = allFieldsWithDotSeparator.find(
(field: FieldConfig) => field.id === reference.$$field
)

if (!referenceFieldConfig) {
return undefined
}

const isReferenceVisible = isFieldVisible(
referenceFieldConfig,
makeFormikFieldIdsOpenCRVSCompatible(fieldValues),
validatorContext
)

if (!isReferenceVisible || !listenerFieldConfig) {
return undefined
}

return resolveFieldReferenceValue(reference, fieldValues)
})
)[0]

if (firstNonFalsyValue) {
Expand All @@ -261,7 +279,7 @@ export function FormSectionComponent({

return
},
[allFieldsWithDotSeparator, systemVariables]
[allFieldsWithDotSeparator, systemVariables, validatorContext]
)

const onFieldValueChange = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
isIdReaderFieldType,
isQrReaderFieldType,
isLoaderFieldType,
isHiddenFieldType,
isAgeFieldType
} from '@opencrvs/commons/client'
import { TextArea } from '@opencrvs/components/lib/TextArea'
Expand Down Expand Up @@ -100,6 +101,7 @@ import { IdReader } from '@client/v2-events/features/events/registered-fields/Id
import { QrReader } from '@client/v2-events/features/events/registered-fields/QrReader'
import { QueryParamReader } from '@client/v2-events/features/events/registered-fields/QueryParamReader'
import { Loader } from '@client/v2-events/features/events/registered-fields/Loader'
import { Hidden } from '@client/v2-events/features/events/registered-fields/Hidden'
import {
makeFormFieldIdFormikCompatible,
makeFormikFieldIdOpenCRVSCompatible
Expand Down Expand Up @@ -800,6 +802,15 @@ export const GeneratedInputField = React.memo(
)
}

if (isHiddenFieldType(field)) {
return (
<Hidden.Input
{...inputProps}
value={field.value as string | undefined}
/>
)
}

throw new Error(`Unsupported field ${JSON.stringify(fieldDefinition)}`)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const COLUMNS = {
ICON_WITH_NAME_EVENT: 'iconWithNameEvent',
EVENT: 'type',
DATE_OF_EVENT: 'dateOfEvent',
PLACE_OF_EVENT: 'placeOfEvent',
SENT_FOR_REVIEW: 'sentForReview',
SENT_FOR_UPDATES: 'sentForUpdates',
SENT_FOR_APPROVAL: 'sentForApproval',
Expand Down Expand Up @@ -98,6 +99,9 @@ function changeSortedColumn(
case COLUMNS.DATE_OF_EVENT:
newSortedCol = COLUMNS.DATE_OF_EVENT
break
case COLUMNS.PLACE_OF_EVENT:
newSortedCol = COLUMNS.PLACE_OF_EVENT
break
case COLUMNS.SENT_FOR_REVIEW:
newSortedCol = COLUMNS.SENT_FOR_REVIEW
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
EventConfig,
EventDocument,
EventState,
getAcceptedActions,
FieldTypesToHideInReview,
getDeclaration,
isFieldDisplayedOnReview,
ValidatorContext
Expand Down Expand Up @@ -199,6 +199,12 @@ function DeclarationComparisonTableComponent({
<>
{declarationConfig.pages.map((page) => {
const changedFields = page.fields
.filter(
({ type }) =>
!FieldTypesToHideInReview.some(
(typeToHide) => type === typeToHide
)
)
.filter((field) =>
isFieldDisplayedOnReview(field, latestDeclaration, validatorContext)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 type { Meta, StoryObj } from '@storybook/react'
import React from 'react'
import styled from 'styled-components'
import { expect, within } from '@storybook/test'
import { TRPCProvider } from '@client/v2-events/trpc'
import { Hidden } from './Hidden'

const meta: Meta<typeof Hidden.Input> = {
title: 'Inputs/Hidden',
component: Hidden.Input,
args: {},
decorators: [
(Story) => (
<TRPCProvider>
<Story />
</TRPCProvider>
)
]
}

export default meta

const Box = styled.div`
border: 2px solid #000;
padding: 1rem;
`

function HiddenComponentBox({ children }: { children: React.ReactNode }) {
return (
<div>
<h2>
{
'Inside the box there is a hidden component, which is not rendered in the UI, but is present in the DOM'
}
</h2>
<Box>{children}</Box>
</div>
)
}

export const HiddenInput: StoryObj<typeof Hidden.Input> = {
args: {
id: 'hidden-input',
value: 'hidden value'
},
render: (args) => (
<HiddenComponentBox>
<Hidden.Input {...args} />
</HiddenComponentBox>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const hiddenInput = await canvas.findByTestId('text__hidden-input')
await expect(hiddenInput).toBeInTheDocument()
await expect(hiddenInput).toHaveValue('hidden value')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 * as React from 'react'

function HiddenInput({ id, value }: { id: string; value?: string }) {
return (
<input
data-testid={`text__${id}`}
disabled={true}
id={id}
name={id}
type="hidden"
value={value}
/>
)
}

export const Hidden = {
Input: HiddenInput,
stringify: (value: string | undefined) => {
return value?.toString() || ''
}
}
18 changes: 17 additions & 1 deletion packages/commons/src/events/EventConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { TranslationConfig } from './TranslationConfig'
import { AdvancedSearchConfig, EventFieldId } from './AdvancedSearchConfig'
import { findAllFields, getDeclarationFields } from './utils'
import { DeclarationFormConfig } from './FormConfig'

import { FieldType } from './FieldType'
import { FieldReference } from './FieldConfig'
import { FlagConfig, InherentFlags } from './Flag'
Expand All @@ -35,6 +34,9 @@ export const EventConfig = z
dateOfEvent: FieldReference.optional().describe(
'Reference to the field capturing the date of the event (e.g. date of birth). Defaults to the event creation date if unspecified.'
),
placeOfEvent: FieldReference.optional().describe(
'Reference to the field capturing the place of the event (e.g. place of birth). Defaults to the meta.createdAtLocation if unspecified.'
),
title: TranslationConfig.describe(
'Title template for the singular event, supporting variables (e.g. "{applicant.name.firstname} {applicant.name.surname}").'
),
Expand Down Expand Up @@ -134,6 +136,20 @@ export const EventConfig = z
}
}

if (event.placeOfEvent) {
const eventPlaceFieldId = getDeclarationFields(event).find(
({ id }) => id === event.placeOfEvent?.$$field
)
if (!eventPlaceFieldId) {
ctx.addIssue({
code: 'custom',
message: `Place of event field id must match a field id in the event.declaration fields.
Invalid place of event field ID for event ${event.id}: ${event.placeOfEvent.$$field}`,
path: ['placeOfEvent']
})
}
}

const isInherentFlag = (value: unknown): value is InherentFlags =>
Object.values(InherentFlags).includes(value as InherentFlags)

Expand Down
7 changes: 7 additions & 0 deletions packages/commons/src/events/EventMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const EventMetadata = z.object({
.datetime()
.describe('The timestamp when the event was first created and saved.'),
dateOfEvent: ZodDate.nullish(),
placeOfEvent: CreatedAtLocation,
createdBy: z.string().describe('ID of the user who created the event.'),
createdByUserType: z
.enum(['user', 'system'])
Expand Down Expand Up @@ -147,6 +148,7 @@ export const EventMetadataKeysArray = [
'status',
'createdAt',
'dateOfEvent',
'placeOfEvent',
'createdBy',
'createdByUserType',
'updatedByUserRole',
Expand Down Expand Up @@ -192,6 +194,11 @@ export const eventMetadataLabelMap: Record<
defaultMessage: 'Date of Event',
description: 'Date of Event'
},
'event.placeOfEvent': {
id: 'event.placeOfEvent.label',
defaultMessage: 'Place of Event',
description: 'Place of Event'
},
'event.createdAtLocation': {
id: 'event.createdAtLocation.label',
defaultMessage: 'Location',
Expand Down
13 changes: 12 additions & 1 deletion packages/commons/src/events/FieldConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,16 @@ const LoaderField = BaseField.extend({

export type LoaderField = z.infer<typeof LoaderField>

const HiddenField = BaseField.extend({
type: z.literal(FieldType.ALPHA_HIDDEN),
required: z.boolean().default(false).optional(),
defaultValue: TextValue.optional()
}).describe(
'A non-interactive, hidden field that only hold a value in the form'
)

export type HiddenField = z.infer<typeof HiddenField>

export const FieldConfig = z
.discriminatedUnion('type', [
Address,
Expand Down Expand Up @@ -886,7 +896,8 @@ export const FieldConfig = z
IdReaderField,
QueryParamReaderField,
LoaderField,
SearchField
SearchField,
HiddenField
])
.meta({
description: 'Form field configuration',
Expand Down
6 changes: 4 additions & 2 deletions packages/commons/src/events/FieldType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const FieldType = {
QUERY_PARAM_READER: 'QUERY_PARAM_READER',
QR_READER: 'QR_READER',
ID_READER: 'ID_READER',
LOADER: 'LOADER'
LOADER: 'LOADER',
ALPHA_HIDDEN: 'ALPHA_HIDDEN'
} as const

/**
Expand Down Expand Up @@ -86,5 +87,6 @@ export const FieldTypesToHideInReview = [
FieldType.LOADER,
FieldType.HTTP,
FieldType.QUERY_PARAM_READER,
FieldType.DATA
FieldType.DATA,
FieldType.ALPHA_HIDDEN
]
12 changes: 11 additions & 1 deletion packages/commons/src/events/FieldTypeMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ import {
QrReaderField,
IdReaderField,
LoaderField,
AgeField
AgeField,
HiddenField
} from './FieldConfig'
import { FieldType } from './FieldType'
import {
Expand Down Expand Up @@ -159,6 +160,7 @@ export function mapFieldTypeToZod(field: FieldConfig, actionType?: ActionType) {
case FieldType.VERIFICATION_STATUS:
case FieldType.ID:
case FieldType.LOADER:
case FieldType.ALPHA_HIDDEN:
schema = field.required ? NonEmptyTextValue : TextValue
break
case FieldType.NUMBER:
Expand Down Expand Up @@ -250,6 +252,7 @@ export function mapFieldTypeToEmptyValue(field: FieldConfig) {
case FieldType.QR_READER:
case FieldType.ID_READER:
case FieldType.LOADER:
case FieldType.ALPHA_HIDDEN:
return null
case FieldType.ADDRESS:
return {
Expand Down Expand Up @@ -555,6 +558,13 @@ export const isLoaderFieldType = (field: {
return field.config.type === FieldType.LOADER
}

export const isHiddenFieldType = (field: {
config: FieldConfig
value: FieldValue | FieldUpdateValue
}): field is { value: undefined; config: HiddenField } => {
return field.config.type === FieldType.ALPHA_HIDDEN
}

export type NonInteractiveFieldType =
| Divider
| PageHeader
Expand Down
Loading