Skip to content
Draft
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
22 changes: 22 additions & 0 deletions .cursor/rules/code-guidelines.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
description: General code guidelines for bugs and features.
globs: src/*.js
alwaysApply: false
---

## Logging
When logging things to the browser console please use the following format.

```
import log from 'loglevel';
import { errorCreator } from 'capture-core-utils';

log.error(
errorCreator('Message to be logged')(
{
// Any details
},
),
);

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @flow
import React from 'react';
import { withStyles } from '@material-ui/core';
import { colors, spacers } from '@dhis2/ui';
import i18n from '@dhis2/d2-i18n';
import { SingleEventRegistrationEntry } from '../DataEntries';
import type { Props } from './EventRegistrationEntryWrapper.types';

const getStyles = () => ({
container: {
marginBottom: spacers.dp12,
padding: spacers.dp16,
background: colors.white,
border: '1px solid',
borderColor: colors.grey400,
borderRadius: 3,
},
title: {
padding: '8px 0 0px 8px',
fontWeight: 500,
marginBottom: spacers.dp16,
},
flexContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
flexWrap: 'wrap',
},
flexItem: {
flex: 1,
minWidth: '500px',
},
noAccessContainer: {
padding: spacers.dp16,
},
});

const EventRegistrationEntryWrapperPlain = ({
classes,
selectedScopeId,
dataEntryId,
eventAccess,
}: Props) => {
if (!eventAccess?.write) {
return (
<div className={classes.noAccessContainer}>
{i18n.t('You don\'t have access to create an event in the current selections')}
</div>
);
}

return (
<SingleEventRegistrationEntry
id={dataEntryId}
selectedScopeId={selectedScopeId}
/>
);
};

export const EventRegistrationEntryWrapperComponent = withStyles(getStyles)(EventRegistrationEntryWrapperPlain);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// @flow
import { connect, useDispatch, useSelector } from 'react-redux';
import * as React from 'react';
import { useEffect, useRef } from 'react';
import { compose } from 'redux';
import { batchActions } from 'redux-batched-actions';
import { EventRegistrationEntryWrapperComponent } from './EventRegistrationEntryWrapper.component';
import { withLoadingIndicator } from '../../HOC/withLoadingIndicator';
import type { ContainerProps, StateProps, ReduxState } from './EventRegistrationEntryWrapper.types';
import { getOpenDataEntryActions } from '../DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry';
import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations';
import { makeEventAccessSelector } from './selectors';
import { itemId } from './constants';

const makeMapStateToProps = () => {
const eventAccessSelector = makeEventAccessSelector();
return (state: ReduxState, ownProps: ContainerProps): StateProps => ({
selectedScopeId: ownProps.selectedScopeId,
dataEntryId: ownProps.dataEntryId,
orgUnitId: state.currentSelections.orgUnitId,
ready: state.dataEntries[ownProps.dataEntryId]?.itemId === itemId,
eventAccess: eventAccessSelector(state),
});
};

const openSingleEventDataEntry = (InnerComponent: React.ComponentType<any>) => (
(props: ContainerProps) => {
const hasRun = useRef<boolean>(false);
const { selectedScopeId } = props;
const dispatch = useDispatch();
const selectedCategories = useSelector((state: ReduxState) => state.currentSelections.categories);
const { isLoading, programCategory } = useCategoryCombinations(selectedScopeId);

useEffect(() => {
if (!isLoading && !hasRun.current) {
dispatch(
batchActions([
...getOpenDataEntryActions(programCategory, selectedCategories),
]),
);
hasRun.current = true;
}
}, [selectedCategories, dispatch, isLoading, programCategory]);

return (
<InnerComponent
{...props}
/>
);
});

export const EventRegistrationEntryWrapper = compose(
openSingleEventDataEntry,
connect(makeMapStateToProps(), () => ({})),
withLoadingIndicator(),
)(EventRegistrationEntryWrapperComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @flow
export type Props = {|
classes: Object,
selectedScopeId: string,
dataEntryId: string,
orgUnitId: string,
eventAccess?: {|
read: boolean,
write: boolean,
|},
|};

export type ContainerProps = {|
selectedScopeId: string,
dataEntryId: string,
|};

export type StateProps = {|
selectedScopeId: string,
orgUnitId: string,
dataEntryId: string,
eventAccess: {|
read: boolean,
write: boolean,
|},
ready: boolean,
|};

export type ReduxState = Object;
export type MapStateToProps = (ReduxState, ContainerProps) => StateProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @flow

// Constants duplicated from SingleEventRegistrationEntry
export const dataEntryId = 'singleEvent';
export const itemId = 'newEvent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { EventRegistrationEntryWrapper } from './EventRegistrationEntryWrapper.container';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @flow

import { createSelector } from 'reselect';
import { getProgramEventAccess } from '../../metaData';

const programIdSelector = state => state.currentSelections.programId;
const categoriesMetaSelector = state => state.currentSelections.categoriesMeta;
const programStageIdSelector = state => state.currentSelections.stageId;

// $FlowFixMe[missing-annot] automated comment
export const makeEventAccessSelector = () => createSelector(
programIdSelector,
categoriesMetaSelector,
programStageIdSelector,
(programId: string, categoriesMeta: ?Object, programStageId: ?string) =>
programId && getProgramEventAccess(programId, programStageId, categoriesMeta),
);
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type InterfaceProps = $ReadOnly<{|
onSetColumnOrder: SetColumnOrder,
onSort: Sort,
onUpdateFilter: UpdateFilter,
onChangeTemplate?: (selectedTemplateId?: string) => void,
rowIdKey: string,
rowsPerPage: number,
sortById: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @flow
export type Props = $ReadOnly<{|
programId: string,
orgUnitId: string
orgUnitId: string,
selectedTemplateId?: string,
onChangeTemplate?: (selectedTemplateId?: string) => void
|}>;
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import { WithoutCategorySelectedMessage } from './WithoutCategorySelectedMessage
import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC';
import { SearchBox } from '../../SearchBox';
import { TemplateSelector } from '../../TemplateSelector';
import { NoSelectionsInfoBox } from './NoSelectionsInfoBox';
import { EventRegistrationEntryWrapper } from '../../EventRegistrationEntryWrapper';
import {
InvalidCategoryCombinationForOrgUnitMessage,
} from './InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage';
import { NoSelectionsInfoBox } from './NoSelectionsInfoBox';

const getStyles = () => ({
listContainer: {
Expand Down Expand Up @@ -56,9 +57,12 @@ const MainPagePlain = ({
const showMainPage = useMemo(() => {
const noProgramSelected = !programId;
const noOrgUnitSelected = !orgUnitId;
const isEventProgram = !trackedEntityTypeId;
return noProgramSelected || noOrgUnitSelected || isEventProgram || displayFrontPageList || selectedTemplateId;
}, [programId, orgUnitId, trackedEntityTypeId, displayFrontPageList, selectedTemplateId]);

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

return (
<>
Expand Down Expand Up @@ -88,14 +92,30 @@ const MainPagePlain = ({
)}
</>
) : (
<div className={classes.container}>
<div className={`${classes.half} ${classes.searchBoxWrapper}`}>
<SearchBox programId={programId} />
</div>
<div className={classes.quarter}>
<TemplateSelector />
</div>
</div>
<>
{trackedEntityTypeId ? (
<div className={classes.container}>
<div className={`${classes.half} ${classes.searchBoxWrapper}`}>
<SearchBox programId={programId} />
</div>
<div className={classes.quarter}>
<TemplateSelector />
</div>
</div>
) : (
<div className={classes.container}>
<div className={classes.half}>
<EventRegistrationEntryWrapper
dataEntryId="singleEvent"
selectedScopeId={programId}
/>
</div>
<div className={classes.quarter}>
<TemplateSelector />
</div>
</div>
)}
</>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import { TopBar } from './TopBar.container';

const mapStateToProps = (state: ReduxState) => ({
error: state.activePage.selectionsError && state.activePage.selectionsError.error, // TODO: Should probably remove this

Check warning on line 16 in src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AJ_S3uyRsdjwKhb&open=AZ6N8AJ_S3uyRsdjwKhb&pullRequest=3983

Check warning on line 16 in src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AJ_S3uyRsdjwKha&open=AZ6N8AJ_S3uyRsdjwKha&pullRequest=3983
ready: !state.activePage.lockedSelectorLoads, // TODO: Should probably remove this

Check warning on line 17 in src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AJ_S3uyRsdjwKhc&open=AZ6N8AJ_S3uyRsdjwKhc&pullRequest=3983
});

const handleChangeTemplateUrl = ({ programId, orgUnitId, selectedTemplateId, showAllAccessible, navigate }) => {
Expand Down Expand Up @@ -46,7 +46,7 @@
if (selectedProgram?.categoryCombination) {
if (!categories) return MainPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED;
const programCategories = Array.from(selectedProgram.categoryCombination.categories.values());
if (programCategories.some(category => !categories || !categories[category.id])) {

Check warning on line 49 in src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AJ_S3uyRsdjwKhd&open=AZ6N8AJ_S3uyRsdjwKhd&pullRequest=3983
return MainPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED;
}
if (withoutOrgUnit) {
Expand Down Expand Up @@ -74,7 +74,7 @@
reduxSelectedTemplateId: workingListsTemplates.teiList?.selectedTemplateId,
workingListProgramId: workingListsContext.teiList?.programIdView,
ready: !activePage.isLoading && !activePage.lockedSelectorLoads,
error: activePage.selectionsError && activePage.selectionsError.error,

Check warning on line 77 in src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AJ_S3uyRsdjwKhe&open=AZ6N8AJ_S3uyRsdjwKhe&pullRequest=3983
}),
shallowEqual,
);
Expand Down Expand Up @@ -115,7 +115,7 @@
const selectedProgram = programCollection.get(programId);
// $FlowFixMe[prop-missing]
const trackedEntityTypeId = selectedProgram?.trackedEntityType?.id;
const displayFrontPageList = trackedEntityTypeId && selectedProgram?.displayFrontPageList;
const displayFrontPageList = selectedProgram?.displayFrontPageList;
const MainPageStatus = useMainPageStatus({
programId,
selectedProgram,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@
export const WorkingListsType = ({ programId, orgUnitId, selectedTemplateId, onChangeTemplate }: Props) => {
const { programType } = useProgramInfo(programId);
if (programType === programTypes.EVENT_PROGRAM) {
return <EventWorkingListsInit programId={programId} orgUnitId={orgUnitId} />;
return (
<EventWorkingListsInit
programId={programId}
orgUnitId={orgUnitId}
selectedTemplateId={selectedTemplateId}
onChangeTemplate={onChangeTemplate}
/>
);
}

if (programType === programTypes.TRACKER_PROGRAM) {
return (
<>
<TeiWorkingLists
programId={programId}
orgUnitId={orgUnitId}
selectedTemplateId={selectedTemplateId}
onChangeTemplate={onChangeTemplate}
/>
</>

Check warning on line 30 in src/core_modules/capture-core/components/Pages/MainPage/WorkingListsType/WorkingListsType.component.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A fragment with only one child is redundant.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AZ6N8AF6S3uyRsdjwKhZ&open=AZ6N8AF6S3uyRsdjwKhZ&pullRequest=3983
);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @flow
import React from 'react';
import { TemplateSelector as TemplateSelectorComponent } from '../TemplateSelector.component';
import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing';
import { useEventTemplates } from '../hooks';

export const EventTemplateSelector = () => {
const { navigate } = useNavigate();
const { programId, orgUnitId } = useLocationQuery();
const { eventTemplates, loading: loadingEventTemplates } = useEventTemplates(programId);

const onSelectTemplate = template =>
navigate(`/?${buildUrlQueryString({ orgUnitId, programId, selectedTemplateId: template.id })}`);
const onCreateTemplate = () => {
const urlQueryString = buildUrlQueryString({
orgUnitId,
programId,
selectedTemplateId: `${programId}-default`,
});
navigate(`/?${urlQueryString}`);
};

return programId && !loadingEventTemplates ? (
<TemplateSelectorComponent
templates={eventTemplates}
onSelectTemplate={onSelectTemplate}
onCreateTemplate={onCreateTemplate}
/>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { EventTemplateSelector } from './EventTemplateSelector.container';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @flow
import React from 'react';
import { TemplateSelector as TemplateSelectorComponent } from '../TemplateSelector.component';
import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing';
import { useTEITemplates, useProgramStageTemplates } from '../hooks';

export const TEITemplateSelector = () => {
const { navigate } = useNavigate();
const { programId, orgUnitId } = useLocationQuery();
const { TEITemplates, loading: loadingTEITemplates } = useTEITemplates(programId);
const { programStageTemplates, loading: loadingProgramStageTemplates } = useProgramStageTemplates(programId);

const onSelectTemplate = template =>
navigate(`/?${buildUrlQueryString({ orgUnitId, programId, selectedTemplateId: template.id })}`);
const onCreateTemplate = () => {
const urlQueryString = buildUrlQueryString({
orgUnitId,
programId,
selectedTemplateId: `${programId}-default`,
});
navigate(`/?${urlQueryString}`);
};

return programId && !loadingTEITemplates && !loadingProgramStageTemplates ? (
<TemplateSelectorComponent
templates={[...TEITemplates, ...programStageTemplates]}
onSelectTemplate={onSelectTemplate}
onCreateTemplate={onCreateTemplate}
/>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { TEITemplateSelector } from './TEITemplateSelector.container';
Loading
Loading