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-13343] hidden program stage rule effect #3406

Merged
merged 6 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1,7 @@
Feature: Hidden program stage

Scenario: The user cannot add an event in a hidden program stage
Given you add an enrollment event that will result in a rule effect to hide a program stage
Then the New Postpartum care visit event button is disabled in the stages and events widget
And and an error is show in the Postpartum care visit stage
And the Postpartum care visit button is disabled in the enrollmentEventNew page
65 changes: 65 additions & 0 deletions cypress/integration/EnrollmentPage/HiddenProgramStage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import moment from 'moment';

const cleanUp = () => {
cy.visit(
'/#/enrollment?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA',
);

cy.get('[data-test="enrollment-page-content"]').contains('Enrollment Dashboard');

cy.get('[data-test="stages-and-events-widget"]')
.find('[data-test="stage-content"]')
.eq(3)
.click();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we run into trouble if there are no events in the program stage (which is initially the case?). Is it possible to do an early return if the program stage is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I updated to code to handle the case of no events in the program stage. Thanks


cy.contains('WHOMCH Pregnancy outcome').should('exist');
cy.contains('[data-test="dhis2-uicore-button"]', 'Edit event').click();
cy.contains('[data-test="dhis2-uicore-button"]', 'Delete').click();
cy.contains('[data-test="dhis2-uicore-button"]', 'Yes, delete event').click();
};

Given('you add an enrollment event that will result in a rule effect to hide a program stage', () => {
cleanUp();
cy.visit(
'/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&stageId=PFDfvmGpsR3&teiId=uW8Y7AIcRKA',
);

cy.get('[data-test="capture-ui-input"]')
.eq(0)
.type(moment().format('YYYY-MM-DD'))
.blur();

cy
.get('[data-test="virtualized-select"]')
.eq(6)
.click()
.contains('Termination of pregnancy')
.click();

cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing').click();
});

Then('the New Postpartum care visit event button is disabled in the stages and events widget', () => {
cy.contains('[data-test="create-new-button"]', 'New Postpartum care visit event')
.should('be.disabled');
});

Then('and an error is show in the Postpartum care visit stage', () => {
cy.visit(
'/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA&stageId=bbKtnxRZKEP',
);
cy.contains('[data-test="dhis2-uicore-button"]', 'Complete')
.should('be.disabled');
cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing')
.should('be.disabled');
cy.contains('[data-test="dhis2-uicore-noticebox-content"]', 'You can\'t add any more Postpartum care visit events')
.should('exist');
});

Then('the Postpartum care visit button is disabled in the enrollmentEventNew page', () => {
cy.visit(
'/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA',
);

cy.contains('[data-test="program-stage-selector-button"]', 'Postpartum care visit').should('be.disabled');
});
4 changes: 2 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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: 2023-08-22T12:04:52.436Z\n"
"PO-Revision-Date: 2023-08-22T12:04:52.436Z\n"
"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n"
"PO-Revision-Date: 2023-09-04T07:07:59.195Z\n"

msgid "Choose one or more dates..."
msgstr "Choose one or more dates..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
IConvertOutputRulesEffectsValue,
AssignOutputEffect,
HideOutputEffect,
HideProgramStageEffect,
MessageEffect,
GeneralErrorEffect,
GeneralWarningEffect,
Expand Down Expand Up @@ -207,6 +208,17 @@ export function getRulesEffectsProcessor(
};
}

function processHideProgramStage(effect: ProgramRuleEffect): ?HideProgramStageEffect {
if (!effect.programStageId) {
return null;
}

return {
type: effectActions.HIDE_PROGRAM_STAGE,
id: effect.programStageId,
};
}

function processMakeCompulsory(effect: ProgramRuleEffect): Array<CompulsoryEffect> {
return createEffectsForConfiguredDataTypes(effect, () => ({
type: effectActions.MAKE_COMPULSORY,
Expand Down Expand Up @@ -267,6 +279,7 @@ export function getRulesEffectsProcessor(
[effectActions.SHOW_WARNING]: processShowWarning,
[effectActions.SHOW_ERROR_ONCOMPLETE]: processShowErrorOnComplete,
[effectActions.SHOW_WARNING_ONCOMPLETE]: processShowWarningOnComplete,
[effectActions.HIDE_PROGRAM_STAGE]: processHideProgramStage,
[effectActions.HIDE_SECTION]: processHideSection,
[effectActions.MAKE_COMPULSORY]: processMakeCompulsory,
[effectActions.DISPLAY_TEXT]: processDisplayText,
Expand Down
4 changes: 4 additions & 0 deletions packages/rules-engine/src/rulesEngine.types.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export type HideOutputEffect = OutputEffect & {

};

export type HideProgramStageEffect = OutputEffect & {

};

export type MessageEffect = OutputEffect & {
message: string,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const EnrollmentPageDefaultPlain = ({
onEventClick,
onUpdateTeiAttributeValues,
onEnrollmentError,
ruleEffects,
}: PlainProps) => (
<>
<div className={classes.title}>{i18n.t('Enrollment Dashboard')}</div>
Expand All @@ -69,6 +70,7 @@ export const EnrollmentPageDefaultPlain = ({
<EnrollmentQuickActions
stages={stages}
events={events}
ruleEffects={ruleEffects}
/>
<WidgetStagesAndEvents
programId={program.id}
Expand All @@ -77,6 +79,7 @@ export const EnrollmentPageDefaultPlain = ({
onViewAll={onViewAll}
onCreateNew={onCreateNew}
onEventClick={onEventClick}
ruleEffects={ruleEffects}
/>
</div>
<div className={classes.rightColumn}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
useCommonEnrollmentDomainData,
useRuleEffects,
updateEnrollmentAttributeValues,
showEnrollmentError,
} from '../../common/EnrollmentOverviewDomain';
Expand All @@ -17,7 +18,6 @@ import {
useProgramMetadata,
useHideWidgetByRuleLocations,
useProgramStages,
useRuleEffects,
} from './hooks';
import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing';
import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions';
Expand Down Expand Up @@ -108,6 +108,7 @@ export const EnrollmentPageDefault = () => {
onEventClick={onEventClick}
onUpdateTeiAttributeValues={onUpdateTeiAttributeValues}
onEnrollmentError={onEnrollmentError}
ruleEffects={ruleEffects}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import { typeof effectActions } from '@dhis2/rules-engine-javascript';
import type { Program } from 'capture-core/metaData';
import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types';
import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain';
Expand All @@ -20,6 +21,7 @@ export type Props = {|
onEventClick: (eventId: string) => void,
onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void,
onEnrollmentError: (message: string) => void,
ruleEffects?: Array<{id: string, type: $Values<effectActions>}>;
|};

export type PlainProps = {|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const styles = {

};

const EnrollmentQuickActionsComponent = ({ stages, events, classes }) => {
const EnrollmentQuickActionsComponent = ({ stages, events, ruleEffects, classes }) => {
const [open, setOpen] = useState(true);
const history = useHistory();
const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery();
Expand All @@ -33,10 +33,20 @@ const EnrollmentQuickActionsComponent = ({ stages, events, classes }) => {
return mutatedStage;
}), [events, stages]);

const hiddenProgramStageRuleEffects = useMemo(
() => ruleEffects?.filter(ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE'),
[ruleEffects],
);

const noStageAvailable = useMemo(
() => stagesWithEventCount.every(programStage =>
(!programStage.repeatable && programStage.eventCount > 0),
), [stagesWithEventCount]);
() =>
stagesWithEventCount.every(
programStage =>
(!programStage.repeatable && programStage.eventCount > 0) ||
hiddenProgramStageRuleEffects?.find(ruleEffect => ruleEffect.id === programStage.id),
),
[stagesWithEventCount, hiddenProgramStageRuleEffects],
);

const onNavigationFromQuickActions = (tab: string) => {
history.push(`/enrollmentEventNew?${buildUrlQueryString({ programId, teiId, enrollmentId, orgUnitId, tab })}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export { useTeiAttributes } from './useTeiAttributes';
export { useProgramMetadata } from './useProgramMetadata';
export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations';
export { useProgramStages } from './useProgramStages';
export { useRuleEffects } from './useRuleEffects';
export type { UseRuleEffectsInput } from './useRuleEffects.types';

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const styles = {
const ProgramStageSelectorComponentPlain = ({ programStages, onSelectProgramStage, onCancel, classes }) => (
<div className={classes.container}>
{programStages.map((programStage) => {
const disableStage = !programStage.repeatable && programStage.eventCount > 0;
const disableStage =
(!programStage.repeatable && programStage.eventCount > 0) || programStage.hiddenProgramStage;
return (
<div
key={programStage.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,33 @@ import log from 'loglevel';
import { ProgramStageSelectorComponent } from './ProgramStageSelector.component';
import { Widget } from '../../../Widget';
import { errorCreator } from '../../../../../capture-core-utils';
import { useCommonEnrollmentDomainData } from '../../common/EnrollmentOverviewDomain';
import { useCommonEnrollmentDomainData, useRuleEffects } from '../../common/EnrollmentOverviewDomain';
import type { Props } from './ProgramStageSelector.types';
import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/useProgramFromIndexedDB';
import { useLocationQuery, buildUrlQueryString } from '../../../../utils/routing';
import { useRulesEngineOrgUnit } from '../../../../hooks/useRulesEngineOrgUnit';
import { useTrackerProgram } from '../../../../hooks/useTrackerProgram';


export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId }: Props) => {
const history = useHistory();
const { tab } = useLocationQuery();
const { error: enrollmentsError, enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId);
const { error: enrollmentsError, enrollment, attributeValues } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId);
const {
program,
isLoading: programLoading,
isError: programError,
} = useProgramFromIndexedDB(programId);

const { orgUnit } = useRulesEngineOrgUnit(orgUnitId);
const programRules = useTrackerProgram(programId);

const ruleEffects = useRuleEffects({
orgUnit,
program: programRules,
apiEnrollment: enrollment,
apiAttributeValues: attributeValues,
});

useEffect(() => {
if (enrollmentsError || programError) {
Expand All @@ -42,9 +53,12 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId
displayName: currentStage.displayName,
style: currentStage.style,
repeatable: currentStage.repeatable,
hiddenProgramStage: ruleEffects?.find(
ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE' && ruleEffect.id === currentStage.id,
),
});
return accStage;
}, []), [enrollment?.events, program?.programStages, programLoading]);
}, []), [enrollment?.events, program?.programStages, programLoading, ruleEffects]);

const onSelectProgramStage = (newStageId: string) =>
history.push(`enrollmentEventNew?${buildUrlQueryString({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
showEnrollmentError,
} from './enrollment.actions';
export { useCommonEnrollmentDomainData } from './useCommonEnrollmentDomainData';
export { useRuleEffects } from './useRuleEffects';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow
export { useRuleEffects } from './useRuleEffects';
export type * from './useRuleEffects.types';
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,16 @@ export const useRuleEffects = ({ orgUnit, program, apiEnrollment, apiAttributeVa

useEffect(() => {
if (orgUnit && attributeValues && enrollmentData && otherEvents) {
setRuleEffects(getApplicableRuleEffectsForTrackerProgram({
const effects = getApplicableRuleEffectsForTrackerProgram({
program,
orgUnit,
otherEvents,
attributeValues,
enrollmentData,
}, true));
}, true);
if (Array.isArray(effects)) {
setRuleEffects(effects);
}
}
}, [attributeValues, enrollmentData, orgUnit, otherEvents, program]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { TrackerProgram } from 'capture-core/metaData';
import type {
EnrollmentData,
AttributeValue,
} from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData';
} from '../useCommonEnrollmentDomainData';

export type UseRuleEffectsInput = {|
orgUnit?: ?OrgUnit,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @flow
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import { NoticeBox } from '@dhis2/ui';
import type { Props } from './ErrorText.types';

export const ErrorText = ({ stageName }: Props) => (
<>
<br />
<NoticeBox error>
<span>
{i18n.t("You can't add any more {{ programStageName }} events", {
programStageName: stageName,
interpolation: { escapeValue: false },
})}
</span>
</NoticeBox>
<br />
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @flow

export type Props = {|
stageName: string,
|};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { ErrorText } from './ErrorText.component';
Loading