Skip to content

Commit 0eb454d

Browse files
cibeliusNil20Nil20
authored
Support action 'SHOW' and 'ENABLE' conditionals (#11132)
* add conditionals schema def * progress with backend conditional validation * minor lint fix * implement conditional check for actions * move custom action config to events backend only * remove import * add action hidden conditional check * fix hiding of custom actions * remove unused action config definitions * remove unused action config schemas, add conditional effects to all action types * minor refactor * fix lint issue * use shorthand * minor refactor * add back missing config * fix hook issue * feat: add flags conditional (#11168) * feat: add flags conditional * fix: amend export of new package * refactor and support array of flags * add support for status conditional * fix lint error * fix import --------- Co-authored-by: Nil20 <[email protected]> Co-authored-by: Cihan Bebek <[email protected]> --------- Co-authored-by: Md. Ashikul Alam <[email protected]> Co-authored-by: Nil20 <[email protected]>
1 parent d8c194e commit 0eb454d

File tree

15 files changed

+868
-147
lines changed

15 files changed

+868
-147
lines changed

packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function EventOverviewFull({ event }: { event: EventDocument }) {
9393
<EventSummary
9494
event={flattenedEventIndex}
9595
eventConfiguration={eventConfiguration}
96-
flags={flags}
96+
eventIndex={eventIndex}
9797
/>
9898
</Content>
9999
)
@@ -148,7 +148,7 @@ function EventOverviewProtected({ eventIndex }: { eventIndex: EventIndex }) {
148148
hideSecuredFields
149149
event={flattenedEventIndex}
150150
eventConfiguration={eventConfiguration}
151-
flags={flags}
151+
eventIndex={eventIndex}
152152
/>
153153
</Content>
154154
)

packages/client/src/v2-events/features/workqueues/EventOverview/components/EventSummary.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
getDeclarationFields,
1717
areConditionsMet,
1818
getMixedPath,
19-
Flag
19+
Flag,
20+
EventIndex
2021
} from '@opencrvs/commons/client'
2122
import { FieldValue } from '@opencrvs/commons/client'
2223
import { useIntlFormatMessageWithFlattenedParams } from '@client/v2-events/messages/utils'
@@ -115,17 +116,17 @@ export const summaryMessages = messages
115116
export function EventSummary({
116117
event,
117118
eventConfiguration,
118-
flags,
119+
eventIndex,
119120
hideSecuredFields = false
120121
}: {
121122
event: Record<string, FieldValue>
122123
eventConfiguration: EventConfig
123-
flags: Flag[]
124+
eventIndex: EventIndex
124125
hideSecuredFields?: boolean
125126
}) {
126127
const intl = useIntlFormatMessageWithFlattenedParams()
127128
const validationContext = useValidatorContext()
128-
const flagLabels = useFlagLabelsString(eventConfiguration, flags)
129+
const flagLabels = useFlagLabelsString(eventConfiguration, eventIndex.flags)
129130
const { summary, label: eventLabelMessage } = eventConfiguration
130131
const declarationFields = getDeclarationFields(eventConfiguration)
131132
const securedFields = declarationFields
@@ -135,7 +136,12 @@ export function EventSummary({
135136
const configuredFields = summary.fields.map((field) => {
136137
if (
137138
field.conditionals &&
138-
!areConditionsMet(field.conditionals, event, validationContext)
139+
!areConditionsMet(
140+
field.conditionals,
141+
event,
142+
validationContext,
143+
eventIndex
144+
)
139145
) {
140146
return null
141147
}

packages/client/src/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations.tsx

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ import {
3030
configurableEventScopeAllowed,
3131
ITokenPayload,
3232
ActionTypes,
33-
CustomActionConfig
33+
CustomActionConfig,
34+
isActionEnabled,
35+
isActionVisible,
36+
getActionConfig,
37+
ValidatorContext,
38+
EventConfig
3439
} from '@opencrvs/commons/client'
3540
import { IconProps } from '@opencrvs/components/src/Icon'
3641
import { useEvents } from '@client/v2-events/features/events/useEvents/useEvents'
@@ -55,6 +60,7 @@ import {
5560
useCustomActionModal
5661
} from '@client/v2-events/features/events/actions/quick-actions/useQuickActionModal'
5762
import { useRejectionModal } from '@client/v2-events/features/events/actions/reject/useRejectionModal'
63+
import { useValidatorContext } from '@client/v2-events/hooks/useValidatorContext'
5864

5965
const STATUSES_THAT_CAN_BE_ASSIGNED: EventStatus[] = [
6066
EventStatus.enum.NOTIFIED,
@@ -182,6 +188,7 @@ interface ActionConfig {
182188
ctaLabel?: TranslationConfig
183189
disabled?: boolean
184190
hidden?: boolean
191+
customActionType?: string
185192
}
186193

187194
type ActionMenuActionType = WorkqueueActionType | ClientSpecificAction
@@ -555,8 +562,6 @@ function useCustomActionConfigs(
555562
const scopes = useSelector(getScope) ?? []
556563
const { eventConfiguration } = useEventConfiguration(event.type)
557564
const { customActionModal, onCustomAction } = useCustomActionModal(event)
558-
559-
// @TODO: Could we share these with the useViewableActionConfigurations() function?
560565
const { findFromCache } = useEvents().getEvent
561566
const isDownloaded = Boolean(findFromCache(event.id).data)
562567
const assignmentStatus = getAssignmentStatus(event, authentication.sub)
@@ -577,7 +582,8 @@ function useCustomActionConfigs(
577582
onCustomAction(action, workqueue),
578583
disabled: !isDownloadedAndAssignedToUser,
579584
hidden: false,
580-
type: ActionType.CUSTOM
585+
type: ActionType.CUSTOM,
586+
customActionType: action.customActionType
581587
}))
582588
}, [eventConfiguration, onCustomAction, isDownloadedAndAssignedToUser])
583589

@@ -597,6 +603,39 @@ function useCustomActionConfigs(
597603
return { customActionModal, customActionConfigs }
598604
}
599605

606+
/** Actions might have configured SHOW or ENABLE conditionals. Let's apply their effects here. */
607+
function applyActionConditionalEffects({
608+
event,
609+
action,
610+
validatorContext,
611+
eventConfiguration
612+
}: {
613+
event: EventIndex
614+
action: ActionMenuItem
615+
validatorContext: ValidatorContext
616+
eventConfiguration: EventConfig
617+
}) {
618+
const actionConfig = getActionConfig({
619+
eventConfiguration,
620+
actionType: action.type as ActionType,
621+
customActionType:
622+
'customActionType' in action ? action.customActionType : undefined
623+
})
624+
625+
if (!actionConfig || !actionConfig.conditionals) {
626+
return action
627+
}
628+
629+
const hidden = !isActionVisible(actionConfig, event, validatorContext)
630+
const disabled = !isActionEnabled(actionConfig, event, validatorContext)
631+
632+
return {
633+
...action,
634+
hidden: action.hidden || hidden,
635+
disabled: action.disabled || disabled
636+
}
637+
}
638+
600639
/**
601640
*
602641
* NOTE: In principle, you should never add new business rules to the `useAction` hook alone. All the actions are validated by the server and their order is enforced.
@@ -611,6 +650,8 @@ export function useAllowedActionConfigurations(
611650
const isPending = event.flags.some((flag) => flag.endsWith(':requested'))
612651
const { isActionAllowed } = useUserAllowedActions(event.type)
613652
const drafts = useDrafts()
653+
const validatorContext = useValidatorContext()
654+
const { eventConfiguration } = useEventConfiguration(event.type)
614655

615656
const openDraft = drafts
616657
.getAllRemoteDrafts()
@@ -645,14 +686,22 @@ export function useAllowedActionConfigurations(
645686
.filter((action) => isActionAllowed(action))
646687
// We need to transform data and filter out hidden actions to ensure hasOnlyMetaAction receives the correct values.
647688
.map((a) => ({ ...config[a], type: a }))
648-
.filter((a: ActionConfig) => !a.hidden)
649689

650690
const { customActionModal, customActionConfigs } = useCustomActionConfigs(
651691
event,
652692
authentication
653693
)
654694

655695
const allActionConfigs = [...allowedActionConfigs, ...customActionConfigs]
696+
.map((action) =>
697+
applyActionConditionalEffects({
698+
event,
699+
action,
700+
validatorContext,
701+
eventConfiguration
702+
})
703+
)
704+
.filter((a: ActionConfig) => !a.hidden)
656705

657706
// Check if the user can perform any action other than ASSIGN, or UNASSIGN
658707
const hasOnlyMetaActions = allActionConfigs.every(({ type }) =>

packages/commons/src/conditionals/conditionals.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import {
1818
ConditionalParameters,
1919
UserConditionalParameters,
2020
EventConditionalParameters,
21-
FormConditionalParameters
21+
FormConditionalParameters,
22+
EventStateConditionalParameters,
23+
flag,
24+
status
2225
} from './conditionals'
2326
import { formatISO } from 'date-fns'
2427
import { SCOPES } from '../scopes'
@@ -28,6 +31,7 @@ import { field } from '../events/field'
2831
import { event } from '../events/event'
2932
import { TokenUserType } from '../authentication'
3033
import { UUID } from '../uuid'
34+
import { EventStatus, InherentFlags } from '../client'
3135

3236
/* eslint-disable max-lines */
3337

@@ -1018,6 +1022,73 @@ describe('"user" conditionals', () => {
10181022
})
10191023
})
10201024

1025+
describe('"flag" conditionals', () => {
1026+
it('validates "flag()" conditional', () => {
1027+
const params = {
1028+
$flags: [InherentFlags.CORRECTION_REQUESTED],
1029+
$status: EventStatus.enum.REGISTERED,
1030+
$now: formatISO(new Date(), { representation: 'date' }),
1031+
$online: true
1032+
} satisfies EventStateConditionalParameters
1033+
1034+
expect(validate(flag(InherentFlags.INCOMPLETE), params)).toBe(false)
1035+
expect(validate(flag(InherentFlags.CORRECTION_REQUESTED), params)).toBe(
1036+
true
1037+
)
1038+
})
1039+
1040+
it('validation fails if flags array is empty', () => {
1041+
const params = {
1042+
$flags: [],
1043+
$status: EventStatus.enum.REGISTERED,
1044+
$now: formatISO(new Date(), { representation: 'date' }),
1045+
$online: true
1046+
} satisfies EventStateConditionalParameters
1047+
1048+
expect(validate(flag(InherentFlags.CORRECTION_REQUESTED), params)).toBe(
1049+
false
1050+
)
1051+
})
1052+
1053+
it('validation fails if params dont include flags', () => {
1054+
const params = {
1055+
$status: EventStatus.enum.REGISTERED,
1056+
$now: formatISO(new Date(), { representation: 'date' }),
1057+
$online: true
1058+
}
1059+
1060+
// @ts-expect-error testing missing flags param
1061+
expect(validate(flag(InherentFlags.CORRECTION_REQUESTED), params)).toBe(
1062+
false
1063+
)
1064+
})
1065+
})
1066+
1067+
describe('"status" conditionals', () => {
1068+
it('validates "status()" conditional', () => {
1069+
const params = {
1070+
$flags: [],
1071+
$status: EventStatus.enum.REGISTERED,
1072+
$now: formatISO(new Date(), { representation: 'date' }),
1073+
$online: true
1074+
} satisfies EventStateConditionalParameters
1075+
1076+
expect(validate(status('VALIDATED'), params)).toBe(false)
1077+
expect(validate(status('REGISTERED'), params)).toBe(true)
1078+
})
1079+
1080+
it('validation fails if params dont include status', () => {
1081+
const params = {
1082+
$flags: [],
1083+
$now: formatISO(new Date(), { representation: 'date' }),
1084+
$online: true
1085+
}
1086+
1087+
// @ts-expect-error testing missing status param
1088+
expect(validate(status('REGISTERED'), params)).toBe(false)
1089+
})
1090+
})
1091+
10211092
describe('"event" conditionals', () => {
10221093
it('validates "event.hasAction" conditional', () => {
10231094
const now = formatISO(new Date(), { representation: 'date' })

packages/commons/src/conditionals/conditionals.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PartialSchema as AjvJSONSchemaType } from 'ajv/dist/types/json-schema'
1616
import { userSerializer } from '../events/serializers/user/serializer'
1717
import { omitKeyDeep } from '../utils'
1818
import { UUID } from '../uuid'
19+
import { EventStatus } from '../events/EventMetadata'
1920

2021
/* eslint-disable max-lines */
2122

@@ -68,10 +69,16 @@ export type FormConditionalParameters = CommonConditionalParameters & {
6869
$leafAdminStructureLocationIds?: Array<{ id: UUID }>
6970
}
7071

72+
export type EventStateConditionalParameters = CommonConditionalParameters & {
73+
$flags: string[]
74+
$status: EventStatus
75+
}
76+
7177
export type ConditionalParameters =
7278
| UserConditionalParameters
7379
| EventConditionalParameters
7480
| FormConditionalParameters
81+
| EventStateConditionalParameters
7582

7683
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7784
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
@@ -328,6 +335,37 @@ function defineComparison(
328335
})
329336
}
330337

338+
/** Check if an event flag is present */
339+
export function flag(flagvalue: string) {
340+
return defineConditional({
341+
type: 'object',
342+
properties: {
343+
$flags: {
344+
type: 'array',
345+
contains: {
346+
type: 'string',
347+
const: flagvalue
348+
}
349+
}
350+
},
351+
required: ['$flags']
352+
})
353+
}
354+
355+
/** Check if an event flag is present */
356+
export function status(statusValue: EventStatus) {
357+
return defineConditional({
358+
type: 'object',
359+
properties: {
360+
$status: {
361+
type: 'string',
362+
const: statusValue
363+
}
364+
},
365+
required: ['$status']
366+
})
367+
}
368+
331369
/**
332370
* Generate conditional rules for a form field.
333371
*

0 commit comments

Comments
 (0)