feat: [DHIS2-21655] Uncomplete events from view mode - #4649
Conversation
|
🚀 Deployed on https://deploy-preview-4649.capture.netlify.dhis2.org |
…us in relevant components
…_uncomplete-event-view-mode
…_uncomplete-event-view-mode
|
Thanks for review @simonadomnisoru! I made the changes and also:
|
…_uncomplete-event-view-mode
…tor related logic
…_uncomplete-event-view-mode
…_uncomplete-event-view-mode
…eEventEditPermissions
| const isCompletionToggleable = (status: string, blockedByCompletion: boolean, blockedByExpiry: boolean) => | ||
| !blockedByCompletion | ||
| && !blockedByExpiry | ||
| && (status === eventStatuses.ACTIVE || status === eventStatuses.COMPLETED); |
There was a problem hiding this comment.
🔴 Users without the uncomplete permission can mark completed events incomplete
The permission to reverse an event's completed status is recomputed from only the expiry and blocking checks (!isEventBlockedByCompletion && !isEventBlockedByExpiry at src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx:120) instead of the authority-aware value, so a user lacking the uncomplete permission is still offered the toggle whenever the stage does not block the form.
Impact: A user who does not have permission to uncomplete events can mark completed events as incomplete (and toggle completion) from the events list, the event widget header, and the edit form, bypassing the intended access control.
How the authority gate is dropped
Previously useEventEditPermissions returned canUncompleteEvent equal to the F_UNCOMPLETE_EVENT authority, and the complete checkbox was disabled for completed events unless the user held that authority. After this PR the hook (src/core_modules/capture-core/hooks/useEventEditPermissions.ts:71-75) computes canUncompleteEvent correctly internally (via canUncompletEvent, which checks hasUncompleteAuthority for completed events) but no longer returns it.
Every consumer now reconstructs the value without the authority check:
src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx:120derivescanUncompleteEvent = !isEventBlockedByCompletion && !isEventBlockedByExpiry, passed toWidgetHeader(gating theCompletionMenuItem) and toEditEventDataEntry(gating the complete checkboxdisabledstate).src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx:92derives the same value.src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx:44-47(isCompletionToggleable) gates theCompletionMenuItemthe same way.
Because isEventBlockedByCompletion = isCompletedAndBlockingForm && !canUncompleteEvent and isCompletedAndBlockingForm is only true when stage.blockEntryForm is set, a COMPLETED event on a stage with blockEntryForm unset (the common case) yields isEventBlockedByCompletion === false and isEventBlockedByExpiry === false, so the derived canUncompleteEvent/canToggleCompletion is true regardless of whether the user holds F_UNCOMPLETE_EVENT.
Prompt for agents
The F_UNCOMPLETE_EVENT authority is no longer enforced when offering the mark-complete/mark-incomplete toggle. useEventEditPermissions (src/core_modules/capture-core/hooks/useEventEditPermissions.ts) internally computes a correct authority-aware canUncompleteEvent (canUncompletEvent, which returns hasUncompleteAuthority for completed events and checks write access), but it no longer returns it. All consumers instead recompute canUncompleteEvent / canToggleCompletion as `!isEventBlockedByCompletion && !isEventBlockedByExpiry`, which omits the F_UNCOMPLETE_EVENT authority check for completed events on stages where blockEntryForm is not set (the common case), and also omits the write-access check. Fix by having the hook return its internal canUncompleteEvent (authority- and write-access-aware) and consuming that value in: WidgetEventEdit.container.tsx:120, EventDetailsSection.component.tsx:92, and EventRow.tsx (isCompletionToggleable / canToggleCompletion). Ensure the EditEventDataEntry complete checkbox and the CompletionMenuItem gating both use the authority-aware value so completed events cannot be uncompleted by users lacking F_UNCOMPLETE_EVENT.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const { isEventBlockedByCompletion, isEventBlockedByExpiry } = useEventEditPermissions({ | ||
| programId, | ||
| stage: programStage, | ||
| eventStatus: eventData?.eventContainer?.event?.status, | ||
| occurredAtClient: convertFormToClient(eventData?.dataEntryValues?.occurredAt, dataElementTypes.DATE) as string, | ||
| completedAtClient: eventData?.eventContainer?.event?.completedAt, | ||
| }); |
There was a problem hiding this comment.
🔍 completedAtClient passed as raw server value in some callers
useEventEditPermissions expects completedAtClient in client date format (it feeds isWithinCompleteEventsExpiry, which internally calls convertClientToServer). Several callers convert correctly (e.g. EnrollmentEditEventPage.container.tsx and EventRow.tsx use convertServerToClient(...completedAt...)), but ViewEvent.component.tsx:110 and the new EventDetailsSection.component.tsx:90 pass the raw server completedAt value. WidgetEventEdit.container.tsx:118 does the same. This appears to be a pre-existing inconsistency (the ViewEvent line is unchanged), but the new EventDetailsSection usage propagates it. If the complete-events-expiry window is configured on a program, the expiry calculation for completed events on the view page may be computed from an unconverted date. Worth confirming whether the client/server formats coincide for the DATE type in the active calendar.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const { event: apiEvent } = await dataEngine.query({ | ||
| event: { | ||
| resource: 'tracker/events', | ||
| id: eventId, | ||
| params: { | ||
| fields: 'event,status,program,programStage,orgUnit,occurredAt,scheduledAt,' + | ||
| 'enrollment,trackedEntity,attributeOptionCombo,notes,assignedUser,geometry,followUp', | ||
| }, | ||
| }, | ||
| }) as any; | ||
| return dataEngine.mutate({ | ||
| resource: 'tracker?async=false&importStrategy=UPDATE', | ||
| type: 'create', | ||
| data: { | ||
| events: [{ | ||
| ...apiEvent, | ||
| status: newStatus, | ||
| }], | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🔍 Single-event completion mutation omits dataValues from UPDATE payload
CompletionMenuItem fetches the event with a fixed field list that excludes dataValues and then re-submits it via tracker?importStrategy=UPDATE. This mirrors the established bulk-complete pattern (useBulkCompleteEvents.ts, which was also narrowed in this PR to the same field set), so it is presumably safe with the server's merge behavior. However, if the tracker UPDATE strategy treats a missing dataValues array as an intent to clear them, toggling completion from view mode could drop event data values. Recommend confirming server behavior since this now runs for individual events triggered directly by users, not just the pre-existing bulk flow.
Was this helpful? React with 👍 or 👎 to provide feedback.
|



DHIS2-21655
This PR adds the ability to mark events as complete or incomplete directly from view mode
(without entering edit mode). The changes introduce:
A new
useCanChangeCompletionStatushook that gates the action on write access,event status, and the
F_UNCOMPLETE_EVENTauthority.A reusable
EventCompletionMenuItemcomponent that toggles betweenACTIVE↔COMPLETEDvia the tracker API.Removing duplicate event status lists so the whole app uses one shared source:
capture-core/events/statusTypes.Removed the
To open this event, please wait until saving is completetooltip that could get stuck in the Stages and Events widget event row, and repleaced it with theCircularLoaderfrom@dhis2/ui.