Skip to content
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

feat: [DHIS2-17878][DHIS2-17048] Add overflow menu with actions to stages&events #3756

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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);
});

19 changes: 19 additions & 0 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,25 @@ msgstr "This stage can only have one 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"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import i18n from '@dhis2/d2-i18n';
import { useRef, useState } from 'react';
import i18n from '@dhis2/d2-i18n';
import { Button, Layer, Popper } from '@dhis2/ui';

type Props = {
Expand All @@ -15,6 +15,7 @@ type Props = {
dataTest?: string,
small?: boolean,
large?: boolean,
disabled?: boolean,
};

export const OverflowButton = ({
Expand All @@ -23,6 +24,7 @@ export const OverflowButton = ({
secondary,
small,
large,
disabled,
onClick: handleClick,
open: propsOpen,
icon,
Expand All @@ -43,10 +45,11 @@ export const OverflowButton = ({
return (
<div ref={anchorRef}>
<Button
aria-label={label ?? i18n.t('More')}
title={label ?? i18n.t('More')}
primary={primary}
secondary={secondary}
dataTest={dataTest}
disabled={disabled}
small={small}
large={large}
onClick={toggle}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -41,11 +43,17 @@ import { LoadingMaskForPage } from '../../../LoadingMasks';
import {
EnrollmentPageKeys,
} from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants';
import {
addPersistedEnrollmentEvents,
deleteEnrollmentEvent,
updateEnrollmentEventStatus,
} from '../../common/EnrollmentOverviewDomain/enrollment.actions';


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);
Expand Down Expand Up @@ -133,6 +141,22 @@ export const EnrollmentPageDefault = () => {
dispatch(updateIncidentDate(incidentDate));
}, [dispatch]);

const onDeleteEvent = useCallback((eventId: string) => {
dispatch(deleteEnrollmentEvent(eventId));
}, [dispatch]);

const onRollbackDeleteEvent = useCallback((eventDetails: ApiEnrollmentEvent) => {
dispatch(addPersistedEnrollmentEvents({ events: [eventDetails] }));
}, [dispatch]);

const onUpdateEventStatus = useCallback((eventId: string, status: string) => {
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 })}`);
};
Expand Down Expand Up @@ -188,6 +212,9 @@ export const EnrollmentPageDefault = () => {
widgetEffects={outputEffects}
hideWidgets={hideWidgets}
onEventClick={onEventClick}
onDeleteEvent={onDeleteEvent}
onUpdateEventStatus={onUpdateEventStatus}
onRollbackDeleteEvent={onRollbackDeleteEvent}
onLinkedRecordClick={onLinkedRecordClick}
onUpdateTeiAttributeValues={onUpdateTeiAttributeValues}
onUpdateEnrollmentDate={onUpdateEnrollmentDate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const StagesAndEvents: WidgetConfig = {
events,
onViewAll,
onCreateNew,
onDeleteEvent,
onUpdateEventStatus,
onRollbackDeleteEvent,
onEventClick,
ruleEffects,
}): StagesAndEventProps => ({
Expand All @@ -55,6 +58,9 @@ export const StagesAndEvents: WidgetConfig = {
events,
onViewAll,
onCreateNew,
onDeleteEvent,
onUpdateEventStatus,
onRollbackDeleteEvent,
onEventClick,
ruleEffects,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const enrollmentSiteActionTypes = {
UPDATE_ENROLLMENT_ATTRIBUTE_VALUES: 'Enrollment.UpdateEnrollmentAttributeValues',
ROLLBACK_ENROLLMENT_EVENT: 'Enrollment.RollbackEnrollmentEvent',
ROLLBACK_ENROLLMENT_EVENTS: 'Enrollment.RollbackEnrollmentEvents',
DELETE_ENROLLMENT_EVENT: 'Enrollment.DeleteEnrollmentEvent',
UPDATE_ENROLLMENT_EVENT_STATUS: 'Enrollment.UpdateEnrollmentEventStatus',
COMMIT_ENROLLMENT_EVENT: 'Enrollment.CommitEnrollmentEvent',
COMMIT_ENROLLMENT_EVENTS: 'Enrollment.CommitEnrollmentEvents',
SAVE_FAILED: 'Enrollment.SaveFailed',
Expand Down Expand Up @@ -55,6 +57,16 @@ export const commitEnrollmentEvent = (eventId: string) =>
export const updateOrAddEnrollmentEvents = ({ events }: EventReducerProps) =>
actionCreator(enrollmentSiteActionTypes.UPDATE_OR_ADD_ENROLLMENT_EVENTS)({ events });

export const deleteEnrollmentEvent = (eventId: string) =>
actionCreator(enrollmentSiteActionTypes.DELETE_ENROLLMENT_EVENT)({ eventId });

export const updateEnrollmentEventStatus = (eventId: string, status: string, updatedAt: string) =>
actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENT_STATUS)({
eventId,
status,
updatedAt,
});

export const rollbackEnrollmentEvents = ({ events }: EventReducerProps) =>
actionCreator(enrollmentSiteActionTypes.ROLLBACK_ENROLLMENT_EVENTS)({ events });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// @flow
import { useEffect, useMemo } from 'react';
import { useEffect } from 'react';
// $FlowFixMe
import { useSelector, useDispatch } from 'react-redux';
import { useDataQuery } from '@dhis2/app-runtime';
import { setCommonEnrollmentSiteData } from '../enrollment.actions';
import type { Output } from './useCommonEnrollmentDomainData.types';
import { useApiDataQuery } from '../../../../../utils/reactQueryHelpers';

export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: string, programId: string): Output => {
const dispatch = useDispatch();
Expand All @@ -15,28 +15,28 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin
attributeValues: storedAttributeValues,
} = useSelector(({ enrollmentDomain }) => enrollmentDomain);

const { data, error, refetch } = useDataQuery(
useMemo(
() => ({
trackedEntityInstance: {
resource: 'tracker/trackedEntities',
id: ({ variables: { teiId: updatedTeiId } }) => updatedTeiId,
params: ({ variables: { programId: updatedProgramId } }) => ({
program: updatedProgramId,
fields: ['enrollments[*,!attributes],attributes'],
}),
},
}),
[],
),
{ lazy: true },
const { data, error } = useApiDataQuery(
['stages&event', 'enrollmentData', teiId, programId, enrollmentId],
{
resource: 'tracker/trackedEntities',
id: teiId,
params: {
program: programId,
fields: ['enrollments[*,!attributes],attributes'],
},
},
{
enabled: !!teiId && !!programId && !!enrollmentId,
staleTime: 0,
cacheTime: 0,
},
);

const fetchedEnrollmentData = {
reference: data,
enrollment: data?.trackedEntityInstance?.enrollments
enrollment: data?.enrollments
?.find(enrollment => enrollment.enrollment === enrollmentId),
attributeValues: data?.trackedEntityInstance?.attributes,
attributeValues: data?.attributes,
};

useEffect(() => {
Expand All @@ -54,12 +54,6 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin
fetchedEnrollmentData.attributeValues,
]);

useEffect(() => {
if (storedEnrollmentId !== enrollmentId) {
refetch({ variables: { teiId, programId } });
}
}, [refetch, storedEnrollmentId, enrollmentId, teiId, programId]);

const inEffectData = enrollmentId === storedEnrollmentId ? {
enrollment: storedEnrollment,
attributeValues: storedAttributeValues,
Expand Down
Loading
Loading