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
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: 2024-08-08T11:49:13.423Z\n"
"PO-Revision-Date: 2024-08-08T11:49:13.423Z\n"
"POT-Creation-Date: 2024-08-22T21:46:48.644Z\n"
"PO-Revision-Date: 2024-08-22T21:46:48.644Z\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 @@ -6,7 +6,6 @@ import { compose } from 'redux';
import type { Props } from './EnrollmentPage.types';
import { enrollmentPageStatuses } from './EnrollmentPage.constants';
import { LoadingMaskForPage } from '../../LoadingMasks/LoadingMaskForPage.component';
import { withErrorMessageHandler } from '../../../HOC';
import { MissingMessage } from './MissingMessage.component';
import { EnrollmentPageDefault } from './EnrollmentPageDefault';
import { TopBar } from './TopBar.container';
Expand Down Expand Up @@ -55,6 +54,5 @@ const EnrollmentPagePlain = ({
);

export const EnrollmentPageComponent: ComponentType<$Diff<Props, CssClasses>> = compose(
withErrorMessageHandler(),
withStyles(getStyles),
)(EnrollmentPagePlain);
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ export const missingStatuses = {
MISSING_ENROLLMENT_SELECTION_ADD_NEW: 'MISSING_ENROLLMENT_SELECTION_ADD_NEW',
MISSING_PROGRAM_CATEGORIES_SELECTION: 'MISSING_PROGRAM_CATEGORIES_SELECTION',
MISSING_PROGRAM_SELECTION: 'MISSING_PROGRAM_SELECTION',
INVALID_PROGRAM_ID: 'INVALID_PROGRAM_ID',
};

const useMissingStatus = () => {
const [missingStatus, setStatus] = useState(null);

const { programId, enrollmentId, teiId } = useLocationQuery();

const { scopeType, tetId: scopeTetId } = useScopeInfo(programId);
const { scopeType, tetId: scopeTetId, programName } = useScopeInfo(programId);
const { programSelectionIsIncomplete } = useMissingCategoriesInProgramSelection();
const {
programHasEnrollments,
Expand All @@ -47,8 +47,9 @@ const useMissingStatus = () => {
useEffect(() => {
const selectedProgramIsTracker = programId && scopeType === scopeTypes.TRACKER_PROGRAM;
const selectedProgramIsEvent = programId && scopeType === scopeTypes.EVENT_PROGRAM;

if (enrollmentAccessLevel === enrollmentAccessLevels.LIMITED_ACCESS) {
if (programId && !programName) {
setStatus(missingStatuses.INVALID_PROGRAM_ID);
} else if (enrollmentAccessLevel === enrollmentAccessLevels.LIMITED_ACCESS) {
setStatus(missingStatuses.PROTECTED_PROGRAM_WITH_BREAKING_THE_GLASS);
} else if (enrollmentAccessLevel === enrollmentAccessLevels.NO_ACCESS) {
setStatus(missingStatuses.RESTRICTED_PROGRAM_NO_ACCESS);
Expand All @@ -71,6 +72,7 @@ const useMissingStatus = () => {
}
}, [
programId,
programName,
programSelectionIsIncomplete,
programHasEnrollments,
programHasActiveEnrollments,
Expand Down Expand Up @@ -132,6 +134,15 @@ export const MissingMessage = withStyles(getStyles)(({
const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId);

return (<>
{
missingStatus === missingStatuses.INVALID_PROGRAM_ID &&
<IncompleteSelectionsMessage>
{i18n.t('Program with id "{{programId}}" does not exist', {
programId,
interpolation: { escapeValue: false },
})}
</IncompleteSelectionsMessage>
}
{
missingStatus === missingStatuses.MISSING_PROGRAM_SELECTION &&
<IncompleteSelectionsMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import type { ComponentType } from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import { OrgUnitFetcher } from 'capture-core/components/OrgUnitFetcher';
import i18n from '@dhis2/d2-i18n';
import { Button } from '@dhis2/ui';
import { Button, NoticeBox } from '@dhis2/ui';
import { TopBar } from './TopBar.container';
import type { ContainerProps, Props } from './NewPage.types';
import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC';
import { withLoadingIndicator } from '../../../HOC';
import { NEW_TEI_DATA_ENTRY_ID, newPageStatuses } from './NewPage.constants';
import { useScopeInfo } from '../../../hooks/useScopeInfo';
import { RegistrationDataEntry } from './RegistrationDataEntry';
Expand Down Expand Up @@ -42,6 +42,7 @@ const NewPagePlain = ({
trackedEntityName,
teiDisplayName,
trackedEntityInstanceAttributes,
error,
}: Props) => {
const { scopeType } = useScopeInfo(currentScopeId);
const [selectedScopeId, setScopeId] = useState(currentScopeId);
Expand All @@ -60,8 +61,7 @@ const NewPagePlain = ({
} else {
showDefaultViewOnNewPage();
}
},
[
}, [
programCategorySelectionIncomplete,
orgUnitSelectionIncomplete,
showMessageToSelectOrgUnitOnNewPage,
Expand All @@ -70,96 +70,104 @@ const NewPagePlain = ({
categoryOptionIsInvalidForOrgUnit,
showMessageThatCategoryOptionIsInvalidForOrgUnit,
]);

const orgUnitId = useSelector(({ currentSelections }) => currentSelections.orgUnitId);

return (<>
<TopBar
orgUnitId={orgUnitId}
programId={programId}
isUserInteractionInProgress={isUserInteractionInProgress}
teiId={teiId}
trackedEntityName={trackedEntityName}
teiDisplayName={teiDisplayName}
formIsOpen={newPageStatus === newPageStatuses.DEFAULT}
/>
<div data-test="registration-page-content" className={classes.container} >
{
!writeAccess ?
<NoWriteAccessMessage
message={
i18n.t("You don't have access to create a {{trackedEntityName}} in the current selections",
{
trackedEntityName,
interpolation: { escapeValue: false },
},
)}
/>
:
<OrgUnitFetcher orgUnitId={orgUnitId}>
{
newPageStatus === newPageStatuses.DEFAULT &&
<RegistrationDataEntry
dataEntryId={NEW_TEI_DATA_ENTRY_ID}
selectedScopeId={selectedScopeId}
setScopeId={setScopeId}
trackedEntityInstanceAttributes={trackedEntityInstanceAttributes}
/>
}
const renderContent = () => {
if (error) {
return (
<NoticeBox
error
>
{error}
</NoticeBox>
);
}

{
newPageStatus === newPageStatuses.WITHOUT_ORG_UNIT_SELECTED &&
<>
<IncompleteSelectionsMessage>
{i18n.t('Choose an organisation unit to start reporting')}
</IncompleteSelectionsMessage>
<Button
dataTest="new-page-cancel-button"
onClick={handleMainPageNavigation}
>
{i18n.t('Cancel')}
</Button>
</>
}
if (!writeAccess) {
return (
<NoWriteAccessMessage
message={
i18n.t("You don't have access to create a {{trackedEntityName}} in the current selections",
{
trackedEntityName,
interpolation: { escapeValue: false },
},
)}
/>
);
}

return (
<OrgUnitFetcher orgUnitId={orgUnitId}>
{newPageStatus === newPageStatuses.DEFAULT && (
<RegistrationDataEntry
dataEntryId={NEW_TEI_DATA_ENTRY_ID}
selectedScopeId={selectedScopeId}
setScopeId={setScopeId}
trackedEntityInstanceAttributes={trackedEntityInstanceAttributes}
/>
)}

{
newPageStatus === newPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED &&
(() => {
const missingCategories = missingCategoriesInProgramSelection.reduce((acc, { name }, index) => {
if ((index + 1 === missingCategoriesInProgramSelection.length)) {
return `${acc} ${name} ${missingCategoriesInProgramSelection.length > 1 ? 'categories' : 'category'}`;
}
return `${acc} ${name},`;
}, '');
{newPageStatus === newPageStatuses.WITHOUT_ORG_UNIT_SELECTED && (
<>
<IncompleteSelectionsMessage>
{i18n.t('Choose an organisation unit to start reporting')}
</IncompleteSelectionsMessage>
<Button
dataTest="new-page-cancel-button"
onClick={handleMainPageNavigation}
>
{i18n.t('Cancel')}
</Button>
</>
)}

return (
<IncompleteSelectionsMessage>
{i18n.t('Choose the {{missingCategories}} to start reporting', {
missingCategories, interpolation: { escapeValue: false },
})}
</IncompleteSelectionsMessage>
);
})()
}
{newPageStatus === newPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED && (
<IncompleteSelectionsMessage>
{i18n.t('Choose the {{missingCategories}} to start reporting', {
missingCategories: missingCategoriesInProgramSelection.reduce((acc, { name }, index) => {
if ((index + 1 === missingCategoriesInProgramSelection.length)) {
return `${acc} ${name} ${missingCategoriesInProgramSelection.length > 1 ? 'categories' : 'category'}`;
}
return `${acc} ${name},`;
}, ''),
interpolation: { escapeValue: false },
})}
</IncompleteSelectionsMessage>
)}

{
newPageStatus === newPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && (
<IncompleteSelectionsMessage>
{i18n.t(
'The category option is not valid for the selected organisation unit. Please select a valid combination.',
)}
</IncompleteSelectionsMessage>
)
}
{newPageStatus === newPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && (
<IncompleteSelectionsMessage>
{i18n.t(
'The category option is not valid for the selected organisation unit. Please select a valid combination.',
)}
</IncompleteSelectionsMessage>
)}
</OrgUnitFetcher>
);
};

</OrgUnitFetcher>
}
</div>
</>);
return (
<>
<TopBar
orgUnitId={orgUnitId}
programId={programId}
isUserInteractionInProgress={isUserInteractionInProgress}
teiId={teiId}
trackedEntityName={trackedEntityName}
teiDisplayName={teiDisplayName}
formIsOpen={newPageStatus === newPageStatuses.DEFAULT}
/>
<div data-test="registration-page-content" className={classes.container}>
{renderContent()}
</div>
</>
);
};

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