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

fix: [DHIS2-16010] app crashes on invalid programid #3765

Merged
merged 9 commits into from
Oct 22, 2024
2 changes: 0 additions & 2 deletions cypress/e2e/ScopeSelector/ScopeSelector.feature
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ Feature: User uses the ScopeSelector to navigate
When you select both org unit and program Malaria case registration
Then you should see the table

# DHIS2-16010 - App crashes on invalid program id
@skip
Scenario: Main page > Url with invalid program id
Given you land on a main page with an invalid program id
Then you should see error message
Expand Down
7 changes: 2 additions & 5 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: 2024-08-08T11:49:13.423Z\n"
"PO-Revision-Date: 2024-08-08T11:49:13.423Z\n"
"POT-Creation-Date: 2024-08-28T07:21:55.353Z\n"
"PO-Revision-Date: 2024-08-28T07:21:55.354Z\n"

msgid "Choose one or more dates..."
msgstr "Choose one or more dates..."
Expand Down Expand Up @@ -762,9 +762,6 @@ msgstr "View working list in this program."
msgid "Page is missing required values from URL"
msgstr "Page is missing required values from URL"

msgid "Program is not valid"
msgstr "Program is not valid"

msgid "Org unit is not valid with current program"
msgstr "Org unit is not valid with current program"

Expand Down
13 changes: 7 additions & 6 deletions src/core_modules/capture-core/HOC/withErrorMessageHandler.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @flow
import * as React from 'react';
import { withStyles } from '@material-ui/core/styles';
import { NoticeBox } from '@dhis2/ui';

const getStyles = (theme: Theme) => ({
const getStyles = () => ({
errorContainer: {
margin: 20,
color: theme.palette.error.main,
},
});

Expand All @@ -23,12 +23,13 @@ export const withErrorMessageHandler = () =>

if (error) {
return (
<div
data-test="error-message-handler"
<NoticeBox
error
dataTest="error-message-handler"
className={classes.errorContainer}
>
{error}
</div>
<div>{error}</div>
</NoticeBox>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { LoadingMaskForPage } from '../../LoadingMasks/LoadingMaskForPage.compon
import { withErrorMessageHandler } from '../../../HOC';
import { MissingMessage } from './MissingMessage.component';
import { EnrollmentPageDefault } from './EnrollmentPageDefault';
import { TopBar } from './TopBar.container';


const getStyles = ({ typography }) => ({
loadingMask: {
Expand All @@ -22,36 +22,19 @@ const getStyles = ({ typography }) => ({

const EnrollmentPagePlain = ({
classes,
programId,
orgUnitId,
enrollmentId,
trackedEntityName,
teiDisplayName,
enrollmentPageStatus,
enrollmentsAsOptions,
}) => (
<>
<TopBar
orgUnitId={orgUnitId}
programId={programId}
trackedEntityName={trackedEntityName}
teiDisplayName={teiDisplayName}
enrollmentsAsOptions={enrollmentsAsOptions}
enrollmentId={enrollmentId}
/>

<div data-test="enrollment-page-content">
{enrollmentPageStatus === enrollmentPageStatuses.MISSING_SELECTIONS && <MissingMessage />}
<div data-test="enrollment-page-content">
{enrollmentPageStatus === enrollmentPageStatuses.MISSING_SELECTIONS && <MissingMessage />}

{enrollmentPageStatus === enrollmentPageStatuses.DEFAULT && <EnrollmentPageDefault />}
{enrollmentPageStatus === enrollmentPageStatuses.DEFAULT && <EnrollmentPageDefault />}

{enrollmentPageStatus === enrollmentPageStatuses.LOADING && (
<div className={classes.loadingMask}>
<LoadingMaskForPage />
</div>
)}
</div>
</>
{enrollmentPageStatus === enrollmentPageStatuses.LOADING && (
<div className={classes.loadingMask}>
<LoadingMaskForPage />
</div>
)}
</div>
);

export const EnrollmentPageComponent: ComponentType<$Diff<Props, CssClasses>> = compose(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
useSetEnrollmentId,
} from '../../ScopeSelector';
import { useLocationQuery } from '../../../utils/routing';
import { TopBar } from './TopBar.container';

const useComponentLifecycle = () => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -114,15 +115,26 @@ export const EnrollmentPage: ComponentType<{||}> = () => {
useSelector(({ activePage }) => activePage.selectionsError && activePage.selectionsError.error);

return (
<EnrollmentPageComponent
error={error}
programId={programId}
orgUnitId={orgUnitId}
enrollmentId={enrollmentId}
teiDisplayName={teiDisplayName}
trackedEntityName={trackedEntityName}
enrollmentsAsOptions={enrollmentsAsOptions}
enrollmentPageStatus={useComputedEnrollmentPageStatus()}
/>
<>
<TopBar
orgUnitId={orgUnitId}
programId={programId}
trackedEntityName={trackedEntityName}
teiDisplayName={teiDisplayName}
enrollmentsAsOptions={enrollmentsAsOptions}
enrollmentId={enrollmentId}
/>
<EnrollmentPageComponent
error={error}
programId={programId}
orgUnitId={orgUnitId}
enrollmentId={enrollmentId}
teiDisplayName={teiDisplayName}
trackedEntityName={trackedEntityName}
enrollmentsAsOptions={enrollmentsAsOptions}
enrollmentPageStatus={useComputedEnrollmentPageStatus()}
/>
</>

);
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,21 @@ export const TopBar = ({
onResetOrgUnitId={() => resetOrgUnitId()}
onStartAgain={() => reset()}
>
<SingleLockedSelect
ready={Boolean(trackedEntityName && teiDisplayName)}
onClear={() => resetTeiId('/')}
options={[
{
label: teiDisplayName,
value: 'alwaysPreselected',
},
]}
selectedValue="alwaysPreselected"
title={trackedEntityName}
displayOnly
/>
{trackedEntityName ? (
<SingleLockedSelect
ready={Boolean(trackedEntityName && teiDisplayName)}
onClear={() => resetTeiId('/')}
options={[
{
label: teiDisplayName,
value: 'alwaysPreselected',
},
]}
selectedValue="alwaysPreselected"
title={trackedEntityName}
displayOnly
/>
) : <></>}
{enrollmentsAsOptions?.length > 0 ? (
<SingleLockedSelect
ready
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow
import React, { useMemo } from 'react';
import React, { useMemo, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import i18n from '@dhis2/d2-i18n';
import { NoticeBox, spacersNum } from '@dhis2/ui';
import withStyles from '@material-ui/core/styles/withStyles';
import { EnrollmentAddEventPageDefault } from './EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container';
import { useLocationQuery } from '../../../utils/routing';
import { useLocationQuery, buildUrlQueryString } from '../../../utils/routing';
import {
IdTypes,
useValidatedIDsFromCache,
Expand All @@ -27,6 +28,7 @@ const styles = {
},
};
const EnrollmentAddEventPagePlain = ({ classes }: Props) => {
const history = useHistory();
const { teiId, programId, orgUnitId, enrollmentId } = useLocationQuery();
const { valid: validIds, loading, error: validatedIdsError } = useValidatedIDsFromCache({ programId, orgUnitId });
const {
Expand Down Expand Up @@ -63,6 +65,12 @@ const EnrollmentAddEventPagePlain = ({ classes }: Props) => {
return EnrollmentAddEventPageStatuses.DEFAULT;
}, [enrollmentId, isLoading, loading, pageIsInvalid, programId, teiId, validIds]);

useEffect(() => {
if (pageStatus === EnrollmentAddEventPageStatuses.PROGRAM_INVALID) {
history.push(`/enrollment?${buildUrlQueryString({ orgUnitId, teiId, enrollmentId })}`);
}
}, [pageStatus, orgUnitId, teiId, enrollmentId, history]);

if (pageStatus === EnrollmentAddEventPageStatuses.LOADING) {
return <LoadingMaskForPage />;
}
Expand All @@ -89,10 +97,6 @@ const EnrollmentAddEventPagePlain = ({ classes }: Props) => {
i18n.t('Page is missing required values from URL')
)}

{pageStatus === EnrollmentAddEventPageStatuses.PROGRAM_INVALID && (
i18n.t('Program is not valid')
)}

{pageStatus === EnrollmentAddEventPageStatuses.ORG_UNIT_INVALID && (
i18n.t('Org unit is not valid with current program')
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import React, { useMemo } from 'react';
import { compose } from 'redux';
import { colors, spacers } from '@dhis2/ui';
import { withStyles } from '@material-ui/core/styles';
import type { ComponentType } from 'react';
import type { Props, ContainerProps } from './mainPage.types';
import { WorkingListsType } from './WorkingListsType';
import type { Props, PlainProps } from './mainPage.types';
import { MainPageStatuses } from './MainPage.constants';
import { WithoutOrgUnitSelectedMessage } from './WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage';
import { WithoutCategorySelectedMessage } from './WithoutCategorySelectedMessage/WithoutCategorySelectedMessage';
import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC';
import { TopBar } from './TopBar.container';
import { SearchBox } from '../../SearchBox';
import { TemplateSelector } from '../../TemplateSelector';
import {
Expand Down Expand Up @@ -41,79 +41,66 @@ const getStyles = () => ({
},
});

const useShowMainPage = ({ programId, orgUnitId, trackedEntityTypeId, displayFrontPageList, selectedTemplateId }) =>
useMemo(() => {
const noProgramSelected = !programId;
const noOrgUnitSelected = !orgUnitId;
const isEventProgram = !trackedEntityTypeId;

return noProgramSelected || noOrgUnitSelected || isEventProgram || displayFrontPageList || selectedTemplateId;
}, [programId, orgUnitId, trackedEntityTypeId, displayFrontPageList, selectedTemplateId]);

const MainPageBody = compose(
withErrorMessageHandler(),
withStyles(getStyles),
)(({ MainPageStatus, setShowAccessible, programId, showMainPage, classes, ...passOnProps }: PlainProps) => (
<>
{showMainPage ? (
<>
{MainPageStatus === MainPageStatuses.WITHOUT_ORG_UNIT_SELECTED && (
<WithoutOrgUnitSelectedMessage programId={programId} setShowAccessible={setShowAccessible} />
)}
{MainPageStatus === MainPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && (
<InvalidCategoryCombinationForOrgUnitMessage />
)}
{MainPageStatus === MainPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED && (
<WithoutCategorySelectedMessage programId={programId} />
)}
{MainPageStatus === MainPageStatuses.SHOW_WORKING_LIST && (
<div className={classes.listContainer} data-test={'main-page-working-list'}>
<WorkingListsType programId={programId} {...passOnProps} />
</div>
)}
</>
) : (
<div className={classes.container}>
<div className={`${classes.half} ${classes.searchBoxWrapper}`}>
<SearchBox programId={programId} />
</div>
<div className={classes.quarter}>
<TemplateSelector />
</div>
</div>
)}
</>
));

const MainPage = ({
const MainPagePlain = ({
programId,
orgUnitId,
trackedEntityTypeId,
displayFrontPageList,
selectedTemplateId,
selectedCategories,
...passOnProps
MainPageStatus,
setShowAccessible,
classes,
onChangeTemplate,
}: Props) => {
const showMainPage = useShowMainPage({
programId,
orgUnitId,
trackedEntityTypeId,
displayFrontPageList,
selectedTemplateId,
});
const showMainPage = useMemo(() => {
const noProgramSelected = !programId;
const noOrgUnitSelected = !orgUnitId;
const isEventProgram = !trackedEntityTypeId;
return noProgramSelected || noOrgUnitSelected || isEventProgram || displayFrontPageList || selectedTemplateId;
}, [programId, orgUnitId, trackedEntityTypeId, displayFrontPageList, selectedTemplateId]);

return (
<>
<TopBar programId={programId} orgUnitId={orgUnitId} selectedCategories={selectedCategories} />
<MainPageBody
programId={programId}
orgUnitId={orgUnitId}
selectedTemplateId={selectedTemplateId}
showMainPage={showMainPage}
{...passOnProps}
/>
{showMainPage ? (
<>
{MainPageStatus === MainPageStatuses.WITHOUT_ORG_UNIT_SELECTED && (
<WithoutOrgUnitSelectedMessage programId={programId} setShowAccessible={setShowAccessible} />
)}
{MainPageStatus === MainPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && (
<InvalidCategoryCombinationForOrgUnitMessage />
)}
{MainPageStatus === MainPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED && (
<WithoutCategorySelectedMessage programId={programId} />
)}
{MainPageStatus === MainPageStatuses.SHOW_WORKING_LIST && (
<div className={classes.listContainer} data-test={'main-page-working-list'}>
<WorkingListsType
programId={programId}
orgUnitId={orgUnitId}
selectedTemplateId={selectedTemplateId}
onChangeTemplate={onChangeTemplate}
/>
</div>
)}
</>
) : (
<div className={classes.container}>
<div className={`${classes.half} ${classes.searchBoxWrapper}`}>
<SearchBox programId={programId} />
</div>
<div className={classes.quarter}>
<TemplateSelector />
</div>
</div>
)}
</>
);
};

export const MainPageComponent = withLoadingIndicator()(MainPage);

export const MainPageComponent: ComponentType<ContainerProps> =
compose(
withLoadingIndicator(),
withErrorMessageHandler(),
withStyles(getStyles),
)(MainPagePlain);
Loading
Loading