diff --git a/packages/client/src/v2-events/features/events/actions/declare/DeclareActionMenu.tsx b/packages/client/src/v2-events/features/events/actions/declare/DeclareActionMenu.tsx new file mode 100644 index 00000000000..88d4f1b5ff9 --- /dev/null +++ b/packages/client/src/v2-events/features/events/actions/declare/DeclareActionMenu.tsx @@ -0,0 +1,353 @@ +/* + * 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 + + /** + * hasValidationErrors is true if: + * - the form has any field validation errors or + * - the form is incomplete + * + * If hasValidationErrors is true, the user is still able to Notify an event (if they have the required scope) + */ + const hasValidationErrors = 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((close) => { + return ( + + ) + }) + + 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: + hasValidationErrors || !isDirectActionPossible(ActionType.REGISTER) + }, + { + icon: 'PaperPlaneTilt' as const, + label: actionLabels[ActionType.VALIDATE], + onClick: async () => handleDeclaration(ActionType.VALIDATE), + hidden: !isActionAllowed(ActionType.VALIDATE), + disabled: + hasValidationErrors || !isDirectActionPossible(ActionType.VALIDATE) + }, + { + icon: 'UploadSimple' as const, + label: actionLabels[ActionType.DECLARE], + onClick: async () => handleDeclaration(ActionType.DECLARE), + hidden: !isActionAllowed(ActionType.DECLARE), + disabled: hasValidationErrors + }, + { + 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 ( + <> + + + } + size="medium" + > + {intl.formatMessage(messages.action)} + + + + {actions.map(({ onClick, icon, label, disabled }, index) => ( + + + {intl.formatMessage(label)} + + ))} + + + {modals} + + ) +} diff --git a/packages/client/src/v2-events/features/events/actions/declare/Review.interaction.stories.tsx b/packages/client/src/v2-events/features/events/actions/declare/Review.interaction.stories.tsx index a5f63d2475d..40ddc90c2a6 100644 --- a/packages/client/src/v2-events/features/events/actions/declare/Review.interaction.stories.tsx +++ b/packages/client/src/v2-events/features/events/actions/declare/Review.interaction.stories.tsx @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ /* * 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 @@ -165,20 +166,18 @@ export const ReviewForLocalRegistrarCompleteInteraction: Story = { const canvas = within(canvasElement) await waitFor(async () => { - const registerButton = await canvas.findByRole('button', { - name: 'Register' - }) - - await expect(registerButton).toBeEnabled() - await userEvent.click(registerButton) + await userEvent.click( + await canvas.findByRole('button', { name: 'Action' }) + ) + await userEvent.click(await canvas.findByText('Register')) }) - const modal = within(await canvas.findByRole('dialog')) + await canvas.findByRole('button', { name: 'Cancel' }) - await modal.findByText('Register the tennis club membership application?') - await modal.findByRole('button', { name: 'Cancel' }) await userEvent.click( - await modal.findByRole('button', { name: 'Register' }) + await canvas.findByRole('button', { + name: 'Register' + }) ) }) @@ -262,18 +261,19 @@ export const ReviewForRegistrationAgentCompleteInteraction: Story = { msw }, play: async ({ canvasElement, step }) => { - await step('Modal has scope based content', async () => { + await step('User can validate', async () => { const canvas = within(canvasElement) await userEvent.click( - await canvas.findByRole('button', { name: 'Send for approval' }) + await canvas.findByRole('button', { name: 'Action' }) ) + await userEvent.click(await canvas.findByText('Validate')) - const modal = within(await canvas.findByRole('dialog')) + await canvas.findByRole('button', { name: 'Cancel' }) - await modal.findByText('Send for approval?') - await modal.findByRole('button', { name: 'Cancel' }) await userEvent.click( - await modal.findByRole('button', { name: 'Confirm' }) + await canvas.findByRole('button', { + name: 'Validate' + }) ) }) @@ -319,19 +319,19 @@ export const ReviewForFieldAgentCompleteInteraction: Story = { msw }, play: async ({ canvasElement, step }) => { - await step('Modal has scope based content', async () => { + await step('User can perform declare action', async () => { const canvas = within(canvasElement) await userEvent.click( - await canvas.findByRole('button', { name: 'Send for review' }) + await canvas.findByRole('button', { name: 'Action' }) ) + await userEvent.click(await canvas.findByText('Declare')) - const modal = within(await canvas.findByRole('dialog')) + await canvas.findByRole('button', { name: 'Cancel' }) - await modal.findByText('Send for review?') - await modal.findByText('This declaration will be sent for review') - await modal.findByRole('button', { name: 'Cancel' }) await userEvent.click( - await modal.findByRole('button', { name: 'Confirm' }) + await canvas.findByRole('button', { + name: 'Declare' + }) ) }) @@ -434,18 +434,16 @@ export const ReviewForFieldAgentIncompleteInteraction: Story = { await step('Modal has scope based content', async () => { const canvas = within(canvasElement) await userEvent.click( - await canvas.findByRole('button', { name: 'Send for review' }) + await canvas.findByRole('button', { name: 'Action' }) ) + await userEvent.click(await canvas.findByText('Notify')) - const modal = within(await canvas.findByRole('dialog')) + await canvas.findByRole('button', { name: 'Cancel' }) - await modal.findByText('Send for review?') - await modal.findByRole('button', { name: 'Cancel' }) - await modal.findByText( - 'This incomplete declaration will be sent for review.' - ) await userEvent.click( - await modal.findByRole('button', { name: 'Confirm' }) + await canvas.findByRole('button', { + name: 'Notify' + }) ) }) @@ -513,7 +511,22 @@ export const ReviewForIncompleteNameInteraction: Story = { const backToReviewButton = await canvas.findByText('Back to review') await userEvent.click(backToReviewButton) - await canvas.findByText('Declaration incomplete') + + await userEvent.click( + await canvas.findByRole('button', { name: 'Action' }) + ) + + await expect( + canvas.getByText('Declare', { selector: 'li[disabled]' }) + ).toBeInTheDocument() + + await userEvent.click(await canvas.findByText('Notify')) + + await userEvent.click( + await canvas.findByRole('button', { + name: 'Notify' + }) + ) }) } } diff --git a/packages/client/src/v2-events/features/events/actions/declare/Review.tsx b/packages/client/src/v2-events/features/events/actions/declare/Review.tsx index 7fca042edd5..6a54df65361 100644 --- a/packages/client/src/v2-events/features/events/actions/declare/Review.tsx +++ b/packages/client/src/v2-events/features/events/actions/declare/Review.tsx @@ -15,31 +15,20 @@ import { useTypedParams, useTypedSearchParams } from 'react-router-typesafe-routes/dom' -import { - ActionType, - EventStatus, - getCurrentEventState, - getDeclaration, - InherentFlags -} from '@opencrvs/commons/client' +import { ActionType, getDeclaration } from '@opencrvs/commons/client' import { useEventConfiguration } from '@client/v2-events/features/events/useEventConfiguration' import { useEventFormData } from '@client/v2-events/features/events/useEventFormData' import { useActionAnnotation } from '@client/v2-events/features/events/useActionAnnotation' -import { useEventFormNavigation } from '@client/v2-events/features/events/useEventFormNavigation' import { useEvents } from '@client/v2-events/features/events/useEvents/useEvents' import { useModal } from '@client/v2-events/hooks/useModal' import { ROUTES } from '@client/v2-events/routes' import { Review as ReviewComponent } from '@client/v2-events/features/events/components/Review' import { FormLayout } from '@client/v2-events/layouts' import { makeFormFieldIdFormikCompatible } from '@client/v2-events/components/forms/utils' -import { useDrafts } from '@client/v2-events/features/drafts/useDrafts' import { withSuspense } from '@client/v2-events/components/withSuspense' -import { useSaveAndExitModal } from '@client/v2-events/components/SaveAndExitModal' import { useIntlFormatMessageWithFlattenedParams } from '@client/v2-events/messages/utils' -import { useUserAllowedActions } from '@client/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations' import { useValidatorContext } from '@client/v2-events/hooks/useValidatorContext' -import { useRejectionModal } from '../reject/useRejectionModal' -import { useReviewActionConfig } from './useReviewActionConfig' +import { DeclareActionMenu } from './DeclareActionMenu' export function Review() { const { eventId } = useTypedParams(ROUTES.V2.EVENTS.DECLARE.REVIEW) @@ -47,24 +36,13 @@ export function Review() { ROUTES.V2.EVENTS.DECLARE.REVIEW ) const events = useEvents() - const drafts = useDrafts() const navigate = useNavigate() - const { rejectionModal, handleRejection } = useRejectionModal(eventId) - const validatorContext = useValidatorContext() const [modal, openModal] = useModal() const { formatMessage } = useIntlFormatMessageWithFlattenedParams() - const { closeActionView } = useEventFormNavigation() - const { saveAndExitModal, handleSaveAndExit } = useSaveAndExitModal() - const event = events.getEvent.getFromCache(eventId) - const { eventConfiguration: config } = useEventConfiguration(event.type) - - const currentEventState = getCurrentEventState(event, config) - const formConfig = getDeclaration(config) - const actionConfiguration = config.actions.find( (a) => a.type === ActionType.DECLARE ) @@ -73,23 +51,11 @@ export function Review() { } const reviewConfig = actionConfiguration.review - const form = useEventFormData((state) => state.getFormValues()) const { setAnnotation, getAnnotation } = useActionAnnotation() const annotation = getAnnotation() - const { isActionAllowed } = useUserAllowedActions(event.type) - - const reviewActionConfiguration = useReviewActionConfig({ - eventType: event.type, - formConfig, - declaration: form, - annotation, - reviewFields: reviewConfig.fields, - validatorContext - }) - async function handleEdit({ pageId, fieldId, @@ -121,43 +87,10 @@ export function Review() { return } - async function handleDeclaration() { - const confirmedDeclaration = await openModal((close) => { - if (reviewActionConfiguration.messages.modal === undefined) { - // eslint-disable-next-line no-console - console.error( - 'Tried to render declare modal without message definitions.' - ) - return null - } - - return ( - - ) - }) - - if (confirmedDeclaration) { - reviewActionConfiguration.onConfirm(eventId) - closeActionView(slug) - } - } - return ( } route={ROUTES.V2.EVENTS.DECLARE} - onSaveAndExit={async () => - handleSaveAndExit(() => { - drafts.submitLocalDraft() - closeActionView(slug) - }) - } > setAnnotation(values)} onEdit={handleEdit} - > - handleRejection(() => closeActionView(slug)) - : undefined - } - /> - + /> {modal} - {rejectionModal} - {saveAndExitModal} ) } diff --git a/packages/client/src/v2-events/features/events/actions/declare/useReviewActionConfig.tsx b/packages/client/src/v2-events/features/events/actions/declare/useReviewActionConfig.tsx deleted file mode 100644 index 4886e34df3b..00000000000 --- a/packages/client/src/v2-events/features/events/actions/declare/useReviewActionConfig.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { v4 as uuid } from 'uuid' -import { - EventState, - DeclarationFormConfig, - FieldConfig, - ActionType, - ValidatorContext -} from '@opencrvs/commons/client' -import { useEvents } from '@client/v2-events/features/events/useEvents/useEvents' -import { validationErrorsInActionFormExist } from '@client/v2-events/components/forms/validation' -import { reviewMessages } from '@client/v2-events/features/events/actions/messages' -import { useUserAllowedActions } from '@client/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations' - -export function useReviewActionConfig({ - formConfig, - declaration, - annotation, - reviewFields, - eventType, - validatorContext -}: { - formConfig: DeclarationFormConfig - declaration: EventState - annotation?: EventState - reviewFields: FieldConfig[] - eventType: string - validatorContext: ValidatorContext -}) { - const events = useEvents() - const { isActionAllowed } = useUserAllowedActions(eventType) - const incomplete = validationErrorsInActionFormExist({ - formConfig, - form: declaration, - annotation, - context: validatorContext, - reviewFields - }) - - const userMayDeclare = isActionAllowed(ActionType.DECLARE) - const userMayNotify = isActionAllowed(ActionType.NOTIFY) - const userMayValidate = isActionAllowed(ActionType.VALIDATE) - const userMayRegister = isActionAllowed(ActionType.REGISTER) - - if (incomplete && userMayNotify && userMayDeclare) { - return { - buttonType: 'primary', - incomplete, - onConfirm: (eventId: string) => { - events.actions.notify.mutate({ - eventId, - declaration, - annotation, - transactionId: uuid() - }) - }, - messages: reviewMessages.incomplete.declare, - icon: 'UploadSimple' - } as const - } - - if (userMayRegister) { - return { - buttonType: 'positive' as const, - incomplete, - onConfirm: (eventId: string) => - events.customActions.registerOnDeclare.mutate({ - eventId, - declaration, - transactionId: uuid(), - annotation - }), - messages: incomplete - ? reviewMessages.incomplete.register - : reviewMessages.complete.register, - icon: 'Check' - } as const - } - - if (userMayValidate) { - return { - buttonType: 'positive', - incomplete, - onConfirm: (eventId: string) => - events.customActions.validateOnDeclare.mutate({ - eventId, - declaration, - transactionId: uuid(), - annotation - }), - messages: incomplete - ? reviewMessages.incomplete.validate - : reviewMessages.complete.validate, - icon: 'PaperPlaneTilt' - } as const - } - - if (userMayDeclare) { - return { - buttonType: 'positive', - incomplete, - onConfirm: (eventId: string) => - events.actions.declare.mutate({ - eventId, - declaration, - annotation, - transactionId: uuid() - }), - messages: incomplete - ? reviewMessages.incomplete.declare - : reviewMessages.complete.declare, - icon: 'UploadSimple' - } as const - } - - throw new Error('No valid scope found for the action') -} diff --git a/packages/client/src/v2-events/features/events/actions/quick-actions/useQuickActionModal.tsx b/packages/client/src/v2-events/features/events/actions/quick-actions/useQuickActionModal.tsx index ada228cf607..ed8051893bd 100644 --- a/packages/client/src/v2-events/features/events/actions/quick-actions/useQuickActionModal.tsx +++ b/packages/client/src/v2-events/features/events/actions/quick-actions/useQuickActionModal.tsx @@ -159,14 +159,14 @@ export function useQuickActionModal(event: EventIndex) { ) => { const config = quickActions[actionType] const label = actionLabels[actionType] - const confirmed = await openModal((close) => ( + const { result } = await openModal((close) => ( )) // On confirmed modal, we will: // - Execute the configured onConfirm() for the action // - Redirect the user to the workqueue they arrived from if provided, or the home page if not - if (confirmed) { + if (result) { void config.onConfirm({ event, actions, diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations.tsx index 5b1391d8b14..10903b3ccb7 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/components/useAllowedActionConfigurations.tsx @@ -106,6 +106,11 @@ function getAvailableAssignmentActions( } export const actionLabels = { + [ActionType.NOTIFY]: { + defaultMessage: 'Notify', + description: 'Notify action label', + id: 'actions.notify' + }, [ActionType.ASSIGN]: { defaultMessage: 'Assign', description: `Label for the ${ActionType.ASSIGN} action in the action menu`, diff --git a/packages/client/src/v2-events/features/workqueues/Outbox.interaction.stories.tsx b/packages/client/src/v2-events/features/workqueues/Outbox.interaction.stories.tsx index c3d29a03e4f..7d1ecc5965b 100644 --- a/packages/client/src/v2-events/features/workqueues/Outbox.interaction.stories.tsx +++ b/packages/client/src/v2-events/features/workqueues/Outbox.interaction.stories.tsx @@ -98,11 +98,17 @@ export const SuccessfulMutation: Story = { }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement) - await step('Send for review', async () => { - const submitButton = await canvas.findByText('Send for review') - await userEvent.click(submitButton) - const confirmButton = await canvas.findByText('Confirm') - await userEvent.click(confirmButton) + await step('Declare', async () => { + await userEvent.click( + await canvas.findByRole('button', { name: 'Action' }) + ) + await userEvent.click(await canvas.findByText('Declare')) + await userEvent.click( + await canvas.findByRole('button', { + name: 'Declare' + }) + ) + const outboxButton = await canvas.findByTestId( 'navigation_workqueue_outbox' ) @@ -183,11 +189,17 @@ export const FailedMutation: Story = { }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement) - await step('Send for review', async () => { - const submitButton = await canvas.findByText('Send for review') - await userEvent.click(submitButton) - const confirmButton = await canvas.findByText('Confirm') - await userEvent.click(confirmButton) + await step('Declare', async () => { + await userEvent.click( + await canvas.findByRole('button', { name: 'Action' }) + ) + await userEvent.click(await canvas.findByText('Declare')) + await userEvent.click( + await canvas.findByRole('button', { + name: 'Declare' + }) + ) + const outboxButton = await canvas.findByTestId( 'navigation_workqueue_outbox' ) @@ -263,11 +275,16 @@ export const FailedMutationConflict: Story = { }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement) - await step('Send for review', async () => { - const submitButton = await canvas.findByText('Send for review') - await userEvent.click(submitButton) - const confirmButton = await canvas.findByText('Confirm') - await userEvent.click(confirmButton) + await step('Declare', async () => { + await userEvent.click( + await canvas.findByRole('button', { name: 'Action' }) + ) + await userEvent.click(await canvas.findByText('Declare')) + await userEvent.click( + await canvas.findByRole('button', { + name: 'Declare' + }) + ) const outboxButton = await canvas.findByTestId( 'navigation_workqueue_outbox' ) diff --git a/packages/client/src/v2-events/layouts/form/FormHeader.tsx b/packages/client/src/v2-events/layouts/form/FormHeader.tsx index e511c5ac8f4..999e33da792 100644 --- a/packages/client/src/v2-events/layouts/form/FormHeader.tsx +++ b/packages/client/src/v2-events/layouts/form/FormHeader.tsx @@ -22,7 +22,7 @@ import { useEventFormNavigation } from '@client/v2-events//features/events/useEv import { useEventConfiguration } from '@client/v2-events/features/events/useEventConfiguration' import { AllowedRouteWithEventId } from './utils' -const messages = defineMessages({ +export const messages = defineMessages({ saveExitButton: { id: 'buttons.saveExit', defaultMessage: 'Save & Exit', @@ -32,6 +32,11 @@ const messages = defineMessages({ id: 'buttons.exit', defaultMessage: 'Exit', description: 'The label for the exit button' + }, + deleteDeclaration: { + id: 'buttons.deleteDeclaration', + defaultMessage: 'Delete declaration', + description: 'The label for the delete declaration button' } }) @@ -39,12 +44,14 @@ export function FormHeader({ label, onSaveAndExit, route, - appbarIcon + appbarIcon, + actionComponent }: { label: string onSaveAndExit?: () => void route: AllowedRouteWithEventId appbarIcon?: React.ReactNode + actionComponent?: React.ReactNode }) { const intl = useIntl() const { modal, exit, closeActionView, deleteDeclaration } = @@ -80,37 +87,96 @@ export function FormHeader({ ] : [] - return ( - { + if (onSaveAndExit) { + return ( <> - {onSaveAndExit ? ( - <> - + - - {menuItems.length > 0 && ( + + {menuItems.length > 0 && ( + + } + /> + )} + + ) + } + + return ( + <> + {actionComponent} + + + ) + } + + return ( + <> + + {onSaveAndExit ? ( + <> + + , + handler: onDelete + } + ]} toggleButton={ } /> - )} - - ) : ( - - )} - {modal} - - } - desktopTitle={label} - mobileLeft={appbarIcon} - mobileRight={ - <> - {onSaveAndExit ? ( - <> + + ) : ( - - , - handler: onDelete - } - ]} - toggleButton={ - - } - /> - - ) : ( - - )} - {modal} - - } - mobileTitle={label} - /> + )} + + } + mobileTitle={label} + /> + {modal} + ) } diff --git a/packages/client/src/v2-events/layouts/form/index.tsx b/packages/client/src/v2-events/layouts/form/index.tsx index dd0a4dac997..44ced007514 100644 --- a/packages/client/src/v2-events/layouts/form/index.tsx +++ b/packages/client/src/v2-events/layouts/form/index.tsx @@ -27,12 +27,14 @@ export function FormLayout({ route, children, onSaveAndExit, - appbarIcon = + appbarIcon = , + actionComponent }: { route: AllowedRouteWithEventId children: React.ReactNode onSaveAndExit?: () => void | Promise appbarIcon?: React.ReactNode + actionComponent?: React.ReactNode }) { const intl = useIntl() const { eventId } = useTypedParams(route) @@ -46,6 +48,7 @@ export function FormLayout({