-
Notifications
You must be signed in to change notification settings - Fork 92
New declaration review page action UI #11184
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
Merged
Merged
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
0623977
progress with new declaring ui
cibelius af55478
progress
cibelius d71ea9d
progress
cibelius 92c4a20
progress
cibelius 6e65a1f
minor fixes
cibelius aace406
cleanup
cibelius 1bfa883
fix lint issue
cibelius 316e9a9
progress
cibelius 86aac00
progress
cibelius d5f3525
fix lint issue
cibelius 5ca1d23
cleanup
cibelius 2470bbb
add check for registering
cibelius e66dc97
add comments
cibelius aa861a0
refactor
cibelius 9db43be
fix lint issues
cibelius b32ce9f
fix storybook tests
cibelius 0ff5eb7
fix lint issues
cibelius 9802639
add eslint disable rule
cibelius 3612adb
fix modal result handling
cibelius a681f1e
rename incomplete -> hasValidationErrors and add comment
cibelius 4a7fea6
try stabilizing test
cibelius 081ec10
Merge branch 'develop' into declaring-poc
cibelius 8e25f89
add wait to fix flakyness on test?
cibelius 3451c1d
remove timeout
cibelius File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
344 changes: 344 additions & 0 deletions
344
packages/client/src/v2-events/features/events/actions/declare/DeclareActionMenu.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,344 @@ | ||
| /* | ||
| * 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 React, { useCallback } from 'react' | ||
| import { useIntl } from 'react-intl' | ||
| import { useTypedSearchParams } from 'react-router-typesafe-routes/dom' | ||
| import { v4 as uuid } from 'uuid' | ||
| import { useSelector } from 'react-redux' | ||
| import { | ||
| ActionType, | ||
| getDeclaration, | ||
| EventStatus, | ||
| EventDocument, | ||
| getCurrentEventState, | ||
| TokenUserType, | ||
| UUID, | ||
| isActionAvailable, | ||
| getActionConfig | ||
| } from '@opencrvs/commons/client' | ||
| import { PrimaryButton } from '@opencrvs/components/lib/buttons' | ||
| import { DropdownMenu } from '@opencrvs/components/lib/Dropdown' | ||
| import { CaretDown } from '@opencrvs/components/lib/Icon/all-icons' | ||
| import { Icon } from '@opencrvs/components' | ||
| import { getUserDetails } from '@client/profile/profileSelectors' | ||
| import { useModal } from '@client/v2-events/hooks/useModal' | ||
| import { useEvents } from '@client/v2-events/features/events/useEvents/useEvents' | ||
| import { useDrafts } from '@client/v2-events/features/drafts/useDrafts' | ||
| import { messages } from '@client/i18n/messages/views/action' | ||
| import { ROUTES } from '@client/v2-events/routes' | ||
| import { useEventFormNavigation } from '@client/v2-events/features/events/useEventFormNavigation' | ||
| import { messages as formHeaderMessages } from '@client/v2-events/layouts/form/FormHeader' | ||
| import { useUserAllowedActions } from '@client/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations' | ||
| import { actionLabels } from '@client/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations' | ||
| import { useValidatorContext } from '@client/v2-events/hooks/useValidatorContext' | ||
| import { Review } from '@client/v2-events/features/events/components/Review' | ||
| import { useSaveAndExitModal } from '@client/v2-events/components/SaveAndExitModal' | ||
| import { validationErrorsInActionFormExist } from '@client/v2-events/components/forms/validation' | ||
| import { reviewMessages } from '@client/v2-events/features/events/actions/messages' | ||
| import { useActionAnnotation } from '../../useActionAnnotation' | ||
| import { useEventFormData } from '../../useEventFormData' | ||
| import { useRejectionModal } from '../reject/useRejectionModal' | ||
| import { useEventConfiguration } from '../../useEventConfiguration' | ||
|
|
||
| // @TODO: These should be made configurable in action config, so that different event types can have different copy | ||
| // This will be implemented as part of https://github.com/opencrvs/opencrvs-core/issues/10900 | ||
| const actionModalMessages = { | ||
| [ActionType.NOTIFY]: reviewMessages.incomplete.declare, | ||
| [ActionType.DECLARE]: reviewMessages.complete.declare, | ||
| [ActionType.VALIDATE]: reviewMessages.complete.validate, | ||
| [ActionType.REGISTER]: reviewMessages.complete.register | ||
| } | ||
|
|
||
| /** | ||
| * Declaration actions contain actions available on the review page of the declare flow. This can include: | ||
| * - Notify (incomplete records) | ||
| * - Declare (non-incomplete records) | ||
| * - Validate (aka. 'direct validation', which means declare+validate actions) | ||
| * - Register (aka. 'direct registration', which means declare+validate+register actions) | ||
| * - Reject (only available for previously notified events) | ||
| * - Save and exit | ||
| * - Delete declaration | ||
| */ | ||
| function useDeclarationActions(event: EventDocument) { | ||
| const eventType = event.type | ||
| const drafts = useDrafts() | ||
| const { | ||
| closeActionView, | ||
| deleteDeclaration, | ||
| modal: deleteDeclarationModal | ||
| } = useEventFormNavigation() | ||
| const { eventConfiguration } = useEventConfiguration(eventType) | ||
| const formConfig = getDeclaration(eventConfiguration) | ||
| const validatorContext = useValidatorContext() | ||
| const declaration = useEventFormData((state) => state.getFormValues()) | ||
| const { getAnnotation } = useActionAnnotation() | ||
| const annotation = getAnnotation() | ||
| const [modal, openModal] = useModal() | ||
| const { rejectionModal, handleRejection } = useRejectionModal(event.id) | ||
| const [{ workqueue: slug }] = useTypedSearchParams( | ||
| ROUTES.V2.EVENTS.DECLARE.REVIEW | ||
| ) | ||
| const { saveAndExitModal, handleSaveAndExit } = useSaveAndExitModal() | ||
| const events = useEvents() | ||
| const userDetails = useSelector(getUserDetails) | ||
|
|
||
| const mutateFns = { | ||
| [ActionType.NOTIFY]: events.actions.notify.mutate, | ||
| [ActionType.DECLARE]: events.actions.declare.mutate, | ||
| [ActionType.VALIDATE]: events.customActions.validateOnDeclare.mutate, | ||
| [ActionType.REGISTER]: events.customActions.registerOnDeclare.mutate | ||
| } | ||
|
|
||
| const actionConfiguration = eventConfiguration.actions.find( | ||
| (a) => a.type === ActionType.DECLARE | ||
| ) | ||
| if (!actionConfiguration) { | ||
| throw new Error('Action configuration not found') | ||
| } | ||
|
|
||
| const reviewConfig = actionConfiguration.review | ||
|
|
||
| const incomplete = validationErrorsInActionFormExist({ | ||
| formConfig, | ||
| form: declaration, | ||
| annotation, | ||
| context: validatorContext, | ||
| reviewFields: reviewConfig.fields | ||
| }) | ||
|
|
||
| const { isActionAllowed } = useUserAllowedActions(eventType) | ||
| const eventId = event.id | ||
|
|
||
| const onDelete = useCallback(async () => { | ||
| await deleteDeclaration(eventId) | ||
| }, [eventId, deleteDeclaration]) | ||
|
|
||
| async function handleDeclaration(actionType: keyof typeof mutateFns) { | ||
| const mutateFn = mutateFns[actionType] | ||
| const msgs = actionModalMessages[actionType] | ||
| const confirmedDeclaration = await openModal<boolean | null>((close) => { | ||
| return ( | ||
| <Review.ActionModal.Accept | ||
| action="Declare" | ||
| close={close} | ||
| copy={{ | ||
| // @TODO: make the messages configurable in action config? | ||
| // Will be implemented as part of https://github.com/opencrvs/opencrvs-core/issues/10900 | ||
| ...msgs.modal, | ||
| onConfirm: actionLabels[actionType], | ||
| eventLabel: eventConfiguration.label | ||
| }} | ||
| /> | ||
| ) | ||
| }) | ||
|
|
||
| if (confirmedDeclaration) { | ||
| mutateFn({ | ||
| eventId, | ||
| declaration, | ||
| annotation, | ||
| transactionId: uuid() | ||
| }) | ||
| closeActionView(slug) | ||
| } | ||
| } | ||
|
|
||
| const eventIndex = getCurrentEventState(event, eventConfiguration) | ||
|
|
||
| /** | ||
| * Logic to check whether direct declare + validate or declare + validate + register is possible. | ||
| * We do this by 'looking in to the future' by applying the would-be actions to the event, | ||
| * and checking if the validate and register actions are still allowed. | ||
| */ | ||
| function isDirectActionPossible( | ||
| actionType: typeof ActionType.VALIDATE | typeof ActionType.REGISTER | ||
| ) { | ||
| if (!userDetails) { | ||
| return false | ||
| } | ||
|
|
||
| const eventAfterDeclare = { | ||
| ...event, | ||
| actions: event.actions.concat({ | ||
| type: ActionType.DECLARE, | ||
| id: 'placeholder' as UUID, | ||
| transactionId: 'placeholder' as UUID, | ||
| createdByUserType: TokenUserType.enum.user, | ||
| createdByRole: userDetails.role.id, | ||
| declaration, | ||
| annotation, | ||
| createdAt: new Date().toISOString(), | ||
| createdBy: userDetails.id, | ||
| originalActionId: null, | ||
| status: 'Accepted', | ||
| createdBySignature: undefined, | ||
| createdAtLocation: userDetails.primaryOffice.id as UUID | ||
| }) | ||
| } | ||
|
|
||
| const eventIndexAfterDeclare = getCurrentEventState( | ||
| eventAfterDeclare, | ||
| eventConfiguration | ||
| ) | ||
|
|
||
| const validateActionConfig = getActionConfig({ | ||
| eventConfiguration, | ||
| actionType: ActionType.VALIDATE | ||
| }) | ||
|
|
||
| if (!validateActionConfig) { | ||
| return false | ||
| } | ||
|
|
||
| const validateIsAvailable = isActionAvailable( | ||
| validateActionConfig, | ||
| eventIndexAfterDeclare, | ||
| validatorContext | ||
| ) | ||
|
|
||
| if (actionType === ActionType.VALIDATE) { | ||
| return validateIsAvailable | ||
| } | ||
|
|
||
| const eventAfterValidate = { | ||
| ...eventAfterDeclare, | ||
| actions: eventAfterDeclare.actions.concat({ | ||
| type: ActionType.VALIDATE, | ||
| id: 'placeholder' as UUID, | ||
| transactionId: 'placeholder' as UUID, | ||
| createdByUserType: TokenUserType.enum.user, | ||
| createdByRole: userDetails.role.id, | ||
| declaration, | ||
| annotation, | ||
| createdAt: new Date().toISOString(), | ||
| createdBy: userDetails.id, | ||
| originalActionId: null, | ||
| status: 'Accepted', | ||
| createdBySignature: undefined, | ||
| createdAtLocation: userDetails.primaryOffice.id as UUID | ||
| }) | ||
| } | ||
|
|
||
| const registerActionConfig = getActionConfig({ | ||
| eventConfiguration, | ||
| actionType: ActionType.REGISTER | ||
| }) | ||
|
|
||
| if (!registerActionConfig) { | ||
| return false | ||
| } | ||
|
|
||
| const eventIndexAfterValidate = getCurrentEventState( | ||
| eventAfterValidate, | ||
| eventConfiguration | ||
| ) | ||
|
|
||
| return isActionAvailable( | ||
| registerActionConfig, | ||
| eventIndexAfterValidate, | ||
| validatorContext | ||
| ) | ||
| } | ||
|
|
||
| return { | ||
| modals: [modal, rejectionModal, saveAndExitModal, deleteDeclarationModal], | ||
| actions: [ | ||
| { | ||
| icon: 'Check' as const, | ||
| label: actionLabels[ActionType.REGISTER], | ||
| onClick: async () => handleDeclaration(ActionType.REGISTER), | ||
| hidden: !isActionAllowed(ActionType.REGISTER), | ||
| disabled: incomplete || !isDirectActionPossible(ActionType.REGISTER) | ||
| }, | ||
| { | ||
| icon: 'PaperPlaneTilt' as const, | ||
| label: actionLabels[ActionType.VALIDATE], | ||
| onClick: async () => handleDeclaration(ActionType.VALIDATE), | ||
| hidden: !isActionAllowed(ActionType.VALIDATE), | ||
| disabled: incomplete || !isDirectActionPossible(ActionType.VALIDATE) | ||
| }, | ||
| { | ||
| icon: 'UploadSimple' as const, | ||
| label: actionLabels[ActionType.DECLARE], | ||
| onClick: async () => handleDeclaration(ActionType.DECLARE), | ||
| hidden: !isActionAllowed(ActionType.DECLARE), | ||
| disabled: incomplete | ||
| }, | ||
| { | ||
| icon: 'UploadSimple' as const, | ||
| label: actionLabels[ActionType.NOTIFY], | ||
| onClick: async () => handleDeclaration(ActionType.NOTIFY), | ||
| hidden: !isActionAllowed(ActionType.NOTIFY), | ||
| disabled: false | ||
| }, | ||
| { | ||
| icon: 'FileX' as const, | ||
| label: actionLabels[ActionType.REJECT], | ||
| onClick: async () => handleRejection(() => closeActionView(slug)), | ||
| hidden: eventIndex.status !== EventStatus.enum.NOTIFIED | ||
| }, | ||
| { | ||
| icon: 'FloppyDisk' as const, | ||
| label: formHeaderMessages.saveExitButton, | ||
| onClick: async () => | ||
| handleSaveAndExit(() => { | ||
| drafts.submitLocalDraft() | ||
| closeActionView(slug) | ||
| }), | ||
| hidden: false | ||
| }, | ||
| { | ||
| icon: 'Trash' as const, | ||
| label: formHeaderMessages.deleteDeclaration, | ||
| onClick: async () => onDelete(), | ||
| hidden: false | ||
| } | ||
| ].filter((a) => !a.hidden) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Menu component available on the declaration review page. | ||
| * We have tried to contain all logic to which actions are available in the declaration in this component. | ||
| * */ | ||
| export function DeclareActionMenu({ event }: { event: EventDocument }) { | ||
| const intl = useIntl() | ||
| const { modals, actions } = useDeclarationActions(event) | ||
|
|
||
| return ( | ||
| <> | ||
| <DropdownMenu id="action"> | ||
| <DropdownMenu.Trigger asChild> | ||
| <PrimaryButton | ||
| data-testid="action-dropdownMenu" | ||
| icon={() => <CaretDown />} | ||
| size="medium" | ||
| > | ||
| {intl.formatMessage(messages.action)} | ||
| </PrimaryButton> | ||
| </DropdownMenu.Trigger> | ||
| <DropdownMenu.Content> | ||
| {actions.map(({ onClick, icon, label, disabled }, index) => ( | ||
| <DropdownMenu.Item | ||
| key={index} | ||
| disabled={disabled} | ||
| onClick={onClick} | ||
| > | ||
| <Icon color="currentColor" name={icon} size="small" /> | ||
| {intl.formatMessage(label)} | ||
| </DropdownMenu.Item> | ||
| ))} | ||
| </DropdownMenu.Content> | ||
| </DropdownMenu> | ||
| {modals} | ||
| </> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.