From b56778326e38d26794e3165e667efe44b8ab21c4 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Tue, 13 Aug 2024 00:17:48 +0200 Subject: [PATCH 1/4] feat: add overflow menu with actions --- i18n/en.pot | 26 +++- .../Buttons/OverflowButton.component.js | 2 + .../EnrollmentPageDefault.container.js | 20 +++ .../LayoutComponentConfig.js | 6 + .../enrollment.actions.js | 11 ++ .../useCommonEnrollmentDomainData.js | 44 +++--- .../EventRow/DeleteAction/DeleteAction.js | 133 ++++++++++++++++++ .../EventRow/DeleteAction/index.js | 3 + .../Stage/StageDetail/EventRow/EventRow.js | 92 ++++++++++++ .../StageDetail/EventRow/EventRow.types.js | 16 +++ .../EventRow/SkipAction/SkipAction.js | 89 ++++++++++++ .../StageDetail/EventRow/SkipAction/index.js | 3 + .../Stage/StageDetail/EventRow/index.js | 3 + .../StageDetail/StageDetail.component.js | 44 ++++-- .../Stage/StageDetail/stageDetail.types.js | 6 +- .../Stages/Stage/stage.types.js | 3 + .../Stages/stages.types.js | 6 + .../stagesAndEvents.types.js | 3 + .../enrollmentDomain.reducerDescription.js | 15 ++ 19 files changed, 484 insertions(+), 41 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/DeleteAction.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/index.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/index.js diff --git a/i18n/en.pot b/i18n/en.pot index 53ce989a6f..6f1cac6bb9 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-08-08T11:49:13.423Z\n" -"PO-Revision-Date: 2024-08-08T11:49:13.423Z\n" +"POT-Creation-Date: 2024-08-12T22:17:50.440Z\n" +"PO-Revision-Date: 2024-08-12T22:17:50.440Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -35,6 +35,9 @@ msgstr "" "(in the same domain). Please refresh this page if you would like to use " "this version again, but be aware that this will close other versions." +msgid "More" +msgstr "More" + msgid "View {{programName}} dashboard" msgstr "View {{programName}} dashboard" @@ -1502,6 +1505,25 @@ msgstr "Please select a valid event" msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" +msgid "An error occurred while deleting the event" +msgstr "An error occurred while deleting the event" + +msgid "" +"Deleting an event is permanent and cannot be undone. Are you sure you want " +"to delete this event?" +msgstr "" +"Deleting an event is permanent and cannot be undone. Are you sure you want " +"to delete this event?" + +msgid "An error occurred when updating event status" +msgstr "An error occurred when updating event status" + +msgid "Unskip" +msgstr "Unskip" + +msgid "Skip" +msgstr "Skip" + msgid "To open this event, please wait until saving is complete" msgstr "To open this event, please wait until saving is complete" diff --git a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js index 476e950611..2edfd554f6 100644 --- a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js +++ b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; import { useRef, useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; import { Button, Layer, Popper } from '@dhis2/ui'; type Props = { @@ -42,6 +43,7 @@ export const OverflowButton = ({ return (
+ + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js new file mode 100644 index 0000000000..83db3ccf94 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js @@ -0,0 +1,3 @@ +// @flow + +export { DeleteAction } from './DeleteAction'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js new file mode 100644 index 0000000000..c62d24491c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js @@ -0,0 +1,92 @@ +// @flow +import React, { useState } from 'react'; +import { withStyles } from '@material-ui/core/'; +import { + DataTableCell, + DataTableRow, + FlyoutMenu, + IconMore16, +} from '@dhis2/ui'; +import { OverflowButton } from '../../../../../Buttons'; +import type { EventRowProps } from './EventRow.types'; +import { DeleteAction } from './DeleteAction'; +import { SkipAction } from './SkipAction'; + +const styles = { + row: { + maxWidth: '100%', + whiteSpace: 'nowrap', + cursor: 'pointer', + }, + rowDisabled: { + cursor: 'not-allowed', + opacity: 0.5, + }, +}; + +export const EventStatuses = { + ACTIVE: 'ACTIVE', + COMPLETED: 'COMPLETED', + SKIPPED: 'SKIPPED', + SCHEDULE: 'SCHEDULE', +}; + +const EventRowPlain = ({ + id, + pendingApiResponse, + eventDetails, + cells, + onDeleteEvent, + onRollbackDeleteEvent, + onUpdateEventStatus, + teiId, + programId, + enrollmentId, + classes, +}: EventRowProps) => { + const [actionsOpen, setActionsOpen] = useState(false); + + return ( + + {cells} + + + setActionsOpen(prev => !prev)} + secondary + small + icon={} + component={( + + {(eventDetails.status === EventStatuses.SCHEDULE || eventDetails.status === EventStatuses.SKIPPED) && ( + + )} + + + + )} + /> + + + ); +}; + +export const EventRow = withStyles(styles)(EventRowPlain); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js new file mode 100644 index 0000000000..0de3df8537 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js @@ -0,0 +1,16 @@ +// @flow + +export type EventRowProps = {| + id: string, + pendingApiResponse: boolean, + eventDetails: ApiEnrollmentEvent, + cells: Array>, + onEventClick: (id: string, options: ?Object) => void, + onDeleteEvent: (id: string) => void, + onUpdateEventStatus: (id: string, status: string) => void, + onRollbackDeleteEvent: (event: ApiEnrollmentEvent) => void, + teiId: string, + programId: string, + enrollmentId: string, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js new file mode 100644 index 0000000000..43fc1a6501 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js @@ -0,0 +1,89 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import log from 'loglevel'; +import { + MenuItem, + IconArrowRight16, IconRedo16, +} from '@dhis2/ui'; +import { useMutation } from 'react-query'; +import { useAlert, useDataEngine } from '@dhis2/app-runtime'; +import { EventStatuses } from '../EventRow'; +import { errorCreator } from '../../../../../../../../capture-core-utils'; + +type Props = {| + eventId: string, + eventDetails: ApiEnrollmentEvent, + pendingApiResponse: boolean, + onUpdateEventStatus: (eventId: string, status: string) => void, + setActionsOpen: (open: boolean) => void, +|} + +export const SkipAction = ({ + eventId, + eventDetails, + pendingApiResponse, + setActionsOpen, + onUpdateEventStatus, +}: Props) => { + const dataEngine = useDataEngine(); + const { show: showError } = useAlert( + ({ message }) => message, + { critical: true }, + ); + const { mutate: updateEventStatus } = useMutation( + ({ status }) => dataEngine.mutate({ + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: { + events: [ + { + ...eventDetails, + event: eventId, + status, + }, + ], + }, + }), + { + onMutate: (payload) => { + const status = EventStatuses[payload.status]; + const previousStatus = eventDetails.status; + + status && onUpdateEventStatus(eventId, status); + + return { previousStatus }; + }, + onError: (error, payload, context) => { + showError({ message: i18n.t('An error occurred when updating event status') }); + log.error(errorCreator('An error occurred when updating event status')({ error, payload, context })); + context && onUpdateEventStatus(eventId, context.previousStatus); + }, + }, + ); + + const handleMenuItemClick = (status) => { + setActionsOpen(false); + !pendingApiResponse && updateEventStatus({ status }); + }; + + if (eventDetails.status === 'SKIPPED') { + return ( + } + label={i18n.t('Unskip')} + onClick={() => handleMenuItemClick(EventStatuses.SCHEDULE)} + /> + ); + } + + return ( + } + label={i18n.t('Skip')} + onClick={() => handleMenuItemClick(EventStatuses.SKIPPED)} + /> + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/index.js new file mode 100644 index 0000000000..860c3a51f5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/index.js @@ -0,0 +1,3 @@ +// @flow + +export { SkipAction } from './SkipAction'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/index.js new file mode 100644 index 0000000000..acea1793f8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/index.js @@ -0,0 +1,3 @@ +// @flow + +export { EventRow } from './EventRow'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js index 055befd20d..f6dba91fec 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js @@ -3,7 +3,8 @@ import React, { type ComponentType, useState, useCallback } from 'react'; import { withStyles } from '@material-ui/core'; import i18n from '@dhis2/d2-i18n'; // $FlowFixMe -import { colors, +import { + colors, spacersNum, DataTableBody, DataTableHead, @@ -16,12 +17,15 @@ import { colors, IconAdd16, Tooltip, } from '@dhis2/ui'; +import log from 'loglevel'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { sortDataFromEvent } from './hooks/sortFuntions'; import { useComputeDataFromEvent, useComputeHeaderColumn, formatRowForView } from './hooks/useEventList'; import { DEFAULT_NUMBER_OF_ROW, SORT_DIRECTION } from './hooks/constants'; import { getProgramAndStageForProgram } from '../../../../../metaData/helpers'; import type { Props } from './stageDetail.types'; +import { EventRow } from './EventRow'; +import { errorCreator } from '../../../../../../capture-core-utils'; const styles = { @@ -30,10 +34,6 @@ const styles = { whiteSpace: 'nowrap', cursor: 'pointer', }, - rowDisabled: { - cursor: 'not-allowed', - opacity: 0.5, - }, container: { display: 'flex', marginRight: spacersNum.dp16, @@ -68,10 +68,14 @@ const StageDetailPlain = (props: Props) => { repeatable = false, enableUserAssignment = false, onEventClick, + onDeleteEvent, + onUpdateEventStatus, + onRollbackDeleteEvent, onViewAll, onCreateNew, hiddenProgramStage, - classes } = props; + classes, + } = props; const defaultSortState = { columnName: 'status', sortDirection: SORT_DIRECTION.DESC, @@ -126,6 +130,8 @@ const StageDetailPlain = (props: Props) => { className={classes.row} > {headerCells} + + ); } @@ -173,15 +179,27 @@ const StageDetailPlain = (props: Props) => { )} )); + const eventDetails = events.find(event => event.event === row.id); + if (!eventDetails) { + log.error(errorCreator('Event details not found')({ row })); + return null; + } return ( - - {cells} - + ); }); } @@ -252,7 +270,7 @@ const StageDetailPlain = (props: Props) => { return ( - + {renderShowMoreButton()} {renderViewAllButton()} {renderCreateNewButton()} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js index 31d8c79dea..d5a07514d3 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js @@ -1,7 +1,7 @@ // @flow import type { StageDataElement, StageCommonProps } from '../../../types/common.types'; - type ExtractedProps = {| +type ExtractedProps = {| events: Array, dataElements: Array, eventName: string, @@ -9,6 +9,10 @@ import type { StageDataElement, StageCommonProps } from '../../../types/common.t repeatable?: boolean, enableUserAssignment?: boolean, stageId: string, + onCreateNew: (stageId: string) => void, + onDeleteEvent: (eventId: string) => void, + onUpdateEventStatus: (eventId: string, status: string) => void, + onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void, hiddenProgramStage?: boolean, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js index d8ce73c7e6..10be4ac00d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js @@ -7,6 +7,9 @@ type ExtractedProps = {| events: Array, className?: string, onEventClick: (eventId: string) => void, + onDeleteEvent: (eventId: string) => void, + onUpdateEventStatus: (eventId: string, status: string) => void, + onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.js index 82208aa356..c4ca0b8fd6 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.js @@ -5,6 +5,9 @@ export type PlainProps = {| stages: Array, events: Array, onEventClick: (eventId: string) => void, + onDeleteEvent: (eventId: string) => void, + onUpdateEventStatus: (eventId: string, status: string) => void, + onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void, ...StageCommonProps, ...CssClasses, |}; @@ -13,5 +16,8 @@ export type InputProps = {| stages?: Array, events?: ?Array, onEventClick: (eventId: string) => void, + onDeleteEvent: (eventId: string) => void, + onUpdateEventStatus: (eventId: string, status: string) => void, + onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void, ...StageCommonProps, |}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js index c7e2453f24..5dce9859fb 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js @@ -6,6 +6,9 @@ type ExtractedProps = {| stages?: Array, events: ?Array, onEventClick: (eventId: string) => void, + onDeleteEvent: (eventId: string) => void, + onUpdateEventStatus: (eventId: string, status: string) => void, + onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void, className?: string, |}; diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js index 2eecc8429a..2a9e26ee06 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -23,6 +23,8 @@ const { COMMIT_ENROLLMENT_EVENTS, ADD_PERSISTED_ENROLLMENT_EVENTS, COMMIT_ENROLLMENT_AND_EVENTS, + DELETE_ENROLLMENT_EVENT, + UPDATE_ENROLLMENT_EVENT_STATUS, } = enrollmentSiteActionTypes; const setAssignee = (state, action) => { @@ -98,6 +100,19 @@ export const enrollmentDomainDesc = createReducerDescription( return { ...state, enrollment: { ...state.enrollment, events } }; }, + [DELETE_ENROLLMENT_EVENT]: (state, { payload: { eventId } }) => { + const events = state.enrollment.events?.filter(event => event.event !== eventId); + return { ...state, enrollment: { ...state.enrollment, events } }; + }, + [UPDATE_ENROLLMENT_EVENT_STATUS]: (state, { payload: { eventId, status } }) => { + const events = state.enrollment.events?.map(event => + (event.event === eventId + ? { ...event, status } + : event), + ); + + return { ...state, enrollment: { ...state.enrollment, events } }; + }, [UPDATE_OR_ADD_ENROLLMENT_EVENTS]: ( state, { payload: { events } }, From 836e50134a97f0c00402ab3d57f06a94b1022284 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Tue, 13 Aug 2024 21:21:07 +0200 Subject: [PATCH 2/4] fix: review comments --- .../Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js index 43fc1a6501..70cd1068b4 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.js @@ -67,7 +67,7 @@ export const SkipAction = ({ !pendingApiResponse && updateEventStatus({ status }); }; - if (eventDetails.status === 'SKIPPED') { + if (eventDetails.status === EventStatuses.SKIPPED) { return ( Date: Fri, 23 Aug 2024 10:46:39 +0200 Subject: [PATCH 3/4] fix: pr-comments --- .../Buttons/OverflowButton.component.js | 3 + .../EnrollmentPageDefault.container.js | 11 ++- .../enrollment.actions.js | 3 +- .../EventRow/DeleteAction/index.js | 3 - .../DeleteActionButton/DeleteActionButton.js | 30 +++++++ .../EventRow/DeleteActionButton/index.js | 3 + .../DeleteActionModal.js} | 90 ++++++++----------- .../EventRow/DeleteActionModal/index.js | 3 + .../Stage/StageDetail/EventRow/EventRow.js | 69 ++++++++------ .../StageDetail/EventRow/EventRow.types.js | 1 + .../StageDetail/StageDetail.component.js | 1 + .../enrollmentDomain.reducerDescription.js | 4 +- 12 files changed, 131 insertions(+), 90 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.js create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/index.js rename src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/{DeleteAction/DeleteAction.js => DeleteActionModal/DeleteActionModal.js} (57%) create mode 100644 src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/index.js diff --git a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js index 2edfd554f6..57e99a125e 100644 --- a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js +++ b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js @@ -15,6 +15,7 @@ type Props = { dataTest?: string, small?: boolean, large?: boolean, + disabled?: boolean, }; export const OverflowButton = ({ @@ -23,6 +24,7 @@ export const OverflowButton = ({ secondary, small, large, + disabled, onClick: handleClick, open: propsOpen, icon, @@ -47,6 +49,7 @@ export const OverflowButton = ({ primary={primary} secondary={secondary} dataTest={dataTest} + disabled={disabled} small={small} large={large} onClick={toggle} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index cdbd793009..1259861683 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -1,11 +1,13 @@ // @flow import React, { useCallback } from 'react'; import i18n from '@dhis2/d2-i18n'; +import moment from 'moment'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; // $FlowFixMe import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { useCommonEnrollmentDomainData, useRuleEffects, @@ -51,6 +53,7 @@ import { export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); + const { fromClientDate } = useTimeZoneConversion(); const { status: widgetEnrollmentStatus } = useSelector(({ widgetEnrollment }) => widgetEnrollment); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); @@ -147,8 +150,12 @@ export const EnrollmentPageDefault = () => { }, [dispatch]); const onUpdateEventStatus = useCallback((eventId: string, status: string) => { - dispatch(updateEnrollmentEventStatus(eventId, status)); - }, [dispatch]); + const nowClient = fromClientDate(new Date()); + const nowServer = new Date(nowClient.getServerZonedISOString()); + const updatedAt = moment(nowServer).format('YYYY-MM-DDTHH:mm:ss'); + + dispatch(updateEnrollmentEventStatus(eventId, status, updatedAt)); + }, [dispatch, fromClientDate]); const onAddNew = () => { history.push(`/new?${buildUrlQueryString({ orgUnitId, programId, teiId })}`); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js index 179b5ebc3e..1d5bfbc2bd 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js @@ -60,10 +60,11 @@ export const updateOrAddEnrollmentEvents = ({ events }: EventReducerProps) => export const deleteEnrollmentEvent = (eventId: string) => actionCreator(enrollmentSiteActionTypes.DELETE_ENROLLMENT_EVENT)({ eventId }); -export const updateEnrollmentEventStatus = (eventId: string, status: string) => +export const updateEnrollmentEventStatus = (eventId: string, status: string, updatedAt: string) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENT_STATUS)({ eventId, status, + updatedAt, }); export const rollbackEnrollmentEvents = ({ events }: EventReducerProps) => diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js deleted file mode 100644 index 83db3ccf94..0000000000 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow - -export { DeleteAction } from './DeleteAction'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.js new file mode 100644 index 0000000000..bef374d0c5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.js @@ -0,0 +1,30 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + colors, + IconDelete16, + MenuItem, +} from '@dhis2/ui'; + +type Props = { + setActionsOpen: (open: boolean) => void, + setDeleteModalOpen: (open: boolean) => void, +}; + +export const DeleteActionButton = ({ + setActionsOpen, + setDeleteModalOpen, +}: Props) => ( + <> + } + label={i18n.t('Delete')} + onClick={() => { + setDeleteModalOpen(true); + setActionsOpen(false); + }} + /> + +); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/index.js new file mode 100644 index 0000000000..0fa62aa1e1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/index.js @@ -0,0 +1,3 @@ +// @flow + +export { DeleteActionButton } from './DeleteActionButton'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/DeleteAction.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js similarity index 57% rename from src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/DeleteAction.js rename to src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js index 68a528928d..c4cbdbc257 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteAction/DeleteAction.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js @@ -1,18 +1,8 @@ // @flow -import React, { useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; +import React from 'react'; import log from 'loglevel'; -import { - Button, - ButtonStrip, - colors, - IconDelete16, - MenuItem, - Modal, - ModalActions, - ModalContent, - ModalTitle, -} from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } from '@dhis2/ui'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from 'react-query'; import { ReactQueryAppNamespace } from '../../../../../../../utils/reactQueryHelpers'; @@ -26,18 +16,19 @@ type Props = { enrollmentId: string, onDeleteEvent: (eventId: string) => void, onRollbackDeleteEvent: (eventToRollbackOnFail: ApiEnrollmentEvent) => void, + setDeleteModalOpen: (open: boolean) => void, } -export const DeleteAction = ({ - eventId, +export const DeleteActionModal = ({ + setDeleteModalOpen, pendingApiResponse, + eventId, teiId, programId, enrollmentId, onDeleteEvent, onRollbackDeleteEvent, }: Props) => { - const [deleteModalOpen, setDeleteModalOpen] = useState(false); const { show: showError } = useAlert( ({ message }) => message, { @@ -73,8 +64,7 @@ export const DeleteAction = ({ .getQueryData(enrollmentDomainQueryKey); const eventToRollbackOnFail = previousData ?.enrollments - ?.[0] - ?.events + ?.flatMap(enrollment => enrollment.events) ?.find(event => event.event === eventId); onDeleteEvent(eventId); @@ -92,42 +82,32 @@ export const DeleteAction = ({ ); return ( - <> - } - label={i18n.t('Delete')} - onClick={() => setDeleteModalOpen(true)} - /> - - setDeleteModalOpen(false)} - > - - {i18n.t('Delete event')} - - -

- {i18n.t('Deleting an event is permanent and cannot be undone. Are you sure you want to delete this event?')} -

-
- - - - - - -
- + setDeleteModalOpen(false)} + > + + {i18n.t('Delete event')} + + +

+ {i18n.t('Deleting an event is permanent and cannot be undone. Are you sure you want to delete this event?')} +

+
+ + + + + + +
); }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/index.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/index.js new file mode 100644 index 0000000000..f288fff5b2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/index.js @@ -0,0 +1,3 @@ +// @flow + +export { DeleteActionModal } from './DeleteActionModal'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js index c62d24491c..a1286962b4 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js @@ -9,8 +9,9 @@ import { } from '@dhis2/ui'; import { OverflowButton } from '../../../../../Buttons'; import type { EventRowProps } from './EventRow.types'; -import { DeleteAction } from './DeleteAction'; +import { DeleteActionButton } from './DeleteActionButton'; import { SkipAction } from './SkipAction'; +import { DeleteActionModal } from './DeleteActionModal'; const styles = { row: { @@ -36,6 +37,7 @@ const EventRowPlain = ({ pendingApiResponse, eventDetails, cells, + stageWriteAccess, onDeleteEvent, onRollbackDeleteEvent, onUpdateEventStatus, @@ -45,6 +47,7 @@ const EventRowPlain = ({ classes, }: EventRowProps) => { const [actionsOpen, setActionsOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); return ( - setActionsOpen(prev => !prev)} - secondary - small - icon={} - component={( - - {(eventDetails.status === EventStatuses.SCHEDULE || eventDetails.status === EventStatuses.SKIPPED) && ( - + setActionsOpen(prev => !prev)} + secondary + small + icon={} + disabled={pendingApiResponse || !stageWriteAccess} + component={( + + {(eventDetails.status === EventStatuses.SCHEDULE || eventDetails.status === EventStatuses.SKIPPED) && ( + + )} + + - )} + + )} + /> - - + {deleteModalOpen && ( + )} - /> +
); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js index 0de3df8537..d47cc386fc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.types.js @@ -9,6 +9,7 @@ export type EventRowProps = {| onDeleteEvent: (id: string) => void, onUpdateEventStatus: (id: string, status: string) => void, onRollbackDeleteEvent: (event: ApiEnrollmentEvent) => void, + stageWriteAccess: boolean, teiId: string, programId: string, enrollmentId: string, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js index 430a27a9bf..b9aa9001d3 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js @@ -191,6 +191,7 @@ const StageDetailPlain = (props: Props) => { pendingApiResponse={row.pendingApiResponse} eventDetails={eventDetails} teiId={eventDetails.trackedEntity} + stageWriteAccess={stage?.access?.data?.write} programId={programId} enrollmentId={eventDetails.enrollment} cells={cells} diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js index 2a9e26ee06..f7ca795289 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -104,10 +104,10 @@ export const enrollmentDomainDesc = createReducerDescription( const events = state.enrollment.events?.filter(event => event.event !== eventId); return { ...state, enrollment: { ...state.enrollment, events } }; }, - [UPDATE_ENROLLMENT_EVENT_STATUS]: (state, { payload: { eventId, status } }) => { + [UPDATE_ENROLLMENT_EVENT_STATUS]: (state, { payload: { eventId, status, updatedAt } }) => { const events = state.enrollment.events?.map(event => (event.event === eventId - ? { ...event, status } + ? { ...event, status, updatedAt } : event), ); From ae4ea7033dd4653ce1bff4be44ba607f4f8a0c60 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Mon, 2 Sep 2024 12:26:54 +0200 Subject: [PATCH 4/4] chore: cypress tests --- .../StagesAndEventsWidget.feature | 29 ++++-- .../StagesAndEventsWidget.js | 88 ++++++++++++++++++- .../DeleteActionModal/DeleteActionModal.js | 1 + .../Stage/StageDetail/EventRow/EventRow.js | 6 +- 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature index 07387fcde5..e944741b2f 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature @@ -1,9 +1,5 @@ Feature: User interacts with Stages and Events Widget - @user:trackerAutoTestRestricted - Scenario: Create new event button is disabled if no data write access - Given you open the enrollment page by typing #enrollment?enrollmentId=WKPoiZxZxNG&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=PgmUFEQYZdt - Then you should see the disabled button New Previous deliveries event Scenario: User can view program stages Given you open the enrollment page @@ -57,13 +53,36 @@ Feature: User interacts with Stages and Events Widget When you click New First antenatal care visit event Then you should navigate to Add new page #/enrollmentEventNew?enrollmentId=ek4WWAgXX5i&orgUnitId=DwpbWkiqjMy&programId=WSGAb5XwJ3Y&stageId=WZbXY0S00lP&teiId=yFcOhsM1Yoa - Scenario: User can not go to Add new page if stage is not repeatable and there is event in the stage Given you open the enrollment page by typing #enrollment?programId=IpHINAT79UW&orgUnitId=UgYg0YW7ZIh&teiId=fhFQhO0xILJ&enrollmentId=gPDueU02tn8 Then you should see the disabled button New Birth event + Scenario: User can skip a scheduled event + Given you open the enrollment page by typing #/enrollment?enrollmentId=gL8BooqKhdX&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=RABlFsj5Omi + And there is an Overdue event in the TB visit stage + When you click the Skip event overflow button on the Overdue event + Then the event should be skipped + + Scenario: User can unskip a scheduled event + Given you open the enrollment page by typing #/enrollment?enrollmentId=gL8BooqKhdX&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=RABlFsj5Omi + And there is an Skipped event in the TB visit stage + When you click the Unskip event overflow button on the Skipped event + Then there is an Overdue event in the TB visit stage + + @with-restore-deleted-event + Scenario: User can delete an event + Given you open the enrollment page by typing #/enrollment?enrollmentId=ikYMpSKXik1&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=Trc1H9T5C6f + And there is an Active event in the TB visit stage + When you click the Delete event overflow button on the Active event + And you confirm you want to delete the event + Then the TB visit stage should be empty + @user:trackerAutoTestRestricted Scenario: Program stage is hidden if no data read access And you open the enrollment page by typing #enrollment?enrollmentId=iNEq9d22Nyp&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=k4ODejBytgv Then the Care at birth program stage should be hidden + @user:trackerAutoTestRestricted + Scenario: Create new event button is disabled if no data write access + Given you open the enrollment page by typing #enrollment?enrollmentId=WKPoiZxZxNG&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=PgmUFEQYZdt + Then you should see the disabled button New Previous deliveries event diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js index d772f41034..d4771a8876 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js @@ -1,7 +1,38 @@ -import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; +import { Given, When, Then, defineStep as And, After } from '@badeball/cypress-cucumber-preprocessor'; import { getCurrentYear } from '../../../support/date'; import '../sharedSteps'; +After({ tags: '@with-restore-deleted-event' }, () => { + cy.visit('#/enrollment?enrollmentId=ikYMpSKXik1&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=Trc1H9T5C6f'); + + cy.get('[data-test="stages-and-events-widget"]') + .find('[data-test="widget-contents"]') + .contains('[data-test="stage-content"]', 'TB visit') + .find('[data-test="create-new-button"]') + .click(); + + cy.get('[data-test="capture-ui-input"]') + .first() + .type('2023-01-26') + .blur(); + + cy.get('[data-test="virtualized-select"]') + .eq(0) + .click() + .contains('P+') + .click(); + + cy.get('[data-test="virtualized-select"]') + .eq(1) + .click() + .contains('New') + .click(); + + cy.get('[data-test="dhis2-uicore-button"]') + .contains('Save without completing') + .click(); +}); + Then('the program stages should be displayed', () => { cy.get('[data-test="stages-and-events-widget"]') .within(() => { @@ -162,3 +193,58 @@ Then('the Care at birth program stage should be hidden', () => { cy.contains('[data-test="stages-and-events-widget"]', 'Postpartum care visit').should('exist'); cy.contains('[data-test="stages-and-events-widget"]', 'Care at birth').should('not.exist'); }); + +Given(/there is an (.*) event in the TB visit stage$/, (eventStatus) => { + cy.get('[data-test="stages-and-events-widget"]') + .find('[data-test="widget-contents"]') + .contains('[data-test="stage-content"]', 'TB visit') + .within(() => { + cy.get('[data-test="dhis2-uicore-datatablerow"]') + .contains(eventStatus); + }); +}); + +When(/you click the (.*) event overflow button on the (.*) event$/, (buttonName, eventStatus) => { + cy.get('[data-test="stages-and-events-widget"]') + .find('[data-test="widget-contents"]') + .contains('[data-test="stage-content"]', 'TB visit') + .find('[data-test="dhis2-uicore-tablebody"]') + .contains('tr', eventStatus) + .find('[data-test="overflow-button"]') + .click({ force: true }); + + cy.get('[data-test="overflow-menu"]') + .contains(buttonName) + .click(); +}); + +Then('the event should be skipped', () => { + cy.get('[data-test="stages-and-events-widget"]') + .find('[data-test="widget-contents"]') + .contains('[data-test="stage-content"]', 'TB visit') + .find('[data-test="dhis2-uicore-datatablerow"]') + .contains('Skipped'); +}); + +Then('the TB visit stage should be empty', () => { + cy.get('[data-test="stages-and-events-widget"]') + .find('[data-test="widget-contents"]') + .contains('[data-test="stage-content"]', 'TB visit') + .find('[data-test="dhis2-uicore-datatablerow"]') + .should('not.exist'); +}); + +When('you confirm you want to delete the event', () => { + cy.intercept('POST', '**/tracker?async=false&importStrategy=DELETE') + .as('deleteEvent'); + + cy.get('[data-test="dhis2-uicore-modal"]').within(() => { + cy.contains('button', 'Yes, delete event') + .click(); + }); + + cy.wait('@deleteEvent') + .its('response.statusCode') + .should('eq', 200); +}); + diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js index c4cbdbc257..69d7234476 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.js @@ -84,6 +84,7 @@ export const DeleteActionModal = ({ return ( setDeleteModalOpen(false)} + small > {i18n.t('Delete event')} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js index a1286962b4..1f8ba00e56 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.js @@ -61,12 +61,16 @@ const EventRowPlain = ({ setActionsOpen(prev => !prev)} + dataTest={'overflow-button'} secondary small icon={} disabled={pendingApiResponse || !stageWriteAccess} component={( - + {(eventDetails.status === EventStatuses.SCHEDULE || eventDetails.status === EventStatuses.SKIPPED) && (