From ac4b6fe0309ca7339f0268466f854eb004c95e97 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Fri, 21 Jan 2022 07:58:00 +0000 Subject: [PATCH 01/95] feat: [DHIS2-12362] add widget relationship component --- i18n/en.pot | 24 +++++++---------- .../WidgetRelationship.component.js | 26 +++++++++++++++++++ .../hooks/useTeiRelationship.js | 21 +++++++++++++++ .../components/WidgetRelationship/index.js | 1 + .../widgetRelationship.types.js | 6 +++++ 5 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js diff --git a/i18n/en.pot b/i18n/en.pot index d2050949c2..3b9be1e06c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2021-12-21T16:14:01.098Z\n" -"PO-Revision-Date: 2021-12-21T16:14:01.098Z\n" +"POT-Creation-Date: 2022-01-21T07:58:05.383Z\n" +"PO-Revision-Date: 2022-01-21T07:58:05.383Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -614,19 +614,19 @@ msgid "No feedback for this enrollment yet" msgstr "No feedback for this enrollment yet" msgid "Quick actions" -msgstr "" +msgstr "Quick actions" msgid "New Event" -msgstr "" +msgstr "New Event" msgid "Schedule an event" -msgstr "" +msgstr "Schedule an event" msgid "Make referral" -msgstr "" +msgstr "Make referral" msgid "No available program stages" -msgstr "" +msgstr "No available program stages" msgid "Program stage not found" msgstr "Program stage not found" @@ -1097,14 +1097,8 @@ msgstr "Profile widget could not be loaded. Please try again later" msgid "Person Profile" msgstr "Person Profile" -msgid "You can’t add any more events in this program" -msgstr "" - -msgid "Cancel without saving" -msgstr "" - -msgid "Choose a stage for a new event" -msgstr "" +msgid "TEI's Relationships" +msgstr "TEI's Relationships" msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js new file mode 100644 index 0000000000..22bb7de8c8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -0,0 +1,26 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Widget } from '../Widget'; +import { useTeiRelationship } from './hooks/useTeiRelationship'; +import type { Props } from './widgetRelationship.types'; + +export const WidgetRelationship = ({ teiId }: Props) => { + const [open, setOpenStatus] = useState(true); + const { teiRelationship } = useTeiRelationship(teiId); + console.log({ teiRelationship }); + return ( +
+ setOpenStatus(true), [setOpenStatus])} + onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} + open={open} + > + Hello world + +
+ ); +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js new file mode 100644 index 0000000000..efacae8f9e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js @@ -0,0 +1,21 @@ +// @flow +import { useMemo } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +export const useTeiRelationship = (teiId) => { + const teiRelationshipQuery = useMemo(() => ({ + teiRelationship: { + resource: 'relationships', + params: { + tei: teiId, + }, + }, + }), [teiId]); + const { + loading, + data, + error, + } = useDataQuery(teiRelationshipQuery); + + return { error, teiRelationship: !loading && data?.teiRelationship }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/index.js b/src/core_modules/capture-core/components/WidgetRelationship/index.js new file mode 100644 index 0000000000..2fdfb26a3a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/index.js @@ -0,0 +1 @@ +export { WidgetRelationship } from './WidgetRelationship.component'; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js new file mode 100644 index 0000000000..06000fe42d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -0,0 +1,6 @@ +// @flow + + +export type Props = {| + teiId: string +|}; From 7d9f3dec1d65a644f37a5d2914fb1239f219d65a Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Fri, 21 Jan 2022 19:49:43 +0100 Subject: [PATCH 02/95] feat: first draft to New TEI relationship --- i18n/en.pot | 37 +++++---- .../EnrollmentPageDefault.component.js | 12 ++- .../EnrollmentPageDefault.container.js | 7 +- .../EnrollmentPageDefault.types.js | 3 + .../NewTrackedEntityRelationship.component.js | 79 +++++++++++++++++++ .../NewTrackedEntityRelationship.container.js | 74 +++++++++++++++++ .../NewTrackedEntityRelationship.types.js | 8 ++ .../WidgetTrackedEntityRelationship.const.js | 7 ++ .../WidgetTrackedEntityRelationship.js | 35 ++++++++ .../WidgetTrackedEntityRelationsip.types.js | 8 ++ .../WidgetTrackedEntityRelationship/hooks.js | 12 +++ 11 files changed, 265 insertions(+), 17 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js diff --git a/i18n/en.pot b/i18n/en.pot index 6ea68f1b79..9649e6a80c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2021-12-21T16:14:01.098Z\n" -"PO-Revision-Date: 2021-12-21T16:14:01.098Z\n" +"POT-Creation-Date: 2022-01-21T18:49:46.745Z\n" +"PO-Revision-Date: 2022-01-21T18:49:46.745Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -614,19 +614,19 @@ msgid "No feedback for this enrollment yet" msgstr "No feedback for this enrollment yet" msgid "Quick actions" -msgstr "" +msgstr "Quick actions" msgid "New Event" -msgstr "" +msgstr "New Event" msgid "Schedule an event" -msgstr "" +msgstr "Schedule an event" msgid "Make referral" -msgstr "" +msgstr "Make referral" msgid "No available program stages" -msgstr "" +msgstr "No available program stages" msgid "Program stage not found" msgstr "Program stage not found" @@ -1120,14 +1120,6 @@ msgstr "Person Profile" msgid "Edit" msgstr "Edit" -msgid "You can’t add any more events in this program" -msgstr "" - -msgid "Cancel without saving" -msgstr "" - -msgid "Choose a stage for a new event" -msgstr "" msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" @@ -1168,6 +1160,21 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "Link to an existing person" +msgstr "Link to an existing person" + +msgid "Create new" +msgstr "Create new" + +msgid "An error occurred" +msgstr "An error occurred" + +msgid "New TEI - Relationship handler" +msgstr "New TEI - Relationship handler" + +msgid "New Relationship" +msgstr "New Relationship" + msgid "Delete event" msgstr "Delete event" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 88b6560f03..619fe46853 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -13,10 +13,14 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; +import { + WidgetTrackedEntityRelationship, +} from '../../../WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship'; const getStyles = ({ typography }) => ({ columns: { display: 'flex', + position: 'relative', }, leftColumn: { flexGrow: 3, @@ -56,10 +60,12 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, + renderRelationshipRef, + relationshipTypes, }: PlainProps) => ( <>
{i18n.t('Enrollment Dashboard')}
-
+
+ diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 055781945d..5b24168338 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useRef } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; // $FlowFixMe @@ -18,11 +18,13 @@ import { import { buildUrlQueryString } from '../../../../utils/routing'; import { deleteEnrollment } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useRelationshipTypes } from '../../../WidgetTrackedEntityRelationship/hooks'; export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); + const renderRelationshipRef = useRef(); const { enrollmentId, programId, teiId, orgUnitId } = useSelector( ({ router: { @@ -36,6 +38,7 @@ export const EnrollmentPageDefault = () => { orgUnitId: query.orgUnitId, }), shallowEqual); const { orgUnit } = useOrganisationUnit(orgUnitId); + const { relationshipTypes } = useRelationshipTypes(); const program = useTrackerProgram(programId); const { @@ -98,6 +101,8 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} + renderRelationshipRef={renderRelationshipRef} + relationshipTypes={relationshipTypes} /> ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 8b5ede4c51..65bcf8deb8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -3,6 +3,7 @@ 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'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { RelationshipType } from '../../../../metaData'; export type Props = {| program: Program, @@ -17,6 +18,8 @@ export type Props = {| onViewAll: (stageId: string) => void, onCreateNew: (stageId: string) => void, onEventClick: (eventId: string, stageId: string) => void, + renderRelationshipRef: Object, + relationshipTypes: ?Array |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js new file mode 100644 index 0000000000..a214743897 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -0,0 +1,79 @@ +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button, IconSearch24, IconAdd24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; + +const styles = { + container: { + padding: spacers.dp16, + paddingTop: 0, + }, + typeselector: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp8, + marginBottom: spacers.dp16, + }, + creationselector: { + display: 'flex', + gap: spacers.dp4, + }, + selectorButton: { + display: 'block', + }, + cancelButton: { + marginTop: spacers.dp8, + }, +}; + +const NewTrackedEntityRelationshipComponentPlain = ({ relationshipTypes, onSelectType, onCancel, pageStatus, classes }) => { + if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { + return ( +
+
+ {relationshipTypes?.map(relationship => ( +
+ +
+ ))} +
+ +
+ ); + } + + if (pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE) { + return ( +
+
+ + +
+ +
+ ); + } + + return

{i18n.t('An error occurred')}

; +}; + +export const NewTrackedEntityRelationshipComponent = withStyles(styles)(NewTrackedEntityRelationshipComponentPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js new file mode 100644 index 0000000000..27a60ffb86 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -0,0 +1,74 @@ +// @flow +import React, { useCallback, useMemo, useState } from 'react'; +import * as ReactDOM from 'react-dom'; +import { colors } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../Widget'; +import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; +import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; +import type { Props } from './NewTrackedEntityRelationship.types'; + +const styles = { + container: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: '#fff', + borderRadius: 3, + borderStyle: 'solid', + borderColor: colors.grey400, + borderWidth: 1, + }, +}; + +export const NewTrackedEntityRelationshipPlain = ({ renderRef, showDialog, hideDialog, classes, ...passOnProps }: Props) => { + const [selectedRelationshipType, setSelectedRelationshipType] = useState(); + const [creationMode, setCreationMode] = useState(); + + const pageStatus = useMemo(() => { + if (!selectedRelationshipType) { + return NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE; + } + if (!creationMode) { + return NewTEIRelationshipStatuses.MISSING_CREATION_MODE; + } + return NewTEIRelationshipStatuses.DEFAULT; + }, [creationMode, selectedRelationshipType]); + + + const onSelectRelationshipType = useCallback( + relationshipType => setSelectedRelationshipType(relationshipType), [], + ); + + const onCancel = useCallback(() => { + hideDialog(); + setSelectedRelationshipType(); + setCreationMode(); + }, [hideDialog]); + + if (!showDialog || !renderRef.current) { + return null; + } + + return ReactDOM.createPortal(( +
+ {i18n.t('New TEI - Relationship handler')}

} + > + +
+
+ ), renderRef.current); +}; + +export const NewTrackedEntityRelationship = withStyles(styles)(NewTrackedEntityRelationshipPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..c22faef02b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -0,0 +1,8 @@ +// @flow + +export type Props = {| + renderRef: Object, + showDialog: boolean, + hideDialog: () => void, + ...CssClasses, +|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js new file mode 100644 index 0000000000..6cca69cf45 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js @@ -0,0 +1,7 @@ +export const NewTEIRelationshipStatuses = Object.freeze({ + DEFAULT: 'Default', + DATA_ENTRY_NEW_RECORD: 'DataEntryNewRecord', + LINK_TO_EXISTING: 'LinkToExisting', + MISSING_RELATIONSHIP_TYPE: 'MissingRelationshipType', + MISSING_CREATION_MODE: 'MissingCreationMode', +}); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js new file mode 100644 index 0000000000..6e4e1ab1a7 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js @@ -0,0 +1,35 @@ +// @flow +import React, { useCallback, useState } from 'react'; +import { Button } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship/NewTrackedEntityRelationship.container'; +import type { Props } from './WidgetTrackedEntityRelationsip.types'; + +export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef }: Props) => { + const [showDialog, setShowDialog] = useState(false); + + const onStartNewRelationship = useCallback(() => { + setShowDialog(prevState => !prevState); + }, []); + + const hideDialog = useCallback(() => { + setShowDialog(prevState => !prevState); + }, []); + + return ( + <> + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js new file mode 100644 index 0000000000..9fb4ad35d0 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js @@ -0,0 +1,8 @@ +// @flow + +import type { RelationshipType } from '../../metaData'; + +export type Props = {| + relationshipTypes: ?Array, + renderRef: Object, +|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js new file mode 100644 index 0000000000..626a46a9b6 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +export const useRelationshipTypes = () => { + const { data, error, loading } = useDataQuery(useMemo(() => ({ + relationshipTypes: { + resource: 'relationshipTypes', + }, + }), [])); + + return !loading && !error && data.relationshipTypes; +}; From 07b42faeffc4c035517256bd5c9d0f3a38bbc32b Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 25 Jan 2022 09:49:56 +0000 Subject: [PATCH 03/95] feat: [DHIS2-12362] add relationship widget --- .../EnrollmentPageDefault.component.js | 2 + .../RelationshipTable.component.js | 73 +++++++++++++++++++ .../Relationships/RelationshipTable/index.js | 5 ++ .../Relationships/Relationships.component.js | 43 +++++++++++ .../WidgetRelationship/Relationships/index.js | 5 ++ .../WidgetRelationship.component.js | 15 +++- .../WidgetRelationship/hooks/useComputeTEI.js | 22 ++++++ .../hooks/useTeiRelationship.js | 2 +- .../widgetRelationship.types.js | 3 +- 9 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 442930ddaa..89a9941eb4 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -13,6 +13,7 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; +import { WidgetRelationship } from '../../../WidgetRelationship'; const getStyles = ({ typography }) => ({ columns: { @@ -76,6 +77,7 @@ export const EnrollmentPageDefaultPlain = ({ + {!hideWidgets.indicator && ( { + const { classes, from, to } = props; + const { headers, commonAttributes } = useComputeTEI(from, to); + + function renderHeader() { + const headerCells = headers + .map(column => ( + + {column.displayName} + + )); + return ( + + {headerCells} + + ); + } + const renderRelationshipRows = () => { + if (!commonAttributes) { + return null; + } + return Object.keys(commonAttributes).map(teiId => ( + + {headers.map(({ attributeId }) => ( + {commonAttributes[teiId][attributeId].value} + + ))} + + )); + }; + + return ( + + + {renderHeader()} + + + {renderRelationshipRows()} + + + ); +}; + + +export const RelationshipTable: ComponentType = withStyles(styles)(RelationshipTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js new file mode 100644 index 0000000000..444fc01ba0 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js @@ -0,0 +1,5 @@ +import { RelationshipTable } from './RelationshipTable.component'; + +export { + RelationshipTable, +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js new file mode 100644 index 0000000000..7a2d5cb6c7 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -0,0 +1,43 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { spacersNum, spacers, colors } from '@dhis2/ui'; +import { RelationshipTable } from './RelationshipTable'; + +type Props = { + teiRelationship: Object, + ...CssClasses, +} + +const styles = { + container: { + padding: `${spacers.dp8} ${spacers.dp16}`, + }, + title: { + fontWeight: 500, + fontSize: 16, + color: colors.grey800, + paddingBottom: spacersNum.dp8, + }, + wrapper: { + paddingBottom: spacersNum.dp16, + }, +}; +const RelationshipsPlain = ({ teiRelationship, classes }: Props) => ( +
+ { + teiRelationship ? teiRelationship.map((relationship) => { + const { relationshipName, from, to } = relationship; + return (
+
{relationshipName}
+ +
); + }) : null + } +
+); + +export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js new file mode 100644 index 0000000000..f158af4269 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js @@ -0,0 +1,5 @@ +import { Relationships } from './Relationships.component'; + +export { + Relationships, +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index 22bb7de8c8..a474090adc 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -1,25 +1,34 @@ // @flow import React, { useState, useCallback } from 'react'; import i18n from '@dhis2/d2-i18n'; +import { Chip } from '@dhis2/ui'; import { Widget } from '../Widget'; import { useTeiRelationship } from './hooks/useTeiRelationship'; import type { Props } from './widgetRelationship.types'; +import { Relationships } from './Relationships/'; export const WidgetRelationship = ({ teiId }: Props) => { const [open, setOpenStatus] = useState(true); const { teiRelationship } = useTeiRelationship(teiId); - console.log({ teiRelationship }); + return (
+ {i18n.t("TEI's Relationships")} + {teiRelationship && + {teiRelationship.length} + + } +
+ } onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - Hello world +
); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js new file mode 100644 index 0000000000..811a21351f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js @@ -0,0 +1,22 @@ +// @flow +import { useMemo } from 'react'; + +export const useComputeTEI = (from: Object, to: Object) => { + const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; + const { attributes: toAttributes, trackedEntityInstance: toTeiId } = to.trackedEntityInstance; + const { headers, commonAttributes } = useMemo(() => fromAttributes + .reduce((acc, currentAttr) => { + const attributeId = currentAttr.attribute; + const toAttr = toAttributes.find(item => item.attribute === attributeId); + if (toAttr) { + acc.commonAttributes[toTeiId] = { ...acc.commonAttributes[toTeiId] ?? [], [attributeId]: toAttr }; + acc.commonAttributes[fromTeiId] = { + ...acc.commonAttributes[fromTeiId] ?? [], [attributeId]: currentAttr, + }; + acc.headers.push({ attributeId, displayName: currentAttr.displayName }); + } + return acc; + }, { commonAttributes: {}, headers: [] }), [fromAttributes, toAttributes, fromTeiId, toTeiId]); + + return { headers, commonAttributes }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js index efacae8f9e..07b6ce267c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; -export const useTeiRelationship = (teiId) => { +export const useTeiRelationship = (teiId: string) => { const teiRelationshipQuery = useMemo(() => ({ teiRelationship: { resource: 'relationships', diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js index 06000fe42d..d41c53d79d 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -2,5 +2,6 @@ export type Props = {| - teiId: string + teiId: string, + ...CssClasses, |}; From c85b9d099808e33ac8f9341a3047ad49b9649f3d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 26 Jan 2022 10:30:40 +0000 Subject: [PATCH 04/95] feat: [DHIS2-12362] compute tei data --- .../RelationshipTable.component.js | 21 ++++---- .../Relationships/Relationships.component.js | 20 ++++--- .../WidgetRelationship.component.js | 1 - .../WidgetRelationship/hooks/useComputeTEI.js | 53 +++++++++++++------ 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js index e53b7fff7f..97a3fe155c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js @@ -9,20 +9,17 @@ import { DataTableCell, DataTableColumnHeader, } from '@dhis2/ui'; -import { useComputeTEI } from '../../hooks/useComputeTEI'; type Props = { - from: Object, - to: Object, + headers: Array, + relationshipAttributes: Array, ...CssClasses, } const styles = { }; const RelationshipTablePlain = (props: Props) => { - const { classes, from, to } = props; - const { headers, commonAttributes } = useComputeTEI(from, to); - + const { classes, headers, relationshipAttributes } = props; function renderHeader() { const headerCells = headers .map(column => ( @@ -30,7 +27,7 @@ const RelationshipTablePlain = (props: Props) => { key={column.id} name={column.id} > - {column.displayName} + {column.label} )); return ( @@ -42,13 +39,13 @@ const RelationshipTablePlain = (props: Props) => { ); } const renderRelationshipRows = () => { - if (!commonAttributes) { + if (!relationshipAttributes) { return null; } - return Object.keys(commonAttributes).map(teiId => ( - - {headers.map(({ attributeId }) => ( - {commonAttributes[teiId][attributeId].value} + return relationshipAttributes.map(({ id: teiId, attributes }) => ( + + {headers.map(({ id }) => ( + {attributes.find(att => att.attribute === id).value} ))} diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index 7a2d5cb6c7..736db69526 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -3,9 +3,11 @@ import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import { RelationshipTable } from './RelationshipTable'; +import { useComputeTEIRelationship } from '../hooks/useComputeTEI'; type Props = { teiRelationship: Object, + teiId: string, ...CssClasses, } @@ -23,21 +25,23 @@ const styles = { paddingBottom: spacersNum.dp16, }, }; -const RelationshipsPlain = ({ teiRelationship, classes }: Props) => ( -
{ + const { relationshipsByType, headersByType } = useComputeTEIRelationship(teiId, teiRelationship); + + return (
{ - teiRelationship ? teiRelationship.map((relationship) => { - const { relationshipName, from, to } = relationship; - return (
+ relationshipsByType ? relationshipsByType.map((relationship) => { + const { relationshipName, id, ...passOnProps } = relationship; + return (
{relationshipName}
- +
); }) : null } -
-); +
); +}; export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index a474090adc..20823aa399 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -10,7 +10,6 @@ import { Relationships } from './Relationships/'; export const WidgetRelationship = ({ teiId }: Props) => { const [open, setOpenStatus] = useState(true); const { teiRelationship } = useTeiRelationship(teiId); - return (
{ + +const getRelationshipAttributes = (bidirectional: boolean, teiId: string, from: Object, to: Object) => { const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; const { attributes: toAttributes, trackedEntityInstance: toTeiId } = to.trackedEntityInstance; - const { headers, commonAttributes } = useMemo(() => fromAttributes - .reduce((acc, currentAttr) => { - const attributeId = currentAttr.attribute; - const toAttr = toAttributes.find(item => item.attribute === attributeId); - if (toAttr) { - acc.commonAttributes[toTeiId] = { ...acc.commonAttributes[toTeiId] ?? [], [attributeId]: toAttr }; - acc.commonAttributes[fromTeiId] = { - ...acc.commonAttributes[fromTeiId] ?? [], [attributeId]: currentAttr, - }; - acc.headers.push({ attributeId, displayName: currentAttr.displayName }); - } - return acc; - }, { commonAttributes: {}, headers: [] }), [fromAttributes, toAttributes, fromTeiId, toTeiId]); - - return { headers, commonAttributes }; + + if (!bidirectional) { return { id: toTeiId, attributes: toAttributes }; } + + return fromTeiId !== teiId + ? { id: fromTeiId, attributes: fromAttributes } + : { id: toTeiId, attributes: toAttributes }; +}; + +export const useComputeTEIRelationship = (teiId: string, teiRelationships: Array) => { + const relationshipsByType = useMemo(() => teiRelationships && teiRelationships.reduce((acc, currentRelationship) => { + const { relationshipType: typeId, relationshipName, bidirectional, from, to } = currentRelationship; + const typeExist = acc.find(item => item.id === typeId); + const relationshipAttributes = getRelationshipAttributes(bidirectional, teiId, from, to); + + if (typeExist) { + typeExist.relationshipAttributes.push(relationshipAttributes); + } else { + acc.push({ + id: typeId, + relationshipName, + relationshipAttributes: [relationshipAttributes], + }); + } + return acc; + }, []), [teiId, teiRelationships]); + const headersByType = relationshipsByType.reduce((acc, { id, relationshipAttributes }) => { + acc[id] = relationshipAttributes.reduce((accAttr, { attributes }) => { + accAttr.push(attributes.map(item => ({ id: item.attribute, label: item.displayName }))); + return accAttr; + }, []).reduce((p, current) => p.filter(e => current.find(item => item.id === e.id))); + + return acc; + }, []); + + return { relationshipsByType, headersByType }; }; From 11a2bf7b7cec4974bff906526bcdeb67b936fdb2 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 27 Jan 2022 09:58:08 +0000 Subject: [PATCH 05/95] feat: [DHIS2-12362] add relationship in redux --- i18n/en.pot | 10 ++--- .../EnrollmentPageDefault.component.js | 3 +- .../EnrollmentPageDefault.container.js | 6 ++- .../EnrollmentPageDefault.types.js | 6 ++- .../EnrollmentPageDefault/hooks/index.js | 1 + .../hooks/useComputeTEIRelationships.js} | 18 +++++---- .../enrollment.actions.js | 4 +- .../useCommonEnrollmentDomainData.js | 34 +++++++++++++++-- .../useCommonEnrollmentDomainData.types.js | 30 +++++++++++++++ .../Relationships/Relationships.component.js | 37 ++++++++----------- .../WidgetRelationship.component.js | 14 +++---- .../hooks/useTeiRelationship.js | 21 ----------- .../widgetRelationship.types.js | 3 +- .../enrollmentDomain.reducerDescription.js | 3 +- 14 files changed, 116 insertions(+), 74 deletions(-) rename src/core_modules/capture-core/components/{WidgetRelationship/hooks/useComputeTEI.js => Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js} (66%) delete mode 100644 src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js diff --git a/i18n/en.pot b/i18n/en.pot index 3b9be1e06c..95766b0d65 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-01-21T07:58:05.383Z\n" -"PO-Revision-Date: 2022-01-21T07:58:05.383Z\n" +"POT-Creation-Date: 2022-01-27T08:34:18.846Z\n" +"PO-Revision-Date: 2022-01-27T08:34:18.846Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -607,6 +607,9 @@ msgstr "Enrollment with id \"{{enrollmentId}}\" does not exist" msgid "Enrollment Dashboard" msgstr "Enrollment Dashboard" +msgid "TEI's Relationships" +msgstr "TEI's Relationships" + msgid "No indicator output for this enrollment yet" msgstr "No indicator output for this enrollment yet" @@ -1097,9 +1100,6 @@ msgstr "Profile widget could not be loaded. Please try again later" msgid "Person Profile" msgstr "Person Profile" -msgid "TEI's Relationships" -msgstr "TEI's Relationships" - msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 89a9941eb4..dae692f32b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -48,6 +48,7 @@ export const EnrollmentPageDefaultPlain = ({ teiId, events, enrollmentId, + relationships, stages, onDelete, onViewAll, @@ -77,7 +78,7 @@ export const EnrollmentPageDefaultPlain = ({ - + {!hideWidgets.indicator && ( { error: enrollmentsError, enrollment, attributeValues, + relationships, } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - + const { relationshipsByType, headersByType } = useComputeTEIRelationships(teiId, relationships); + console.log({ relationshipsByType, headersByType }); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( { programMetaDataError, enrollmentsError }, @@ -91,6 +94,7 @@ export const EnrollmentPageDefault = () => { stages={stages} events={enrollment?.events} enrollmentId={enrollmentId} + relationships={{ relationshipsByType, headersByType }} onDelete={onDelete} onViewAll={onViewAll} onCreateNew={onCreateNew} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 6011535065..39c505da7e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,7 +2,7 @@ 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'; -import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event, OutputRelationship } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type Props = {| program: Program, @@ -10,6 +10,10 @@ export type Props = {| teiId: string, events: ?Array, stages?: Array, + relationships?: ?{ + relationshipsByType: Array, + headersByType: { [key: string]: Array<{id: string, label: string}> } + }, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, onDelete: () => void, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js index c5815fb402..9226d8acae 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js @@ -5,4 +5,5 @@ export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; export { useProgramStages } from './useProgramStages'; export { useOrganisationUnit } from './useOrganisationUnit'; export { useRuleEffects } from './useRuleEffects'; +export { useComputeTEIRelationships } from './useComputeTEIRelationships'; export type { UseRuleEffectsInput } from './useRuleEffects.types'; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js similarity index 66% rename from src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js rename to src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 9bc3bcbde6..5a1362b1b4 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useComputeTEI.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -1,8 +1,8 @@ // @flow import { useMemo } from 'react'; +import { type TEIData, type TEIRelationship } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; - -const getRelationshipAttributes = (bidirectional: boolean, teiId: string, from: Object, to: Object) => { +const getRelationshipAttributes = (bidirectional: boolean, teiId: string, from: TEIData, to: TEIData) => { const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; const { attributes: toAttributes, trackedEntityInstance: toTeiId } = to.trackedEntityInstance; @@ -13,8 +13,9 @@ const getRelationshipAttributes = (bidirectional: boolean, teiId: string, from: : { id: toTeiId, attributes: toAttributes }; }; -export const useComputeTEIRelationship = (teiId: string, teiRelationships: Array) => { - const relationshipsByType = useMemo(() => teiRelationships && teiRelationships.reduce((acc, currentRelationship) => { +export const useComputeTEIRelationships = (teiId: string, relationships?: ?{[key: string]: Array}) => { + const relationshipsByType = useMemo(() => relationships && + relationships[teiId].reduce((acc, currentRelationship) => { const { relationshipType: typeId, relationshipName, bidirectional, from, to } = currentRelationship; const typeExist = acc.find(item => item.id === typeId); const relationshipAttributes = getRelationshipAttributes(bidirectional, teiId, from, to); @@ -29,15 +30,16 @@ export const useComputeTEIRelationship = (teiId: string, teiRelationships: Array }); } return acc; - }, []), [teiId, teiRelationships]); - const headersByType = relationshipsByType.reduce((acc, { id, relationshipAttributes }) => { + }, []), [teiId, relationships]); + const headersByType = useMemo(() => relationshipsByType && + relationshipsByType.reduce((acc, { id, relationshipAttributes }) => { acc[id] = relationshipAttributes.reduce((accAttr, { attributes }) => { accAttr.push(attributes.map(item => ({ id: item.attribute, label: item.displayName }))); return accAttr; }, []).reduce((p, current) => p.filter(e => current.find(item => item.id === e.id))); return acc; - }, []); + }, {}), [relationshipsByType]); - return { relationshipsByType, headersByType }; + return { relationshipsByType: relationshipsByType ?? [], headersByType: headersByType ?? {} }; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js index 696e25ebf5..e42802a82d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js @@ -13,8 +13,8 @@ export const enrollmentSiteActionTypes = { SAVE_FAILED: 'Enrollment.SaveFailed', }; -export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues) => - actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues }); +export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues, relationships: Object) => + actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues, relationships }); export const updateEnrollmentEvents = (eventId: string, eventData: Object) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENTS)({ diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index 89edd78a00..4fc851ff61 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -1,5 +1,5 @@ // @flow -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; // $FlowFixMe import { useSelector, useDispatch } from 'react-redux'; import { useDataQuery } from '@dhis2/app-runtime'; @@ -13,6 +13,7 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin enrollmentId: storedEnrollmentId, enrollment: storedEnrollment, attributeValues: storedAttributeValues, + relationships: storedRelationships, } = useSelector(({ enrollmentDomain }) => enrollmentDomain); const { data, error, refetch } = useDataQuery({ @@ -28,11 +29,31 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin lazy: true, }); + const { + data: relationshipsData, + error: relationshipsError, + refetch: refetchRelationships, + } = useDataQuery( + useMemo( + () => ({ + teiRelationships: { + resource: 'relationships', + params: ({ variables: { teiId: updatedTeiId } }) => ({ + tei: updatedTeiId, + }), + }, + }), + [], + ), + { lazy: true }, + ); + const fetchedEnrollmentData = { reference: data, enrollment: data?.trackedEntityInstance?.enrollments ?.find(enrollment => enrollment.enrollment === enrollmentId), attributeValues: data?.trackedEntityInstance?.attributes, + relationships: relationshipsData?.teiRelationships, }; useEffect(() => { @@ -41,6 +62,7 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues .map(({ attribute, value }) => ({ id: attribute, value })), + { [teiId]: fetchedEnrollmentData.relationships }, )); } }, [ @@ -48,21 +70,25 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.reference, fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues, + fetchedEnrollmentData.relationships, + teiId, ]); useEffect(() => { if (storedEnrollmentId !== enrollmentId) { refetch({ variables: { teiId, programId } }); + refetchRelationships({ variables: { teiId } }); } - }, [refetch, storedEnrollmentId, enrollmentId, teiId, programId]); + }, [refetch, refetchRelationships, storedEnrollmentId, enrollmentId, teiId, programId]); const inEffectData = enrollmentId === storedEnrollmentId ? { enrollment: storedEnrollment, attributeValues: storedAttributeValues, - } : { enrollment: undefined, attributeValues: undefined }; + relationships: storedRelationships, + } : { enrollment: undefined, attributeValues: undefined, relationships: undefined }; return { - error, + error: error || relationshipsError, ...inEffectData, }; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index aaf16e603d..46e6ec73ee 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -48,8 +48,38 @@ export type AttributeValue = {| value: string, |}; +export type TEIAttribute = {| + attribute: string, + displayName: string, + value: string, + valueType: string, +|} + +export type TEIData = {| + trackedEntityInstance: { + trackedEntityInstance: string, + attributes: Array + } +|} + +export type TEIRelationship = {| + relationshipType: string, + relationshipName: string, + relationship: string, + bidirectional: boolean, + from: TEIData, + to: TEIData +|} + +export type OutputRelationship = { + id: string, + relationshipName: string, + relationshipAttributes: Array<{ id: string, attributes: Array}> +} + export type Output = {| error?: any, enrollment?: EnrollmentData, attributeValues?: Array, + relationships?: {[key: string]: Array} |}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index 736db69526..c1aa09d80a 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -3,11 +3,10 @@ import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import { RelationshipTable } from './RelationshipTable'; -import { useComputeTEIRelationship } from '../hooks/useComputeTEI'; type Props = { - teiRelationship: Object, - teiId: string, + relationshipsByType: Object, + headersByType: Object, ...CssClasses, } @@ -25,23 +24,19 @@ const styles = { paddingBottom: spacersNum.dp16, }, }; -const RelationshipsPlain = ({ teiId, teiRelationship, classes }: Props) => { - const { relationshipsByType, headersByType } = useComputeTEIRelationship(teiId, teiRelationship); - - return (
- { - relationshipsByType ? relationshipsByType.map((relationship) => { - const { relationshipName, id, ...passOnProps } = relationship; - return (
-
{relationshipName}
- -
); - }) : null - } -
); -}; +const RelationshipsPlain = ({ relationshipsByType, headersByType, classes }: Props) => (
+ { + relationshipsByType ? relationshipsByType.map((relationship) => { + const { relationshipName, id, ...passOnProps } = relationship; + return (
+
{relationshipName}
+ +
); + }) : null + } +
); export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index 20823aa399..8f68a711cd 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -1,24 +1,22 @@ // @flow import React, { useState, useCallback } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { Chip } from '@dhis2/ui'; import { Widget } from '../Widget'; -import { useTeiRelationship } from './hooks/useTeiRelationship'; import type { Props } from './widgetRelationship.types'; import { Relationships } from './Relationships/'; -export const WidgetRelationship = ({ teiId }: Props) => { +export const WidgetRelationship = ({ relationships, title }: Props) => { const [open, setOpenStatus] = useState(true); - const { teiRelationship } = useTeiRelationship(teiId); + return (
- {i18n.t("TEI's Relationships")} - {teiRelationship && - {teiRelationship.length} + {title} + {relationships && + {relationships.relationshipsByType.length} }
@@ -27,7 +25,7 @@ export const WidgetRelationship = ({ teiId }: Props) => { onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - + ); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js b/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js deleted file mode 100644 index 07b6ce267c..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationship/hooks/useTeiRelationship.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { useMemo } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; - -export const useTeiRelationship = (teiId: string) => { - const teiRelationshipQuery = useMemo(() => ({ - teiRelationship: { - resource: 'relationships', - params: { - tei: teiId, - }, - }, - }), [teiId]); - const { - loading, - data, - error, - } = useDataQuery(teiRelationshipQuery); - - return { error, teiRelationship: !loading && data?.teiRelationship }; -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js index d41c53d79d..897c06461d 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -2,6 +2,7 @@ export type Props = {| - teiId: string, + relationships: Object, + title: string, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js index ce2aafd4c0..8636d53648 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -17,11 +17,12 @@ const { export const enrollmentDomainDesc = createReducerDescription( { - [COMMON_ENROLLMENT_SITE_DATA_SET]: (state, { payload: { enrollment, attributeValues } }) => ({ + [COMMON_ENROLLMENT_SITE_DATA_SET]: (state, { payload: { enrollment, attributeValues, relationships } }) => ({ ...state, enrollment, attributeValues, enrollmentId: enrollment?.enrollment, + relationships, }), [UPDATE_ENROLLMENT_EVENTS]: ( state, From 9362c1a9693392d22cab047ffef9e23b34d0cba9 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 27 Jan 2022 10:30:15 +0000 Subject: [PATCH 06/95] feat: [DHIS2-12362] minor change --- .../EnrollmentPageDefault/EnrollmentPageDefault.container.js | 2 +- .../EnrollmentPageDefault/hooks/useComputeTEIRelationships.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 3af2db0496..9317dc5feb 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -48,7 +48,7 @@ export const EnrollmentPageDefault = () => { const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); const { relationshipsByType, headersByType } = useComputeTEIRelationships(teiId, relationships); - console.log({ relationshipsByType, headersByType }); + if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( { programMetaDataError, enrollmentsError }, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 5a1362b1b4..7c07649b38 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -31,6 +31,7 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: ?{[key } return acc; }, []), [teiId, relationships]); + // this will change after https://jira.dhis2.org/browse/DHIS2-12249 is done const headersByType = useMemo(() => relationshipsByType && relationshipsByType.reduce((acc, { id, relationshipAttributes }) => { acc[id] = relationshipAttributes.reduce((accAttr, { attributes }) => { From addbe06d94bc3859120e84a78900c6dd15ddab91 Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Mon, 31 Jan 2022 20:19:48 +0100 Subject: [PATCH 07/95] feat: first draft to new TEI relationshiphandler --- i18n/en.pot | 10 +- .../MetaDataStoreUtils/MetaDataStoreUtils.js | 8 ++ .../EnrollmentPageDefault.component.js | 15 ++- .../EnrollmentPageDefault.types.js | 2 +- .../Breadcrumbs/Breadcrumbs.js | 7 + .../NewTrackedEntityRelationship.component.js | 44 ++----- .../NewTrackedEntityRelationship.container.js | 75 +++++++---- .../NewTrackedEntityRelationship.types.js | 4 + .../RelationshipTypeSelector.js | 120 ++++++++++++++++++ .../WidgetTrackedEntityRelationship.js | 15 +-- .../WidgetTrackedEntityRelationship.types.js | 22 ++++ .../WidgetTrackedEntityRelationsip.types.js | 8 -- .../WidgetTrackedEntityRelationship/hooks.js | 70 ++++++++-- .../RelationshipType/RelationshipType.js | 18 +++ .../storeRelationshipTypes.js | 2 +- .../cachedData/useProgramFromIndexedDB.js | 0 16 files changed, 325 insertions(+), 95 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js delete mode 100644 src/core_modules/capture-core/utils/cachedData/useProgramFromIndexedDB.js diff --git a/i18n/en.pot b/i18n/en.pot index c0939b0789..d7bbcb959a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-01-30T14:59:03.316Z\n" -"PO-Revision-Date: 2022-01-30T14:59:03.316Z\n" +"POT-Creation-Date: 2022-01-31T19:19:52.073Z\n" +"PO-Revision-Date: 2022-01-31T19:19:52.073Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1160,6 +1160,9 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "New TEI Relationship" +msgstr "New TEI Relationship" + msgid "Link to an existing person" msgstr "Link to an existing person" @@ -1169,9 +1172,6 @@ msgstr "Create new" msgid "An error occurred" msgstr "An error occurred" -msgid "New TEI - Relationship handler" -msgstr "New TEI - Relationship handler" - msgid "New Relationship" msgstr "New Relationship" diff --git a/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js b/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js index 5848dd91cf..c7162de1e6 100644 --- a/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js +++ b/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js @@ -11,6 +11,14 @@ export const getCachedSingleResourceFromKeyAsync = ( return storageController.get(store, key).then(response => ({ response, ...propsToPass })); }; +export const getCachedResourceAsync = ( + store: $Values, + propsToPass?: any = {}, +) => { + const storageController = getUserStorageController(); + return storageController.getAll(store).then(response => ({ response, ...propsToPass })); +}; + export const containsKeyInStorageAsync = (store: $Values, key: string, propsToPass?: any = {}) => { const storageController = getUserStorageController(); return storageController.contains(store, key).then(response => ({ response, ...propsToPass })); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 619fe46853..95b56f21a5 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -18,9 +18,11 @@ import { } from '../../../WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship'; const getStyles = ({ typography }) => ({ + container: { + position: 'relative', + }, columns: { display: 'flex', - position: 'relative', }, leftColumn: { flexGrow: 3, @@ -63,9 +65,12 @@ export const EnrollmentPageDefaultPlain = ({ renderRelationshipRef, relationshipTypes, }: PlainProps) => ( - <> +
{i18n.t('Enrollment Dashboard')}
-
+
@@ -108,7 +115,7 @@ export const EnrollmentPageDefaultPlain = ({ />}
- +
); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 65bcf8deb8..efb9ab144e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -3,7 +3,7 @@ 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'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import type { RelationshipType } from '../../../../metaData'; +import type { RelationshipType } from '../../../WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types'; export type Props = {| program: Program, diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js new file mode 100644 index 0000000000..80099c4ab2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js @@ -0,0 +1,7 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; + +export const Breadcrumbs = () => ( +

{i18n.t('New TEI Relationship')}

+); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index a214743897..68fceb71ed 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -1,52 +1,30 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { Button, IconSearch24, IconAdd24, spacers } from '@dhis2/ui'; +import { Button, IconSearch16, IconAdd16, spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; +import { RelationshipTypeSelector } from './RelationshipTypeSelector/RelationshipTypeSelector'; const styles = { container: { padding: spacers.dp16, paddingTop: 0, }, - typeselector: { - display: 'flex', - flexDirection: 'column', - gap: spacers.dp8, - marginBottom: spacers.dp16, - }, creationselector: { display: 'flex', gap: spacers.dp4, }, - selectorButton: { - display: 'block', - }, cancelButton: { marginTop: spacers.dp8, }, }; -const NewTrackedEntityRelationshipComponentPlain = ({ relationshipTypes, onSelectType, onCancel, pageStatus, classes }) => { +const NewTrackedEntityRelationshipComponentPlain = ({ pageStatus, classes, ...PassOnProps }) => { if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { return ( -
-
- {relationshipTypes?.map(relationship => ( -
- -
- ))} -
- -
+ ); } @@ -55,20 +33,14 @@ const NewTrackedEntityRelationshipComponentPlain = ({ relationshipTypes, onSelec
-
); } diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index 27a60ffb86..13cb279ad9 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -1,13 +1,15 @@ // @flow import React, { useCallback, useMemo, useState } from 'react'; import * as ReactDOM from 'react-dom'; -import { colors } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../Widget'; import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; import type { Props } from './NewTrackedEntityRelationship.types'; +import { useFilteredRelationshipTypes } from '../hooks'; +import { LinkButton } from '../../Buttons/LinkButton.component'; +import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'; +import { useLocationQuery } from '../../../utils/routing'; const styles = { container: { @@ -16,17 +18,37 @@ const styles = { left: 0, right: 0, bottom: 0, - backgroundColor: '#fff', - borderRadius: 3, - borderStyle: 'solid', - borderColor: colors.grey400, - borderWidth: 1, + backgroundColor: '#FAFAFA', + }, + bar: { + color: '#494949', + padding: '8px', + display: 'inline-block', + fontSize: '14px', + borderRadius: '4px', + marginBottom: '10px', + backgroundColor: '#E9EEF4', + }, + linkText: { + backgroundColor: 'transparent', + fontSize: 'inherit', + color: 'inherit', }, }; -export const NewTrackedEntityRelationshipPlain = ({ renderRef, showDialog, hideDialog, classes, ...passOnProps }: Props) => { +export const NewTrackedEntityRelationshipPlain = ({ + renderRef, + showDialog, + hideDialog, + relationshipTypes, + trackedEntityType, + classes, + ...passOnProps +}: Props) => { const [selectedRelationshipType, setSelectedRelationshipType] = useState(); const [creationMode, setCreationMode] = useState(); + const { programId } = useLocationQuery(); + const filteredRelationshipTypes = useFilteredRelationshipTypes(relationshipTypes, trackedEntityType, programId); const pageStatus = useMemo(() => { if (!selectedRelationshipType) { @@ -38,7 +60,6 @@ export const NewTrackedEntityRelationshipPlain = ({ renderRef, showDialog, hideD return NewTEIRelationshipStatuses.DEFAULT; }, [creationMode, selectedRelationshipType]); - const onSelectRelationshipType = useCallback( relationshipType => setSelectedRelationshipType(relationshipType), [], ); @@ -54,20 +75,28 @@ export const NewTrackedEntityRelationshipPlain = ({ renderRef, showDialog, hideD } return ReactDOM.createPortal(( -
- {i18n.t('New TEI - Relationship handler')}

} - > - -
-
+ <> +
+
+ + Go back without saving relationship + +
+ } + > + + +
+ ), renderRef.current); }; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index c22faef02b..face528b63 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -1,7 +1,11 @@ // @flow +import type { RelationshipType } from '../WidgetTrackedEntityRelationship.types'; + export type Props = {| renderRef: Object, + relationshipTypes: Array, + trackedEntityType: string, showDialog: boolean, hideDialog: () => void, ...CssClasses, diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js new file mode 100644 index 0000000000..1c5df35fd8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js @@ -0,0 +1,120 @@ +// @flow +import { Button, spacers } from '@dhis2/ui'; +import React, { useMemo } from 'react'; +import { withStyles } from '@material-ui/core'; + +const styles = { + container: { + padding: spacers.dp16, + paddingTop: 0, + }, + typeselector: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp8, + marginBottom: spacers.dp16, + }, + dualButtonContainer: { + display: 'flex', + gap: spacers.dp8, + }, + selectorButton: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp4, + marginBottom: spacers.dp8, + }, + title: { + fontWeight: 500, + marginBottom: spacers.dp4, + }, +}; + +const RelationshipTypeSelectorPlain = ({ relationshipTypes, trackedEntityType, programId, onSelectType, classes }) => useMemo(() => { + const renderButtons = (relationship) => { + const defaultButton = ( + + ); + + const { fromConstraint, toConstraint } = relationship; + if (relationship.bidirectional) { + if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType && relationship.toConstraint.trackedEntityType?.id === trackedEntityType) { + let fromConstraintValid = true; + let toConstraintValid = true; + if (fromConstraint.program) { + if ((fromConstraint.program?.id !== programId) || (fromConstraint?.trackedEntityType.id !== trackedEntityType)) { + fromConstraintValid = false; + } + } + if (toConstraint.program) { + if ((toConstraint.program?.id !== programId) || (toConstraint?.trackedEntityType.id !== trackedEntityType)) { + toConstraintValid = false; + } + } + + return ( +
+ {fromConstraintValid && ( + + )} + {toConstraintValid && ( + + )} +
+ ); + } + if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType) { + return defaultButton; + } + if (relationship.toConstraint.trackedEntityType?.id === trackedEntityType) { + return ( + + ); + } + } + + return defaultButton; + }; + + return ( +
+
+ {relationshipTypes?.map(relationship => ( +
+
+ {relationship.displayName} +
+
+ {renderButtons(relationship)} +
+
+ ))} +
+
+ ); +}, [classes, onSelectType, programId, relationshipTypes, trackedEntityType]); + +export const RelationshipTypeSelector = withStyles(styles)(RelationshipTypeSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js index 6e4e1ab1a7..50876f1ff9 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js @@ -3,23 +3,19 @@ import React, { useCallback, useState } from 'react'; import { Button } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship/NewTrackedEntityRelationship.container'; -import type { Props } from './WidgetTrackedEntityRelationsip.types'; +import type { Props } from './WidgetTrackedEntityRelationship.types'; -export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef }: Props) => { +export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef, trackedEntityType }: Props) => { const [showDialog, setShowDialog] = useState(false); - const onStartNewRelationship = useCallback(() => { - setShowDialog(prevState => !prevState); - }, []); - - const hideDialog = useCallback(() => { + const changeDialogView = useCallback(() => { setShowDialog(prevState => !prevState); }, []); return ( <> @@ -27,8 +23,9 @@ export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef } ); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..b4b5befef7 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -0,0 +1,22 @@ +// @flow + +type Constraint = {| + program?: { id: string }, + trackedEntityType?: { id: string }, +|} + +export type RelationshipType = {| + id: string, + displayName: string, + bidirectional: boolean, + fromToName: string, + toFromName?: string, + fromConstraint: Constraint, + toConstraint: Constraint, +|} + +export type Props = {| + relationshipTypes: ?Array, + renderRef: Object, + trackedEntityType: string, +|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js deleted file mode 100644 index 9fb4ad35d0..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationsip.types.js +++ /dev/null @@ -1,8 +0,0 @@ -// @flow - -import type { RelationshipType } from '../../metaData'; - -export type Props = {| - relationshipTypes: ?Array, - renderRef: Object, -|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js index 626a46a9b6..0574c23d14 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js @@ -1,12 +1,66 @@ -import { useMemo } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; +// @flow +import { useMemo, useRef, useState } from 'react'; +import { + getCachedResourceAsync, +} from '../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { userStores } from '../../storageControllers/stores'; +import type { RelationshipType } from './WidgetTrackedEntityRelationship.types'; export const useRelationshipTypes = () => { - const { data, error, loading } = useDataQuery(useMemo(() => ({ - relationshipTypes: { - resource: 'relationshipTypes', - }, - }), [])); + const [cachedTypes, setCachedTypes] = useState(); + const [loading, setLoading] = useState(true); + const error = useRef(); + useMemo(() => { + getCachedResourceAsync(userStores.RELATIONSHIP_TYPES) + .then((relationshipTypes) => { + setCachedTypes(relationshipTypes?.response); + setLoading(false); + }) + .catch((e) => { + console.error(e); + }); + }, []); - return !loading && !error && data.relationshipTypes; + return { + loading, + error: error.current, + relationshipTypes: cachedTypes, + }; }; + +export const useFilteredRelationshipTypes = (relationshipTypes: Array, trackedEntityType: string, programId: string) => + // $FlowFixMe + useMemo(() => relationshipTypes?.filter((type) => { + const { toConstraint, fromConstraint } = type; + if (type.bidirectional) { + if (toConstraint.trackedEntityType?.id === trackedEntityType || fromConstraint.trackedEntityType?.id === trackedEntityType) { + let fromConstraintValid = true; + let toConstraintValid = true; + if (fromConstraint.program) { + if ((fromConstraint.program?.id !== programId) || (fromConstraint.trackedEntityType?.id !== trackedEntityType)) { + fromConstraintValid = false; + } + } + if (toConstraint.program) { + if ((toConstraint.program?.id !== programId) || (toConstraint.trackedEntityType?.id !== trackedEntityType)) { + toConstraintValid = false; + } + } + + if (fromConstraintValid || toConstraintValid) { + return true; + } + return false; + } + return false; + } + if (fromConstraint.trackedEntityType?.id === trackedEntityType) { + if (fromConstraint.program) { + if (fromConstraint.program.id !== programId) { + return false; + } + } + return true; + } + return false; + }), [relationshipTypes, programId, trackedEntityType]); diff --git a/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js b/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js index 27227868ab..ce6fc5e114 100644 --- a/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js +++ b/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js @@ -13,6 +13,8 @@ type RelationshipConstraint = { export class RelationshipType { _id: string; _name: string; + _fromToName: string; + _toFromName: string; _access: Access; _from: RelationshipConstraint; _to: RelationshipConstraint; @@ -60,4 +62,20 @@ export class RelationshipType { get to(): RelationshipConstraint { return this._to; } + + get fromToName(): string { + return this._fromToName; + } + + set fromToName(value: string) { + this._fromToName = value; + } + + get toFromName(): string { + return this._toFromName; + } + + set toFromName(value: string) { + this._toFromName = value; + } } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js index 571fee90bb..83bd29b0e3 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js @@ -6,7 +6,7 @@ export const storeRelationshipTypes = () => { const query = { resource: 'relationshipTypes', params: { - fields: 'id,displayName,fromConstraint[*],toConstraint[*],access[*]', + fields: 'id,displayName,bidirectional,fromToName,toFromName,fromConstraint[*],toConstraint[*],access[*]', }, }; diff --git a/src/core_modules/capture-core/utils/cachedData/useProgramFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedData/useProgramFromIndexedDB.js deleted file mode 100644 index e69de29bb2..0000000000 From 98445b7174c382ac077e06db5de84d9efa6c1410 Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Thu, 3 Feb 2022 14:07:53 +0100 Subject: [PATCH 08/95] fix: add constraint to state --- .../RelationshipTypeSelector.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js index 1c5df35fd8..90dfd5b358 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js @@ -34,7 +34,10 @@ const RelationshipTypeSelectorPlain = ({ relationshipTypes, trackedEntityType, p const renderButtons = (relationship) => { const defaultButton = ( +
); export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index 8f68a711cd..99177b9b34 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -5,7 +5,7 @@ import { Widget } from '../Widget'; import type { Props } from './widgetRelationship.types'; import { Relationships } from './Relationships/'; -export const WidgetRelationship = ({ relationships, title }: Props) => { +export const WidgetRelationship = ({ relationships, title, onAddRelationship }: Props) => { const [open, setOpenStatus] = useState(true); return ( @@ -25,7 +25,7 @@ export const WidgetRelationship = ({ relationships, title }: Props) => { onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - + ); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js index 897c06461d..90b81c7481 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -4,5 +4,6 @@ export type Props = {| relationships: Object, title: string, + onAddRelationship: void, ...CssClasses, |}; From 2fb56208bb43c76276703918d4aa7551aa0199be Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 7 Feb 2022 19:31:44 +0000 Subject: [PATCH 12/95] fix: [DHIS2-12362] update tet --- .../EnrollmentPageDefault.container.js | 4 +- .../hooks/useComputeTEIRelationships.js | 186 +++++++++++++----- .../useCommonEnrollmentDomainData.js | 2 +- .../RelationshipTable.component.js | 2 +- .../Relationships/Relationships.component.js | 4 +- .../storeRelationshipTypes.js | 2 +- 6 files changed, 149 insertions(+), 51 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 5fd2ecc73c..016a19d2e1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -36,7 +36,7 @@ export const EnrollmentPageDefault = () => { } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - const { relationshipsByType, headersByType } = useComputeTEIRelationships(teiId, relationships); + const { relationshipsByType } = useComputeTEIRelationships(teiId, relationships); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( @@ -85,7 +85,7 @@ export const EnrollmentPageDefault = () => { events={enrollment?.events} enrollmentId={enrollmentId} // $FlowFixMe - relationships={{ relationshipsByType, headersByType }} + relationships={{ relationshipsByType }} onDelete={onDelete} onViewAll={onViewAll} onCreateNew={onCreateNew} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index aa4f954fad..ff8e0b2db3 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -1,58 +1,156 @@ // @flow -import { useMemo } from 'react'; +import { useCallback, useMemo, useEffect, useState } from 'react'; +import { + getCachedSingleResourceFromKeyAsync, +} from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { userStores } from '../../../../../storageControllers/stores'; import type { TEIRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; const getRelationshipAttributes = ( - bidirectional: boolean, + relationshipType: Object, teiId: string, from: RelationshipData, to: RelationshipData, + headers: Array<{id: string, label: string}>, + options: Object, ) => { - if (from.event) { - return { - id: from.event.event, attributes: from.event.dataValues, - }; - } - if (to.trackedEntityInstance) { - const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; - const { attributes: toAttributes, trackedEntityInstance: toTeiId } = to.trackedEntityInstance; - - if (!bidirectional) { return { id: toTeiId, attributes: toAttributes }; } - return fromTeiId !== teiId - ? { id: fromTeiId, attributes: fromAttributes } - : { id: toTeiId, attributes: toAttributes }; - } - return {}; -}; + const { bidirectional } = relationshipType; + const getId = () => { + if (bidirectional) { + return from.event.event; + } + + if (to.trackedEntityInstance.trackedEntityInstance !== teiId) { + return to.trackedEntityInstance.trackedEntityInstance; + } + return teiId; + }; + + const getAttributes = () => { + let returnAttributes = []; + if (bidirectional) { + returnAttributes = headers.map((item) => { + if (item.convertValue) { + return { + id: item.id, + value: item.convertValue(options), + }; + } + const attributeItem = from.event.dataValues.find(({ dataElement }) => dataElement === item.id); + return { + id: item.id, + value: attributeItem.value, + }; + }); + } else if (from.trackedEntityInstance) { + const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; + const { attributes: toAttributes } = to.trackedEntityInstance; -export const useComputeTEIRelationships = (teiId: string, relationships?: ?{[key: string]: Array}) => { - const relationshipsByType = useMemo(() => (relationships?.[teiId]?.reduce((acc, currentRelationship) => { - const { relationshipType: typeId, relationshipName, bidirectional, from, to } = currentRelationship; - const typeExist = acc.find(item => item.id === typeId); - const relationshipAttributes = getRelationshipAttributes(bidirectional, teiId, from, to); - if (typeExist) { - typeExist.relationshipAttributes.push(relationshipAttributes); - } else { - acc.push({ - id: typeId, - relationshipName, - relationshipAttributes: [relationshipAttributes], + if (fromTeiId !== teiId) { + returnAttributes = fromAttributes; + } else { + returnAttributes = toAttributes; + } + returnAttributes = headers.map((item) => { + if (item.convertValue) { + return { + id: item.id, + value: item.convertValue(options), + }; + } + const attributeItem = returnAttributes.find(({ attribute }) => attribute === item.id); + return { + id: item.id, + value: attributeItem.value, + }; }); } - return acc; - }, [])), [teiId, relationships]); - // this will change after https://jira.dhis2.org/browse/DHIS2-12249 is done - const headersByType = useMemo(() => (relationshipsByType?.reduce((acc, { id, relationshipAttributes }) => { - acc[id] = relationshipAttributes.reduce((accAttr, { attributes }) => { - // $FlowFixMe - accAttr.push(attributes.map(item => ({ id: item.attribute ?? item.dataElement, label: item.displayName }))); - return accAttr; - }, []).reduce((p, current) => p.filter(e => current.find(item => item.id === e.id))); - - return acc; - }, {})), [relationshipsByType]); - - return { relationshipsByType: relationshipsByType ?? [], headersByType: headersByType ?? {} }; + + return returnAttributes; + }; + + return { + id: getId(), + attributes: getAttributes(), + }; +}; + +const relationshipEntities = { + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', +}; + +const getDisplayFields = () => [{ id: 'zDhUuAYrxNC', label: 'Last name' }, { id: 'w75KJ2mc4zz', label: 'First name' }]; + +const getBaseConfigHeaders = { + [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ + id: 'tetName', + label: 'TET name', + convertValue: props => props.trackedEntityType?.displayName, + }, { + id: 'createdDate', + label: 'Created date', + convertValue: props => props.created, + }], + [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ + id: 'programStageName', + label: 'Program stage name', + convertValue: props => props.programStage?.name, + }, + { + id: 'createdDate', + label: 'Created date', + convertValue: props => props.created, + }], +}; +export const useComputeTEIRelationships = (teiId: string, relationships?: Array) => { + const [relationshipsByType, setRelationshipByType] = useState([]); + + const computeData = useCallback(async () => { + const groupped = []; + if (!relationships) { return; } + for await (const relationship of relationships) { + const { relationshipType: typeId, from, to, created } = relationship; + + const typeExist = groupped.find(item => item.id === typeId); + const { + bidirectional, + toFromName, + fromToName, + fromConstraint, + } = await getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, typeId) + .then(result => result.response); + + let displayFields = getDisplayFields(); + if (!displayFields.length) { + displayFields = getBaseConfigHeaders[fromConstraint.relationshipEntity]; + } + + console.log({ displayFields }); + const relationshipAttributes = getRelationshipAttributes( + bidirectional, teiId, from, to, displayFields, { created }, + ); + if (typeExist) { + typeExist.relationshipAttributes.push(relationshipAttributes); + } else { + groupped.push({ + id: typeId, + relationshipName: bidirectional ? toFromName : fromToName, + relationshipAttributes: [relationshipAttributes], + headers: displayFields, + }); + } + } + + + setRelationshipByType(groupped); + }, [relationships, teiId]); + + useEffect(() => { + computeData(); + }, [computeData]); + + return { relationshipsByType }; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index 4a2ee7f2c8..ed95c4f984 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -66,7 +66,7 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues .map(({ attribute, value }) => ({ id: attribute, value })), - { [teiId]: fetchedEnrollmentData.relationships }, + fetchedEnrollmentData.relationships, )); } }, [ diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js index 6b9dff423e..badf59fb2c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js @@ -40,7 +40,7 @@ export const RelationshipTable = (props: Props) => { return relationshipAttributes.map(({ id: teiId, attributes }) => ( {headers.map(({ id }) => { - const attribute = attributes.find(att => att.attribute === id || att.dataElement === id); + const attribute = attributes.find(att => att.id === id); return ( {attribute?.value} diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index 963bd564ae..3d2bce5564 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -27,7 +27,7 @@ const styles = { overflow: 'scroll', }, }; -const RelationshipsPlain = ({ relationshipsByType, headersByType, classes, onAddRelationship }: Props) => ( +const RelationshipsPlain = ({ relationshipsByType, classes, onAddRelationship }: Props) => (
{relationshipName}
- +
); }) : null } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js index 571fee90bb..83bd29b0e3 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js @@ -6,7 +6,7 @@ export const storeRelationshipTypes = () => { const query = { resource: 'relationshipTypes', params: { - fields: 'id,displayName,fromConstraint[*],toConstraint[*],access[*]', + fields: 'id,displayName,bidirectional,fromToName,toFromName,fromConstraint[*],toConstraint[*],access[*]', }, }; From 279f63922c0fbec15f7ca7b1169439f0233be908 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 08:39:45 +0000 Subject: [PATCH 13/95] chore: [DHIS2-12362] cleanup fromconstraint --- .../hooks/useComputeTEIRelationships.js | 162 +++++++++--------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index ff8e0b2db3..9e1d2aaedc 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -1,5 +1,6 @@ // @flow -import { useCallback, useMemo, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import moment from 'moment'; import { getCachedSingleResourceFromKeyAsync, } from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; @@ -13,68 +14,70 @@ const getRelationshipAttributes = ( teiId: string, from: RelationshipData, to: RelationshipData, - headers: Array<{id: string, label: string}>, - options: Object, + relationship: Object, ) => { - const { bidirectional } = relationshipType; - const getId = () => { - if (bidirectional) { - return from.event.event; + const { bidirectional, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; + + const getAttributes = (attributes, displayFields, options) => displayFields.map((item) => { + if (item.convertValue) { + return { + id: item.id, + value: item.convertValue(options), + }; } - - if (to.trackedEntityInstance.trackedEntityInstance !== teiId) { - return to.trackedEntityInstance.trackedEntityInstance; + const attributeItem = Array.isArray(attributes) + ? attributes.find(({ attribute }) => attribute === item.id)?.value : attributes[item.id]; + return { + id: item.id, + value: attributeItem, + }; + }); + + const getDisplayFields = (type) => { + let displayFields = getDisplayFieldsFromAPI[type]; + if (!displayFields.length) { + displayFields = getBaseConfigHeaders[type]; } - return teiId; - }; - - const getAttributes = () => { - let returnAttributes = []; - if (bidirectional) { - returnAttributes = headers.map((item) => { - if (item.convertValue) { - return { - id: item.id, - value: item.convertValue(options), - }; - } - const attributeItem = from.event.dataValues.find(({ dataElement }) => dataElement === item.id); - return { - id: item.id, - value: attributeItem.value, - }; - }); - } else if (from.trackedEntityInstance) { - const { attributes: fromAttributes, trackedEntityInstance: fromTeiId } = from.trackedEntityInstance; - const { attributes: toAttributes } = to.trackedEntityInstance; - - if (fromTeiId !== teiId) { - returnAttributes = fromAttributes; - } else { - returnAttributes = toAttributes; - } - returnAttributes = headers.map((item) => { - if (item.convertValue) { - return { - id: item.id, - value: item.convertValue(options), - }; - } - const attributeItem = returnAttributes.find(({ attribute }) => attribute === item.id); - return { - id: item.id, - value: attributeItem.value, - }; - }); - } - - return returnAttributes; + return displayFields; }; - return { - id: getId(), - attributes: getAttributes(), - }; + if (to?.trackedEntityInstance && to?.trackedEntityInstance?.trackedEntityInstance !== teiId) { + const displayFields = getDisplayFields(toConstraint.relationshipEntity); + const attributes = getAttributes(to.trackedEntityInstance.attributes, displayFields, { ...relationship }); + + return { + id: to.trackedEntityInstance.trackedEntityInstance, + relationshipName: fromToName, + relationshipProgram: toConstraint.program, + attributes, + displayFields, + }; + } else if (bidirectional && from?.trackedEntityInstance && + from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { + const displayFields = getDisplayFields(fromConstraint.relationshipEntity); + const attributes = getAttributes(from.trackedEntityInstance.attributes, displayFields, { ...relationship }); + + return { + id: from.trackedEntityInstance.trackedEntityInstance, + relationshipName: toFromName, + relationshipProgram: fromConstraint.program, + attributes, + displayFields, + }; + } else if (bidirectional && from?.event?.event) { + const displayFields = getDisplayFields(fromConstraint.relationshipEntity); + const attributes = getAttributes(from.event, displayFields, { ...from.event }); + + return { + id: from.event.event, + relationshipName: toFromName, + relationshipProgram: fromConstraint.program, + attributes, + displayFields, + }; + } + + return {}; }; const relationshipEntities = { @@ -82,7 +85,21 @@ const relationshipEntities = { PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', }; -const getDisplayFields = () => [{ id: 'zDhUuAYrxNC', label: 'Last name' }, { id: 'w75KJ2mc4zz', label: 'First name' }]; +const getDisplayFieldsFromAPI = { + [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [ + { id: 'w75KJ2mc4zz', label: 'First name' }, + { id: 'zDhUuAYrxNC', label: 'Last name' }, + ], + [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [ + { id: 'orgUnitName', label: 'Organisation unit' }, + { id: 'program', label: 'Program' }, + { id: 'eventDate', + label: 'Event date', + convertValue: props => moment(props.eventDate).format('YYYY-MM-DD'), + }, + { id: 'status', label: 'Status' }, + ], +}; const getBaseConfigHeaders = { [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ @@ -92,7 +109,7 @@ const getBaseConfigHeaders = { }, { id: 'createdDate', label: 'Created date', - convertValue: props => props.created, + convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', @@ -102,9 +119,10 @@ const getBaseConfigHeaders = { { id: 'createdDate', label: 'Created date', - convertValue: props => props.created, + convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], }; + export const useComputeTEIRelationships = (teiId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); @@ -112,39 +130,27 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: Array< const groupped = []; if (!relationships) { return; } for await (const relationship of relationships) { - const { relationshipType: typeId, from, to, created } = relationship; + const { relationshipType: typeId, from, to } = relationship; const typeExist = groupped.find(item => item.id === typeId); - const { - bidirectional, - toFromName, - fromToName, - fromConstraint, - } = await getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, typeId) + const relationshipType = await getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, typeId) .then(result => result.response); - let displayFields = getDisplayFields(); - if (!displayFields.length) { - displayFields = getBaseConfigHeaders[fromConstraint.relationshipEntity]; - } - - console.log({ displayFields }); - const relationshipAttributes = getRelationshipAttributes( - bidirectional, teiId, from, to, displayFields, { created }, + const { relationshipName, displayFields, ...relationshipAttributes } = getRelationshipAttributes( + relationshipType, teiId, from, to, { relationship }, ); if (typeExist) { typeExist.relationshipAttributes.push(relationshipAttributes); } else { groupped.push({ id: typeId, - relationshipName: bidirectional ? toFromName : fromToName, + relationshipName, relationshipAttributes: [relationshipAttributes], headers: displayFields, }); } } - setRelationshipByType(groupped); }, [relationships, teiId]); From d8a1e89195c92323a5ecf0994cd840ed8cf4fe2c Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 10:17:31 +0000 Subject: [PATCH 14/95] fix: [DHIS2-12362] fix flow --- .../EnrollmentPageDefault/hooks/useComputeTEIRelationships.js | 3 ++- .../useCommonEnrollmentDomainData.types.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 9e1d2aaedc..507999fa73 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -64,7 +64,7 @@ const getRelationshipAttributes = ( attributes, displayFields, }; - } else if (bidirectional && from?.event?.event) { + } else if (bidirectional && from?.event) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); const attributes = getAttributes(from.event, displayFields, { ...from.event }); @@ -129,6 +129,7 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: Array< const computeData = useCallback(async () => { const groupped = []; if (!relationships) { return; } + // $FlowFixMe for await (const relationship of relationships) { const { relationshipType: typeId, from, to } = relationship; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 928908f2de..9346d7777e 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -87,5 +87,5 @@ export type Output = {| error?: any, enrollment?: EnrollmentData, attributeValues?: Array, - relationships?: {[key: string]: Array} + relationships?: Array |}; From 6ff4edbb5d93d22bd1476bbe3735845a4194e874 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 13:25:07 +0000 Subject: [PATCH 15/95] fix: [DHIS2-12362] fix flow --- .../EnrollmentPageDefault.container.js | 6 ++++-- .../EnrollmentPageDefault/EnrollmentPageDefault.types.js | 6 +++--- .../hooks/useComputeTEIRelationships.js | 6 +++--- .../WidgetRelationship/WidgetRelationship.component.js | 2 +- .../WidgetRelationship/widgetRelationship.types.js | 9 ++++++--- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 016a19d2e1..44d1217d0e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -84,8 +84,10 @@ export const EnrollmentPageDefault = () => { stages={stages} events={enrollment?.events} enrollmentId={enrollmentId} - // $FlowFixMe - relationships={{ relationshipsByType }} + relationships={{ + relationshipsByType, + count: relationships && relationships.length, + }} onDelete={onDelete} onViewAll={onViewAll} onCreateNew={onCreateNew} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 411a42fad1..c54a5ff030 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -10,9 +10,9 @@ export type Props = {| teiId: string, events: ?Array, stages?: Array, - relationships?: ?{ - relationshipsByType: Array, - headersByType: { [key: string]: Array<{id: string, label: string}> } + relationships: ?{ + relationshipsByType?: ?Array, + count?: ?number, }, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 507999fa73..1dfd1d008c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -137,16 +137,16 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: Array< const relationshipType = await getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, typeId) .then(result => result.response); - const { relationshipName, displayFields, ...relationshipAttributes } = getRelationshipAttributes( + const { relationshipName, displayFields, id, attributes } = getRelationshipAttributes( relationshipType, teiId, from, to, { relationship }, ); if (typeExist) { - typeExist.relationshipAttributes.push(relationshipAttributes); + typeExist.relationshipAttributes.push({ id, attributes }); } else { groupped.push({ id: typeId, relationshipName, - relationshipAttributes: [relationshipAttributes], + relationshipAttributes: [{ id, attributes }], headers: displayFields, }); } diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index 99177b9b34..1a527b2260 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -16,7 +16,7 @@ export const WidgetRelationship = ({ relationships, title, onAddRelationship }: header={
{title} {relationships && - {relationships.relationshipsByType.length} + {relationships.count} }
diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js index 90b81c7481..6ba0e2bc83 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -1,9 +1,12 @@ // @flow - +import type { OutputRelationship } from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type Props = {| - relationships: Object, + relationships: { + relationshipsByType?: ?Array, + count: ?number, + }, title: string, - onAddRelationship: void, + onAddRelationship: () => void, ...CssClasses, |}; From fd5c535d2b5d14b88214388781dacd6987de6a03 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 13:46:47 +0000 Subject: [PATCH 16/95] fix: [DHIS2-12362] fix flow --- i18n/en.pot | 36 ++++++++--------- .../hooks/useComputeTEIRelationships.js | 40 ++++++++++++++----- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 861f673caa..68c76516cf 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-01-28T15:06:33.964Z\n" -"PO-Revision-Date: 2022-01-28T15:06:33.964Z\n" +"POT-Creation-Date: 2022-02-08T13:46:49.745Z\n" +"PO-Revision-Date: 2022-02-08T13:46:49.745Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -144,7 +144,7 @@ msgid "Search for user" msgstr "Search for user" msgid "Complete event" -msgstr "" +msgstr "Complete event" msgid "Basic info" msgstr "Basic info" @@ -464,10 +464,10 @@ msgid "No results" msgstr "No results" msgid "Description" -msgstr "" +msgstr "Description" msgid "URL" -msgstr "" +msgstr "URL" msgid "Icon for {{field}}" msgstr "Icon for {{field}}" @@ -631,6 +631,15 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" +msgid "TET name" +msgstr "TET name" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "Program stage not found" msgstr "Program stage not found" @@ -742,7 +751,7 @@ msgid "Registered events" msgstr "Registered events" msgid "Please select {{category}}." -msgstr "" +msgstr "Please select {{category}}." msgid "Or see all records accessible to you in {{program}} " msgstr "Or see all records accessible to you in {{program}} " @@ -1051,7 +1060,7 @@ msgid "This event doesn't have any comments" msgstr "This event doesn't have any comments" msgid "stage not found in rules execution" -msgstr "" +msgstr "stage not found in rules execution" msgid "Event completed" msgstr "Event completed" @@ -1123,14 +1132,6 @@ msgstr "Person Profile" msgid "Edit" msgstr "Edit" -msgid "You can’t add any more events in this program" -msgstr "" - -msgid "Cancel without saving" -msgstr "" - -msgid "Choose a stage for a new event" -msgstr "" msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" @@ -1172,7 +1173,7 @@ msgid "Stages and Events" msgstr "Stages and Events" msgid "Working list could not be loaded" -msgstr "" +msgstr "Working list could not be loaded" msgid "Delete event" msgstr "Delete event" @@ -1192,9 +1193,6 @@ msgstr "Download with current filters" msgid "Download data..." msgstr "Download data..." -msgid "Working list could not be loaded" -msgstr "Working list could not be loaded" - msgid "an error occurred loading working lists" msgstr "an error occurred loading working lists" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 1dfd1d008c..85adea6337 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -1,9 +1,12 @@ // @flow import { useCallback, useEffect, useState } from 'react'; import moment from 'moment'; +import i18n from '@dhis2/d2-i18n'; import { getCachedSingleResourceFromKeyAsync, } from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } + from '../../../../../metaData'; import { userStores } from '../../../../../storageControllers/stores'; import type { TEIRelationship, RelationshipData, @@ -43,7 +46,9 @@ const getRelationshipAttributes = ( if (to?.trackedEntityInstance && to?.trackedEntityInstance?.trackedEntityInstance !== teiId) { const displayFields = getDisplayFields(toConstraint.relationshipEntity); - const attributes = getAttributes(to.trackedEntityInstance.attributes, displayFields, { ...relationship }); + const tet = getTrackedEntityTypeThrowIfNotFound(toConstraint.trackedEntityType.id); + const attributes = getAttributes(to.trackedEntityInstance.attributes, displayFields, + { ...relationship, trackedEntityTypeName: tet.name }); return { id: to.trackedEntityInstance.trackedEntityInstance, @@ -55,7 +60,9 @@ const getRelationshipAttributes = ( } else if (bidirectional && from?.trackedEntityInstance && from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); - const attributes = getAttributes(from.trackedEntityInstance.attributes, displayFields, { ...relationship }); + const tet = getTrackedEntityTypeThrowIfNotFound(toConstraint.trackedEntityType.id); + const attributes = getAttributes(from.trackedEntityInstance.attributes, displayFields, + { ...relationship, trackedEntityTypeName: tet.name }); return { id: from.trackedEntityInstance.trackedEntityInstance, @@ -66,7 +73,17 @@ const getRelationshipAttributes = ( }; } else if (bidirectional && from?.event) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); - const attributes = getAttributes(from.event, displayFields, { ...from.event }); + // $FlowFixMe + const { stage, program } = getProgramAndStageFromEvent({ + evenId: from.event.event, + programId: from.event.program, + programStageId: from.event.programStage, + }); + const attributes = getAttributes(from.event, displayFields, { + ...from.event, + programName: program?.name, + programStageName: stage?.stageForm?.name, + }); return { id: from.event.event, @@ -92,7 +109,10 @@ const getDisplayFieldsFromAPI = { ], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [ { id: 'orgUnitName', label: 'Organisation unit' }, - { id: 'program', label: 'Program' }, + { id: 'program', + label: 'Program', + convertValue: props => props.programName, + }, { id: 'eventDate', label: 'Event date', convertValue: props => moment(props.eventDate).format('YYYY-MM-DD'), @@ -104,21 +124,21 @@ const getDisplayFieldsFromAPI = { const getBaseConfigHeaders = { [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ id: 'tetName', - label: 'TET name', - convertValue: props => props.trackedEntityType?.displayName, + label: i18n.t('TET name'), + convertValue: props => props.trackedEntityTypeName, }, { id: 'createdDate', - label: 'Created date', + label: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', - label: 'Program stage name', - convertValue: props => props.programStage?.name, + label: i18n.t('Program stage name'), + convertValue: props => props.programStageName, }, { id: 'createdDate', - label: 'Created date', + label: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], }; From 5faa3e11a26687c730f509418e640ccca369a128 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 13:51:22 +0000 Subject: [PATCH 17/95] fix: [DHIS2-12362] fix flow --- .../EnrollmentPageDefault/hooks/constants.js | 51 +++++++++++++++++++ .../hooks/useComputeTEIRelationships.js | 48 +---------------- 2 files changed, 52 insertions(+), 47 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js new file mode 100644 index 0000000000..d512395d45 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js @@ -0,0 +1,51 @@ + +import moment from 'moment'; +import i18n from '@dhis2/d2-i18n'; + +export const relationshipEntities = Object.freeze({ + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', +}); + + +// this will change after https://jira.dhis2.org/browse/DHIS2-12249 is done +export const getDisplayFieldsFromAPI = { + [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [ + { id: 'w75KJ2mc4zz', label: 'First name' }, + { id: 'zDhUuAYrxNC', label: 'Last name' }, + ], + [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [ + { id: 'orgUnitName', label: 'Organisation unit' }, + { id: 'program', + label: 'Program', + convertValue: props => props.programName, + }, + { id: 'eventDate', + label: 'Event date', + convertValue: props => moment(props.eventDate).format('YYYY-MM-DD'), + }, + { id: 'status', label: 'Status' }, + ], +}; + +export const getBaseConfigHeaders = { + [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ + id: 'tetName', + label: i18n.t('TET name'), + convertValue: props => props.trackedEntityTypeName, + }, { + id: 'createdDate', + label: i18n.t('Created date'), + convertValue: props => moment(props.created).format('YYYY-MM-DD'), + }], + [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ + id: 'programStageName', + label: i18n.t('Program stage name'), + convertValue: props => props.programStageName, + }, + { + id: 'createdDate', + label: i18n.t('Created date'), + convertValue: props => moment(props.created).format('YYYY-MM-DD'), + }], +}; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js index 85adea6337..b67bb72ede 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js @@ -1,7 +1,5 @@ // @flow import { useCallback, useEffect, useState } from 'react'; -import moment from 'moment'; -import i18n from '@dhis2/d2-i18n'; import { getCachedSingleResourceFromKeyAsync, } from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; @@ -11,6 +9,7 @@ import { userStores } from '../../../../../storageControllers/stores'; import type { TEIRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; const getRelationshipAttributes = ( relationshipType: Object, @@ -97,51 +96,6 @@ const getRelationshipAttributes = ( return {}; }; -const relationshipEntities = { - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', -}; - -const getDisplayFieldsFromAPI = { - [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [ - { id: 'w75KJ2mc4zz', label: 'First name' }, - { id: 'zDhUuAYrxNC', label: 'Last name' }, - ], - [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [ - { id: 'orgUnitName', label: 'Organisation unit' }, - { id: 'program', - label: 'Program', - convertValue: props => props.programName, - }, - { id: 'eventDate', - label: 'Event date', - convertValue: props => moment(props.eventDate).format('YYYY-MM-DD'), - }, - { id: 'status', label: 'Status' }, - ], -}; - -const getBaseConfigHeaders = { - [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ - id: 'tetName', - label: i18n.t('TET name'), - convertValue: props => props.trackedEntityTypeName, - }, { - id: 'createdDate', - label: i18n.t('Created date'), - convertValue: props => moment(props.created).format('YYYY-MM-DD'), - }], - [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ - id: 'programStageName', - label: i18n.t('Program stage name'), - convertValue: props => props.programStageName, - }, - { - id: 'createdDate', - label: i18n.t('Created date'), - convertValue: props => moment(props.created).format('YYYY-MM-DD'), - }], -}; export const useComputeTEIRelationships = (teiId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); From 65b9ecc8fbc13d460ce41276979714586162055d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 14:25:17 +0000 Subject: [PATCH 18/95] fix: [DHIS2-12362] minor fix --- .../useCommonEnrollmentDomainData.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index ed95c4f984..e0bc3a82b5 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -75,7 +75,6 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues, fetchedEnrollmentData.relationships, - teiId, ]); useEffect(() => { From a9492bdabd8bbc0b7135f4739549325cf76b183d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 14:27:34 +0000 Subject: [PATCH 19/95] fix: [DHIS2-12362] minor fix --- .../WidgetRelationship/Relationships/Relationships.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index 3d2bce5564..4d7869bb8d 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -14,7 +14,7 @@ type Props = { const styles = { container: { - padding: `${spacers.dp8} ${spacers.dp16}`, + padding: `0 ${spacers.dp16} ${spacers.dp6} ${spacers.dp16}`, }, title: { fontWeight: 500, From 6650036cf95e2c4306ac8b04f2496076e3a65455 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 8 Feb 2022 14:30:30 +0000 Subject: [PATCH 20/95] fix: [DHIS2-12362] minor fix --- .../WidgetRelationship/Relationships/Relationships.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index 4d7869bb8d..a778406ed5 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -14,7 +14,7 @@ type Props = { const styles = { container: { - padding: `0 ${spacers.dp16} ${spacers.dp6} ${spacers.dp16}`, + padding: `0 ${spacers.dp16} ${spacers.dp24} ${spacers.dp16}`, }, title: { fontWeight: 500, From 02a2802b65b0b124b04df20319f6511381ef8008 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 14 Feb 2022 09:03:38 +0000 Subject: [PATCH 21/95] chore: [DHIS2-12362] update relationships --- .../EnrollmentPageDefault.component.js | 4 +- .../EnrollmentPageDefault.container.js | 10 ++-- .../EnrollmentPageDefault.types.js | 6 +-- .../EnrollmentPageDefault/hooks/constants.js | 13 +++++ .../EnrollmentPageDefault/hooks/index.js | 2 +- ...EIRelationships.js => useRelationships.js} | 48 +++++++++++++++---- .../useCommonEnrollmentDomainData.types.js | 14 ++++-- .../Relationships/Relationships.component.js | 6 +-- .../WidgetRelationship.component.js | 6 +-- .../widgetRelationship.types.js | 5 +- 10 files changed, 78 insertions(+), 36 deletions(-) rename src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/{useComputeTEIRelationships.js => useRelationships.js} (77%) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index c5f8d7c050..74c1187fc3 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -49,7 +49,7 @@ export const EnrollmentPageDefaultPlain = ({ orgUnitId, events, enrollmentId, - relationships, + teiRelationships, stages, onDelete, onViewAll, @@ -81,7 +81,7 @@ export const EnrollmentPageDefaultPlain = ({ {}} /> {!hideWidgets.indicator && ( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 44d1217d0e..075b779abe 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -14,7 +14,7 @@ import { useProgramStages, useOrganisationUnit, useRuleEffects, - useComputeTEIRelationships, + useRelationships, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment } from '../EnrollmentPage.actions'; @@ -36,7 +36,7 @@ export const EnrollmentPageDefault = () => { } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - const { relationshipsByType } = useComputeTEIRelationships(teiId, relationships); + const { teiRelationships, enrollmentRelationships } = useRelationships(teiId, relationships); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( @@ -84,10 +84,8 @@ export const EnrollmentPageDefault = () => { stages={stages} events={enrollment?.events} enrollmentId={enrollmentId} - relationships={{ - relationshipsByType, - count: relationships && relationships.length, - }} + teiRelationships={teiRelationships} + enrollmentRelationships={enrollmentRelationships} onDelete={onDelete} onViewAll={onViewAll} onCreateNew={onCreateNew} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index c54a5ff030..9712ab57bd 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -10,10 +10,8 @@ export type Props = {| teiId: string, events: ?Array, stages?: Array, - relationships: ?{ - relationshipsByType?: ?Array, - count?: ?number, - }, + teiRelationships: Array, + enrollmentRelationships: Array, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, orgUnitId: string, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js index d512395d45..d94fe0bdf9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js @@ -5,6 +5,7 @@ import i18n from '@dhis2/d2-i18n'; export const relationshipEntities = Object.freeze({ TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', }); @@ -26,6 +27,13 @@ export const getDisplayFieldsFromAPI = { }, { id: 'status', label: 'Status' }, ], + [relationshipEntities.PROGRAM_INSTANCE]: [ + { id: 'orgUnitName', label: 'Organisation unit' }, + { id: 'enrollmentDate', + label: 'Enrollment date', + convertValue: props => moment(props.enrollmentDate).format('YYYY-MM-DD'), + }, + ], }; export const getBaseConfigHeaders = { @@ -48,4 +56,9 @@ export const getBaseConfigHeaders = { label: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], + [relationshipEntities.PROGRAM_INSTANCE]: [{ + id: 'createdDate', + label: i18n.t('Created date'), + convertValue: props => moment(props.created).format('YYYY-MM-DD'), + }], }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js index 9226d8acae..a37f7d1a50 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js @@ -5,5 +5,5 @@ export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; export { useProgramStages } from './useProgramStages'; export { useOrganisationUnit } from './useOrganisationUnit'; export { useRuleEffects } from './useRuleEffects'; -export { useComputeTEIRelationships } from './useComputeTEIRelationships'; +export { useRelationships } from './useRelationships'; export type { UseRuleEffectsInput } from './useRuleEffects.types'; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js similarity index 77% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js rename to src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js index b67bb72ede..6829a4b954 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useComputeTEIRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js @@ -7,10 +7,11 @@ import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } from '../../../../../metaData'; import { userStores } from '../../../../../storageControllers/stores'; import type { - TEIRelationship, RelationshipData, + InputRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; +/* eslint-disable complexity */ const getRelationshipAttributes = ( relationshipType: Object, teiId: string, @@ -55,6 +56,7 @@ const getRelationshipAttributes = ( relationshipProgram: toConstraint.program, attributes, displayFields, + isTeiRelationship: true, }; } else if (bidirectional && from?.trackedEntityInstance && from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { @@ -69,6 +71,7 @@ const getRelationshipAttributes = ( relationshipProgram: fromConstraint.program, attributes, displayFields, + isTeiRelationship: true, }; } else if (bidirectional && from?.event) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); @@ -90,6 +93,19 @@ const getRelationshipAttributes = ( relationshipProgram: fromConstraint.program, attributes, displayFields, + isTeiRelationship: true, + }; + } else if (from.enrollment && from.enrollment.enrollment) { + const displayFields = getDisplayFields(fromConstraint.relationshipEntity); + const attributes = getAttributes(from.enrollment, displayFields, + { ...from.enrollment }); + + return { + id: from.enrollment.enrollment, + relationshipName: toFromName, + isTeiRelationship: false, + attributes, + displayFields, }; } @@ -97,23 +113,25 @@ const getRelationshipAttributes = ( }; -export const useComputeTEIRelationships = (teiId: string, relationships?: Array) => { +export const useRelationships = (teiId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); const computeData = useCallback(async () => { const groupped = []; + if (!relationships) { return; } // $FlowFixMe for await (const relationship of relationships) { const { relationshipType: typeId, from, to } = relationship; + const relationshipType = await getCachedSingleResourceFromKeyAsync( + userStores.RELATIONSHIP_TYPES, typeId, + ).then(result => result.response); - const typeExist = groupped.find(item => item.id === typeId); - const relationshipType = await getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, typeId) - .then(result => result.response); - - const { relationshipName, displayFields, id, attributes } = getRelationshipAttributes( + const { relationshipName, displayFields, id, attributes, isTeiRelationship } = getRelationshipAttributes( relationshipType, teiId, from, to, { relationship }, ); + const typeExist = groupped.find(item => item.id === typeId); + if (typeExist) { typeExist.relationshipAttributes.push({ id, attributes }); } else { @@ -122,6 +140,7 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: Array< relationshipName, relationshipAttributes: [{ id, attributes }], headers: displayFields, + isTeiRelationship, }); } } @@ -133,5 +152,18 @@ export const useComputeTEIRelationships = (teiId: string, relationships?: Array< computeData(); }, [computeData]); - return { relationshipsByType }; + const { teiRelationships, enrollmentRelationships } = relationshipsByType + .reduce((acc, { isTeiRelationship, ...currentRel }) => { + if (isTeiRelationship) { + acc.teiRelationships.push(currentRel); + } else { + acc.enrollmentRelationships.push(currentRel); + } + return acc; + }, { teiRelationships: [], enrollmentRelationships: [] }); + + return { + teiRelationships, + enrollmentRelationships, + }; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 9346d7777e..72522a2bd6 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -55,20 +55,24 @@ export type TEIAttribute = {| valueType: string, |} -export type TEIData = {| +export type TEIRelationshipData = {| trackedEntityInstance: { trackedEntityInstance: string, attributes: Array } |} -export type EventData = {| +export type EventRelationshipData = {| event: Event |} -export type RelationshipData = TEIData | EventData +export type EnrollmentRelationshipData = {| + enrollment: EnrollmentData +|} + +export type RelationshipData = TEIRelationshipData | EventRelationshipData | EnrollmentRelationshipData -export type TEIRelationship = {| +export type InputRelationship = {| relationshipType: string, relationshipName: string, relationship: string, @@ -87,5 +91,5 @@ export type Output = {| error?: any, enrollment?: EnrollmentData, attributeValues?: Array, - relationships?: Array + relationships?: Array |}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js index a778406ed5..637b22b945 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js @@ -6,7 +6,7 @@ import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; import { RelationshipTable } from './RelationshipTable'; type Props = { - relationshipsByType: Object, + relationships: Object, headersByType: Object, onAddRelationship: void, ...CssClasses, @@ -27,13 +27,13 @@ const styles = { overflow: 'scroll', }, }; -const RelationshipsPlain = ({ relationshipsByType, classes, onAddRelationship }: Props) => ( +const RelationshipsPlain = ({ relationships, classes, onAddRelationship }: Props) => (
{ - relationshipsByType ? relationshipsByType.map((relationship) => { + relationships ? relationships.map((relationship) => { const { relationshipName, id, ...passOnProps } = relationship; return (
{relationshipName}
diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js index 1a527b2260..8cf90cd4b4 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js @@ -7,7 +7,7 @@ import { Relationships } from './Relationships/'; export const WidgetRelationship = ({ relationships, title, onAddRelationship }: Props) => { const [open, setOpenStatus] = useState(true); - + const count = relationships.reduce((acc, curr) => { acc += curr.relationshipAttributes.length; return acc; }, 0); return (
{title} {relationships && - {relationships.count} + {count} }
@@ -25,7 +25,7 @@ export const WidgetRelationship = ({ relationships, title, onAddRelationship }: onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - +
); diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js index 6ba0e2bc83..0b3683897c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js @@ -2,10 +2,7 @@ import type { OutputRelationship } from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type Props = {| - relationships: { - relationshipsByType?: ?Array, - count: ?number, - }, + relationships: Array, title: string, onAddRelationship: () => void, ...CssClasses, From e9b4a70b190832b6482f7d3ae80b31c38902685d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 14 Feb 2022 09:12:47 +0000 Subject: [PATCH 22/95] chore: [DHIS2-12362] update relationships --- .../EnrollmentPageDefault/hooks/constants.js | 5 ++++ .../hooks/useRelationships.js | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js index d94fe0bdf9..ce8a088dba 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js @@ -8,6 +8,11 @@ export const relationshipEntities = Object.freeze({ PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', }); +export const relationshipWidgetTypes = Object.freeze({ + TET_RELATIONSHIP: 'TET_RELATIONSHIP', + ENROLLMENT_RELATIONSHIP: 'ENROLLMENT_RELATIONSHIP', + EVENT_RELATIONSHIP: 'EVENT_RELATIONSHIP', +}); // this will change after https://jira.dhis2.org/browse/DHIS2-12249 is done export const getDisplayFieldsFromAPI = { diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js index 6829a4b954..6c2132150c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js @@ -9,7 +9,7 @@ import { userStores } from '../../../../../storageControllers/stores'; import type { InputRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; +import { getDisplayFieldsFromAPI, getBaseConfigHeaders, relationshipWidgetTypes } from './constants'; /* eslint-disable complexity */ const getRelationshipAttributes = ( @@ -56,7 +56,7 @@ const getRelationshipAttributes = ( relationshipProgram: toConstraint.program, attributes, displayFields, - isTeiRelationship: true, + widgetType: relationshipWidgetTypes.TET_RELATIONSHIP, }; } else if (bidirectional && from?.trackedEntityInstance && from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { @@ -71,7 +71,7 @@ const getRelationshipAttributes = ( relationshipProgram: fromConstraint.program, attributes, displayFields, - isTeiRelationship: true, + widgetType: relationshipWidgetTypes.TET_RELATIONSHIP, }; } else if (bidirectional && from?.event) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); @@ -93,7 +93,7 @@ const getRelationshipAttributes = ( relationshipProgram: fromConstraint.program, attributes, displayFields, - isTeiRelationship: true, + widgetType: relationshipWidgetTypes.EVENT_RELATIONSHIP, }; } else if (from.enrollment && from.enrollment.enrollment) { const displayFields = getDisplayFields(fromConstraint.relationshipEntity); @@ -106,6 +106,7 @@ const getRelationshipAttributes = ( isTeiRelationship: false, attributes, displayFields, + widgetType: relationshipWidgetTypes.ENROLLMENT_RELATIONSHIP, }; } @@ -152,18 +153,27 @@ export const useRelationships = (teiId: string, relationships?: Array { - if (isTeiRelationship) { + const { teiRelationships, enrollmentRelationships, eventRelationships } = relationshipsByType + .reduce((acc, { widgetType, ...currentRel }) => { + switch (widgetType) { + case relationshipWidgetTypes.TET_RELATIONSHIP: acc.teiRelationships.push(currentRel); - } else { + break; + case relationshipWidgetTypes.ENROLLMENT_RELATIONSHIP: acc.enrollmentRelationships.push(currentRel); + break; + case relationshipWidgetTypes.EVENT_RELATIONSHIP: + acc.eventRelationships.push(currentRel); + break; + default: + break; } return acc; - }, { teiRelationships: [], enrollmentRelationships: [] }); + }, { teiRelationships: [], enrollmentRelationships: [], eventRelationships: [] }); return { teiRelationships, enrollmentRelationships, + eventRelationships, }; }; From eb7239d27caa16d1804e3d8cb223992a0ac11b2f Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 14 Feb 2022 11:16:56 +0000 Subject: [PATCH 23/95] fix: [DHIS2-12362] fix flow --- .../EnrollmentPageDefault/hooks/useRelationships.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js index 6c2132150c..30057effc8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js @@ -103,7 +103,6 @@ const getRelationshipAttributes = ( return { id: from.enrollment.enrollment, relationshipName: toFromName, - isTeiRelationship: false, attributes, displayFields, widgetType: relationshipWidgetTypes.ENROLLMENT_RELATIONSHIP, @@ -128,7 +127,7 @@ export const useRelationships = (teiId: string, relationships?: Array result.response); - const { relationshipName, displayFields, id, attributes, isTeiRelationship } = getRelationshipAttributes( + const { relationshipName, displayFields, id, attributes, widgetType } = getRelationshipAttributes( relationshipType, teiId, from, to, { relationship }, ); const typeExist = groupped.find(item => item.id === typeId); @@ -141,7 +140,7 @@ export const useRelationships = (teiId: string, relationships?: Array Date: Tue, 15 Feb 2022 08:26:05 +0000 Subject: [PATCH 24/95] feat: [DHIS2-12362] add relationship to view enrollment event --- .../EnrollmentEditEventPage.component.js | 7 +++++++ .../EnrollmentEditEventPage.container.js | 6 ++++-- .../EnrollmentEditEvent/EnrollmentEditEventPage.types.js | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 0274edca7f..ef21429cfe 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -28,6 +28,7 @@ import { SingleLockedSelect } from '../../ScopeSelector/QuickSelector/SingleLock import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { TopBarActions } from '../../TopBarActions'; import { WidgetEventComment } from '../../WidgetEventComment'; +import { WidgetRelationship } from '../../WidgetRelationship'; const styles = ({ typography }) => ({ page: { @@ -62,6 +63,7 @@ const EnrollmentEditEventPagePain = ({ teiId, enrollmentId, programId, + teiRelationships, enrollmentsAsOptions, trackedEntityName, teiDisplayName, @@ -183,6 +185,11 @@ const EnrollmentEditEventPagePain = ({ + {}} + /> {!hideWidgets.feedback && ( { dispatch(deleteEnrollment({ enrollmentId })); }; const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ orgUnitId, programId, teiId, enrollmentId })}`); - const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; + const { enrollment: enrollmentSite, relationships } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { teiDisplayName } = useTeiDisplayName(teiId, programId); const { trackedEntityName } = getScopeInfo(enrollmentSite?.trackedEntityType); const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); @@ -48,6 +48,7 @@ export const EnrollmentEditEventPage = () => { ? (pageStatus = pageStatuses.DEFAULT) : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; + const { teiRelationships } = useRelationships(teiId, relationships); return ( { onGoBack={onGoBack} widgetEffects={outputEffects} hideWidgets={hideWidgets} + teiRelationships={teiRelationships} teiId={teiId} enrollmentId={enrollmentId} enrollmentsAsOptions={enrollmentsAsOptions} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 3007493b33..47dcf85c3a 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; +import type { OutputRelationship } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type PlainProps = {| programStage: ?ProgramStage, @@ -13,6 +14,7 @@ export type PlainProps = {| orgUnitId: string, trackedEntityName: string, teiDisplayName: string, + teiRelationships: Array, eventDate?: string, enrollmentsAsOptions: Array, onDelete: () => void, From d0b115d8dd6d41a0c430f6c10081941acaf405ac Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Fri, 18 Feb 2022 23:32:27 +0100 Subject: [PATCH 25/95] fix: changed object for displaying relationships & breadcrumbs --- i18n/en.pot | 7 +- .../EnrollmentPageDefault.component.js | 105 ++++++++------- .../EnrollmentPageDefault.container.js | 4 +- .../EnrollmentPageDefault.types.js | 1 - .../Breadcrumbs/Breadcrumbs.js | 43 +++++- .../NewTrackedEntityRelationship.container.js | 23 +++- .../RelationshipSelectorRow.js | 64 +++++++++ .../RelationshipTypeSelector.js | 126 +++--------------- .../WidgetTrackedEntityRelationship.js | 6 +- .../WidgetTrackedEntityRelationship.types.js | 7 + .../WidgetTrackedEntityRelationship/hooks.js | 54 +++++++- 11 files changed, 261 insertions(+), 179 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipSelectorRow/RelationshipSelectorRow.js diff --git a/i18n/en.pot b/i18n/en.pot index 56874d6518..f973854e00 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-02-17T19:25:03.181Z\n" -"PO-Revision-Date: 2022-02-17T19:25:03.181Z\n" +"POT-Creation-Date: 2022-02-18T22:32:30.675Z\n" +"PO-Revision-Date: 2022-02-18T22:32:30.675Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1178,6 +1178,9 @@ msgstr "Create new" msgid "An error occurred" msgstr "An error occurred" +msgid "Go back without saving relationship" +msgstr "Go back without saving relationship" + msgid "New Relationship" msgstr "New Relationship" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 95b56f21a5..7964be47cf 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useRef } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -62,61 +62,64 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, - renderRelationshipRef, relationshipTypes, -}: PlainProps) => ( -
-
{i18n.t('Enrollment Dashboard')}
-
-
- - -
-
- - - - - {!hideWidgets.indicator && ( - { + const renderRelationshipRef = useRef(); + return ( +
+
{i18n.t('Enrollment Dashboard')}
+
+
+ + - )} - {!hideWidgets.feedback && ( - +
+ - )} - - {enrollmentId !== 'AUTO' && } + + + + {!hideWidgets.indicator && ( + + )} + {!hideWidgets.feedback && ( + + )} + + {enrollmentId !== 'AUTO' && } +
-
-); + ); +}; export const EnrollmentPageDefaultComponent: ComponentType = withStyles( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index c5f163cdd9..3ef5a20682 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -1,5 +1,5 @@ // @flow -import React, { useRef } from 'react'; +import React from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; // $FlowFixMe @@ -24,7 +24,6 @@ import { useRelationshipTypes } from '../../../WidgetTrackedEntityRelationship/h export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); - const renderRelationshipRef = useRef(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit } = useOrganisationUnit(orgUnitId); const { relationshipTypes } = useRelationshipTypes(); @@ -90,7 +89,6 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} - renderRelationshipRef={renderRelationshipRef} relationshipTypes={relationshipTypes} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index efb9ab144e..89ed15793b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -18,7 +18,6 @@ export type Props = {| onViewAll: (stageId: string) => void, onCreateNew: (stageId: string) => void, onEventClick: (eventId: string, stageId: string) => void, - renderRelationshipRef: Object, relationshipTypes: ?Array |}; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js index 80099c4ab2..928ee402c8 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js @@ -1,7 +1,44 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { NewTEIRelationshipStatuses } from '../../WidgetTrackedEntityRelationship.const'; -export const Breadcrumbs = () => ( -

{i18n.t('New TEI Relationship')}

-); +const styles = { + container: { + padding: `${spacers.dp8} 0`, + }, + slash: { + padding: 5, + }, +}; + +const BreadcrumbsPlain = ({ + selectedRelationshipType, + onResetRelationshipType, + pageStatus, + classes, +}) => { + const initialText = i18n.t('New TEI Relationship'); + const renderSlash = () => (/); + + return ( +
+ {pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE && ( + {initialText} + )} + + {pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE && ( + <> + {initialText} + {renderSlash()} + {selectedRelationshipType.displayName} + + )} +
+ ); +}; + +export const Breadcrumbs = withStyles(styles)(BreadcrumbsPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index 13cb279ad9..3536178fbe 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -2,11 +2,11 @@ import React, { useCallback, useMemo, useState } from 'react'; import * as ReactDOM from 'react-dom'; import { withStyles } from '@material-ui/core'; +import i18n from '@dhis2/d2-i18n'; import { Widget } from '../../Widget'; import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; import type { Props } from './NewTrackedEntityRelationship.types'; -import { useFilteredRelationshipTypes } from '../hooks'; import { LinkButton } from '../../Buttons/LinkButton.component'; import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'; import { useLocationQuery } from '../../../utils/routing'; @@ -48,7 +48,6 @@ export const NewTrackedEntityRelationshipPlain = ({ const [selectedRelationshipType, setSelectedRelationshipType] = useState(); const [creationMode, setCreationMode] = useState(); const { programId } = useLocationQuery(); - const filteredRelationshipTypes = useFilteredRelationshipTypes(relationshipTypes, trackedEntityType, programId); const pageStatus = useMemo(() => { if (!selectedRelationshipType) { @@ -61,8 +60,8 @@ export const NewTrackedEntityRelationshipPlain = ({ }, [creationMode, selectedRelationshipType]); const onSelectRelationshipType = useCallback( - relationshipType => setSelectedRelationshipType(relationshipType), [], - ); + relationshipType => setSelectedRelationshipType(relationshipType), + []); const onCancel = useCallback(() => { hideDialog(); @@ -70,6 +69,11 @@ export const NewTrackedEntityRelationshipPlain = ({ setCreationMode(); }, [hideDialog]); + const onResetRelationshipType = useCallback(() => { + setSelectedRelationshipType(); + setCreationMode(); + }, []); + if (!showDialog || !renderRef.current) { return null; } @@ -79,15 +83,20 @@ export const NewTrackedEntityRelationshipPlain = ({
- Go back without saving relationship + {i18n.t('Go back without saving relationship')}
} + header={} > ( +
+
+ {relationship.displayName} +
+
+
+ {relationship.fromConstraint && ( + + )} + {relationship.toConstraint && ( + + )} +
+
+
+); + +export const RelationshipSelectorRow = withStyles(styles)(RelationshipSelectorRowPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js index 90dfd5b358..ba89b0a619 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js @@ -1,132 +1,44 @@ // @flow -import { Button, spacers } from '@dhis2/ui'; -import React, { useMemo } from 'react'; +import React from 'react'; +import { spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; +import { useFilteredRelationshipTypes, useRelationshipsForCurrentTEI } from '../../hooks'; +import { RelationshipSelectorRow } from '../RelationshipSelectorRow/RelationshipSelectorRow'; const styles = { container: { padding: spacers.dp16, paddingTop: 0, }, - typeselector: { + typeSelector: { display: 'flex', flexDirection: 'column', gap: spacers.dp8, marginBottom: spacers.dp16, }, - dualButtonContainer: { - display: 'flex', - gap: spacers.dp8, - }, - selectorButton: { - display: 'flex', - flexDirection: 'column', - gap: spacers.dp4, - marginBottom: spacers.dp8, - }, - title: { - fontWeight: 500, - marginBottom: spacers.dp4, - }, }; -const RelationshipTypeSelectorPlain = ({ relationshipTypes, trackedEntityType, programId, onSelectType, classes }) => useMemo(() => { - const renderButtons = (relationship) => { - const defaultButton = ( - - ); - - const { fromConstraint, toConstraint } = relationship; - if (relationship.bidirectional) { - if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType && relationship.toConstraint.trackedEntityType?.id === trackedEntityType) { - let fromConstraintValid = true; - let toConstraintValid = true; - if (fromConstraint.program) { - if ((fromConstraint.program?.id !== programId) || (fromConstraint?.trackedEntityType.id !== trackedEntityType)) { - fromConstraintValid = false; - } - } - if (toConstraint.program) { - if ((toConstraint.program?.id !== programId) || (toConstraint?.trackedEntityType.id !== trackedEntityType)) { - toConstraintValid = false; - } - } - - return ( -
- {fromConstraintValid && ( - - )} - {toConstraintValid && ( - - )} -
- ); - } - if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType) { - return defaultButton; - } - if (relationship.toConstraint.trackedEntityType?.id === trackedEntityType) { - return ( - - ); - } - } - - return defaultButton; - }; +const RelationshipTypeSelectorPlain = ({ relationshipTypes, trackedEntityType, programId, onSelectType, classes }) => { + const filteredRelationshipTypes = useFilteredRelationshipTypes(relationshipTypes, trackedEntityType, programId); + const relationshipsForCurrentTEI = useRelationshipsForCurrentTEI({ + relationshipTypes: filteredRelationshipTypes, + programId, + trackedEntityType, + }); return (
-
- {relationshipTypes?.map(relationship => ( -
+ {relationshipsForCurrentTEI.map(relationship => ( + -
- {relationship.displayName} -
-
- {renderButtons(relationship)} -
-
+ relationship={relationship} + onSelectType={onSelectType} + /> ))}
); -}, [classes, onSelectType, programId, relationshipTypes, trackedEntityType]); +}; export const RelationshipTypeSelector = withStyles(styles)(RelationshipTypeSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js index 50876f1ff9..a2b8420089 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js @@ -5,7 +5,7 @@ import i18n from '@dhis2/d2-i18n'; import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship/NewTrackedEntityRelationship.container'; import type { Props } from './WidgetTrackedEntityRelationship.types'; -export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef, trackedEntityType }: Props) => { +export const WidgetTrackedEntityRelationship = ({ ...PassOnProps }: Props) => { const [showDialog, setShowDialog] = useState(false); const changeDialogView = useCallback(() => { @@ -21,11 +21,9 @@ export const WidgetTrackedEntityRelationship = ({ relationshipTypes, renderRef, ); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js index b4b5befef7..3abade5297 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -19,4 +19,11 @@ export type Props = {| relationshipTypes: ?Array, renderRef: Object, trackedEntityType: string, + programId: string, +|} + +export type RelationshipsForCurrentTEI = {| + relationshipTypes: Array, + programId: string, + trackedEntityType: string, |} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js index 0574c23d14..3b9faee424 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js @@ -4,7 +4,7 @@ import { getCachedResourceAsync, } from '../../MetaDataStoreUtils/MetaDataStoreUtils'; import { userStores } from '../../storageControllers/stores'; -import type { RelationshipType } from './WidgetTrackedEntityRelationship.types'; +import type { RelationshipsForCurrentTEI, RelationshipType } from './WidgetTrackedEntityRelationship.types'; export const useRelationshipTypes = () => { const [cachedTypes, setCachedTypes] = useState(); @@ -64,3 +64,55 @@ export const useFilteredRelationshipTypes = (relationshipTypes: Array + useMemo(() => relationshipTypes.reduce((acc, relationship) => { + const newRelationship: any = { + id: relationship.id, + displayName: relationship.displayName, + }; + + if (relationship.bidirectional) { + if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType) { + if (relationship.fromConstraint?.program) { + if (relationship.fromConstraint?.program?.id === programId) { + newRelationship.fromConstraint = { + ...relationship.fromConstraint, + program: relationship.fromConstraint.program, + displayName: relationship.fromToName, + }; + } + } else { + newRelationship.fromConstraint = { + ...relationship.fromConstraint, + displayName: relationship.fromToName, + }; + } + } + + if (relationship.toConstraint?.trackedEntityType?.id === trackedEntityType) { + if (relationship.toConstraint?.program) { + if (relationship.toConstraint?.program?.id === programId) { + newRelationship.toConstraint = { + ...relationship.toConstraint, + program: relationship.toConstraint.program, + displayName: relationship.toFromName, + }; + } + } else { + newRelationship.toConstraint = { + ...relationship.toConstraint, + displayName: relationship.toFromName, + }; + } + } + } else { + newRelationship.fromConstraint = { + ...relationship.fromConstraint, + displayName: relationship.fromToName, + }; + } + + acc.push(newRelationship); + return acc; + }, []), [programId, relationshipTypes, trackedEntityType]); From d88383ae5f98eb32ee9a3fd2dd20c15a1e85f155 Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Fri, 25 Feb 2022 19:56:42 +0100 Subject: [PATCH 26/95] feat: initial layout for linking relationship to existing TEI --- .../Breadcrumbs/Breadcrumbs.js | 11 ++ .../NewTrackedEntityRelationship.actions.js | 22 +++ .../NewTrackedEntityRelationship.component.js | 29 ++- .../NewTrackedEntityRelationship.const.js | 5 + .../NewTrackedEntityRelationship.container.js | 46 ++++- .../NewTrackedEntityRelationship.epics.js | 37 ++++ .../SearchOrgUnitSelector.component.js | 152 +++++++++++++++ .../SearchOrgUnitSelector.container.js | 46 +++++ ...archOrgUnitSelectorRefHandler.component.js | 18 ++ .../searchOrgUnitSelector.actions.js | 30 +++ .../searchOrgUnitSelector.epics.js | 82 ++++++++ .../SearchProgramSelector.component.js | 43 +++++ .../SearchProgramSelector.container.js | 26 +++ .../searchProgramSelector.actions.js | 15 ++ .../searchProgramSelector.selectors.js | 19 ++ .../TeiSearch/TeiSearch.component.js | 162 ++++++++++++++++ .../TeiSearch/TeiSearch.container.js | 57 ++++++ .../TeiSearch/TeiSearch.types.js | 27 +++ .../TeiSearchForm/TeiSearchForm.component.js | 179 ++++++++++++++++++ .../TeiSearchForm/TeiSearchForm.container.js | 23 +++ .../TeiSearchResults.component.js | 11 ++ .../TeiSearchResults.container.js | 31 +++ .../TeiSearchResults.types.js | 23 +++ .../TeiSearch/actions/teiSearch.actions.js | 70 +++++++ .../TeiSearch/getSearchGroups.js | 19 ++ .../TeiSearch/teiSearch.selectors.js | 23 +++ src/epics/trackerCapture.epics.js | 4 + 27 files changed, 1206 insertions(+), 4 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js create mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js index 928ee402c8..4c68ceea25 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js @@ -18,6 +18,7 @@ const styles = { const BreadcrumbsPlain = ({ selectedRelationshipType, onResetRelationshipType, + onResetCreationMode, pageStatus, classes, }) => { @@ -37,6 +38,16 @@ const BreadcrumbsPlain = ({ {selectedRelationshipType.displayName} )} + + {pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING && ( + <> + {initialText} + {renderSlash()} + {selectedRelationshipType.displayName} + {renderSlash()} + {'Search'} + + )}
); }; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js new file mode 100644 index 0000000000..516210c91b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js @@ -0,0 +1,22 @@ +import { actionCreator } from '../../../actions/actions.utils'; +import { effectMethods } from '../../../trackerOffline'; + +export const NewTrackedEntityRelationshipActionTypes = { + BATCH_OPEN_TEI_SEARCH: 'BatchOpenTeiSearch', + INIT_TEI_SEARCH_FOR_WIDGET: 'InitTeiSearchForWidget', + REQUEST_SAVE_RELATIONSHIP_FOR_TEI: 'RequestSaveRelationshipForTei', +}; + +export const startTeiSearchForWidget = ({ selectedRelationshipType }) => + actionCreator(NewTrackedEntityRelationshipActionTypes.INIT_TEI_SEARCH_FOR_WIDGET)({ selectedRelationshipType }); + +export const requestSaveRelationshipForTei = ({ serverData }) => + actionCreator(NewTrackedEntityRelationshipActionTypes.REQUEST_SAVE_RELATIONSHIP_FOR_TEI)({ serverData }, { + offline: { + effect: { + url: 'tracker?async=false&importStrategy=UPDATE', + method: effectMethods.POST, + data: serverData, + }, + }, + }); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 68fceb71ed..8a8ea1a37a 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -4,6 +4,11 @@ import { Button, IconSearch16, IconAdd16, spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; import { RelationshipTypeSelector } from './RelationshipTypeSelector/RelationshipTypeSelector'; +import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; +import { TeiSearch } from './TeiSearch/TeiSearch.container'; +import { + TeiRelationshipSearchResults, +} from '../../Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; const styles = { container: { @@ -19,7 +24,7 @@ const styles = { }, }; -const NewTrackedEntityRelationshipComponentPlain = ({ pageStatus, classes, ...PassOnProps }) => { +const NewTrackedEntityRelationshipComponentPlain = ({ pageStatus, onSetCreationMode, addRelationship, classes, ...PassOnProps }) => { if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { return (
- @@ -45,6 +53,23 @@ const NewTrackedEntityRelationshipComponentPlain = ({ pageStatus, classes, ...Pa ); } + if (pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING) { + return ( +
+ ( + + )} + /> +
+ ); + } + return

{i18n.t('An error occurred')}

; }; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js new file mode 100644 index 0000000000..5b0ee59804 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js @@ -0,0 +1,5 @@ + +export const creationModeStatuses = Object.freeze({ + SEARCH: 'search', + NEW: 'new', +}); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index 3536178fbe..f4f7a20307 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -1,5 +1,6 @@ // @flow import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; import * as ReactDOM from 'react-dom'; import { withStyles } from '@material-ui/core'; import i18n from '@dhis2/d2-i18n'; @@ -10,6 +11,8 @@ import type { Props } from './NewTrackedEntityRelationship.types'; import { LinkButton } from '../../Buttons/LinkButton.component'; import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'; import { useLocationQuery } from '../../../utils/routing'; +import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; +import { requestSaveRelationshipForTei, startTeiSearchForWidget } from './NewTrackedEntityRelationship.actions'; const styles = { container: { @@ -47,7 +50,32 @@ export const NewTrackedEntityRelationshipPlain = ({ }: Props) => { const [selectedRelationshipType, setSelectedRelationshipType] = useState(); const [creationMode, setCreationMode] = useState(); - const { programId } = useLocationQuery(); + const { programId, teiId } = useLocationQuery(); + const dispatch = useDispatch(); + + const handleAddRelationship = useCallback((linkedTei) => { + if (selectedRelationshipType) { + const { + constraintSide, + id, + } = selectedRelationshipType; + const linkedTeiConstraintSide: string = constraintSide !== 'from' ? 'from' : 'to'; + + const serverData = { + relationships: [{ + relationshipType: id, + [constraintSide]: { + trackedEntity: teiId, + }, + [linkedTeiConstraintSide]: { + trackedEntity: linkedTei, + }, + }], + }; + + dispatch(requestSaveRelationshipForTei({ serverData })); + } + }, [dispatch, selectedRelationshipType, teiId]); const pageStatus = useMemo(() => { if (!selectedRelationshipType) { @@ -56,6 +84,9 @@ export const NewTrackedEntityRelationshipPlain = ({ if (!creationMode) { return NewTEIRelationshipStatuses.MISSING_CREATION_MODE; } + if (creationMode === creationModeStatuses.SEARCH) { + return NewTEIRelationshipStatuses.LINK_TO_EXISTING; + } return NewTEIRelationshipStatuses.DEFAULT; }, [creationMode, selectedRelationshipType]); @@ -74,6 +105,15 @@ export const NewTrackedEntityRelationshipPlain = ({ setCreationMode(); }, []); + const onResetCreationMode = useCallback(() => { + setCreationMode(); + }, []); + + const onSetCreationMode = useCallback((value) => { + setCreationMode(value); + dispatch(startTeiSearchForWidget({ selectedRelationshipType })); + }, [dispatch, selectedRelationshipType]); + if (!showDialog || !renderRef.current) { return null; } @@ -92,15 +132,17 @@ export const NewTrackedEntityRelationshipPlain = ({ pageStatus={pageStatus} selectedRelationshipType={selectedRelationshipType} onResetRelationshipType={onResetRelationshipType} - creationMode={creationMode} + onResetCreationMode={onResetCreationMode} />} > diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js new file mode 100644 index 0000000000..b1212dd663 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js @@ -0,0 +1,37 @@ +// @flow + +import { ofType } from 'redux-observable'; +import { batchActions } from 'redux-batched-actions'; +import { map } from 'rxjs/operators'; +import { getSearchGroups } from '../../TeiSearch/getSearchGroups'; +import { getSearchFormId } from '../../TeiSearch/getSearchFormId'; +import { addFormData } from '../../D2Form/actions/form.actions'; +import { initializeTeiSearch } from '../../TeiSearch/actions/teiSearch.actions'; +import { batchActionTypes } from '../../Pages/NewRelationship/TeiRelationship/teiRelationship.actions'; +import { NewTrackedEntityRelationshipActionTypes } from './NewTrackedEntityRelationship.actions'; + +const searchId = 'relationshipTeiSearch'; + +export const openRelationshipTeiSearchForWidgetEpic = (action$: InputObservable) => + action$.pipe( + ofType(NewTrackedEntityRelationshipActionTypes.INIT_TEI_SEARCH_FOR_WIDGET), + map((action) => { + const { constraint } = action.payload.selectedRelationshipType; + + const { programId, trackedEntityType } = constraint; + const contextId = programId || trackedEntityType?.id; + + const searchGroups = getSearchGroups(trackedEntityType?.id, programId); + + + const addFormDataActions = searchGroups ? searchGroups.map((sg, i) => { + const key = getSearchFormId(searchId, contextId, i.toString()); + return addFormData(key, {}); + }) : []; + + return batchActions([ + ...addFormDataActions, + initializeTeiSearch(searchId, programId, trackedEntityType?.id), + ], batchActionTypes.BATCH_OPEN_TEI_SEARCH); + })); + diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js new file mode 100644 index 0000000000..931c58c08a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js @@ -0,0 +1,152 @@ +// @flow +import * as React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + SelectionBoxes, + withDefaultFieldContainer, + withLabel, + withFocusSaver, + withCalculateMessages, + withDisplayMessages, + SingleOrgUnitSelectField, +} from '../../../../FormFields/New'; + +const TeiSearchOrgUnitField = withFocusSaver()(withCalculateMessages()(withDefaultFieldContainer()(withLabel()(withDisplayMessages()(SingleOrgUnitSelectField))))); +const TeiSearchSelectionBoxes = withDefaultFieldContainer()(withLabel()(SelectionBoxes)); + +type Props = { + searchId: string, + selectedOrgUnit?: ?any, + selectedOrgUnitScope?: ?string, + treeRoots: ?Array, + treeReady: ?boolean, + treeKey: ?string, + treeSearchText?: ?string, + onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => void, + onSetOrgUnit: (searchId: string, orgUnit: ?Object) => void, + onFilterOrgUnits: (searchId: string, searchText: ?string) => void, + searchAttempted: ?boolean, +} + +const orgUnitFieldStyles = { + labelContainerStyle: { + paddingTop: 12, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +const selectionBoxesStyles = { + labelContainerStyle: { + paddingTop: 13, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +const options = [ + { + name: 'All accessible', + value: 'ACCESSIBLE', + }, + { + name: 'Selected', + value: 'SELECTED', + }, +]; + +const errorMessage = 'Please select an organisation unit'; + +export class SearchOrgUnitSelector extends React.Component { + gotoInstance: any; + + onSelectOrgUnitScope = (value: any) => { + if (value) { + this.props.onSelectOrgUnitScope(this.props.searchId, value); + } + } + onSetOrgUnit = (orgUnit: ?Object) => { + this.props.onSetOrgUnit(this.props.searchId, orgUnit); + } + renderOrgUnitScopeSelector = () => { + const { selectedOrgUnitScope } = this.props; + return ( + + ); + } + + isValid = () => this.props.selectedOrgUnitScope === 'ACCESSIBLE' || this.props.selectedOrgUnit + + validateAndScrollToIfFailed() { + const isValid = this.isValid(); + if (!isValid) { + this.goto(); + } + + return isValid; + } + + goto() { + if (this.gotoInstance) { + this.gotoInstance.scrollIntoView(); + + const scrolledY = window.scrollY; + if (scrolledY) { + // TODO: Set the modifier some other way (caused be the fixed header) + window.scroll(0, scrolledY - 48); + } + } + } + + getErrorMessage = () => { + if (!this.isValid() && this.props.searchAttempted) { + return i18n.t(errorMessage); + } + return null; + } + + handleFilterOrgUnits = (searchText: ?string) => { + this.props.onFilterOrgUnits(this.props.searchId, searchText); + } + + renderOrgUnitField = () => { + const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; + return ( + + ); + } + + render() { + const { selectedOrgUnitScope } = this.props; + return ( +
{ this.gotoInstance = gotoInstance; }} + > + {this.renderOrgUnitScopeSelector()} + {selectedOrgUnitScope !== 'ACCESSIBLE' && this.renderOrgUnitField()} +
+ ); + } +} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js new file mode 100644 index 0000000000..9842e8fd9b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js @@ -0,0 +1,46 @@ +// @flow +import { connect } from 'react-redux'; +import { + setOrgUnitScope, + setOrgUnit, + requestFilterOrgUnits, + clearOrgUnitsFilter, +} from './searchOrgUnitSelector.actions'; +import { get as getOrgUnitRoots } from '../../../../FormFields/New/Fields/OrgUnitField/orgUnitRoots.store'; +import { SearchOrgUnitSelectorRefHandler } from './SearchOrgUnitSelectorRefHandler.component'; + +const mapStateToProps = (state: ReduxState, props: Object) => { + const searchId = props.searchId; + + const filteredRoots = getOrgUnitRoots(searchId); + const roots = filteredRoots || getOrgUnitRoots('searchRoots'); + + return { + selectedOrgUnit: state.teiSearch[searchId].selectedOrgUnit, + selectedOrgUnitScope: state.teiSearch[searchId].selectedOrgUnitScope, + treeRoots: roots, + treeSearchText: state.teiSearch[searchId].orgUnitsSearchText, + treeReady: !state.teiSearch[searchId].orgUnitsLoading, + treeKey: state.teiSearch[searchId].orgUnitsSearchText || 'initial', + }; +}; + +const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ + onFilterOrgUnits: (searchId: string, searchText: string) => { + const action = searchText ? + requestFilterOrgUnits(searchId, searchText) : + clearOrgUnitsFilter(searchId); + dispatch(action); + }, + onSetOrgUnit: (searchId: string, orgUnit: ?any) => { + dispatch(setOrgUnit(searchId, orgUnit)); + }, + onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => { + dispatch(setOrgUnitScope(searchId, orgUnitScope)); + }, +}); + +// $FlowFixMe[missing-annot] automated comment +export const SearchOrgUnitSelector = connect(mapStateToProps, mapDispatchToProps)( + SearchOrgUnitSelectorRefHandler, +); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js new file mode 100644 index 0000000000..9374a93993 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js @@ -0,0 +1,18 @@ +// @flow +import * as React from 'react'; +import { SearchOrgUnitSelector } from './SearchOrgUnitSelector.component'; + +type Props = { + innerRef: Function, +}; + +export const SearchOrgUnitSelectorRefHandler = (props: Props) => { + const { innerRef, ...passOnProps } = props; + return ( + // $FlowFixMe[cannot-spread-inexact] automated comment + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js new file mode 100644 index 0000000000..2208cf9233 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js @@ -0,0 +1,30 @@ +// @flow + +import { actionCreator } from '../../../../../actions/actions.utils'; + +export const actionTypes = { + TEI_SEARCH_REQUEST_FILTER_ORG_UNITS: 'TeiSearchRequestFilterOrgUnits', + TEI_SEARCH_CLEAR_ORG_UNITS_FILTER: 'TeiSearchClearOrgUnitsFilter', + TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED: 'TeiSearchFilteredOrgUnitsRetrieved', + TEI_SEARCH_FILTER_ORG_UNITS_FAILED: 'TeiSearchFilterOrgUnitsFailed', + TEI_SEARCH_SET_ORG_UNIT_SCOPE: 'TeiSearchSetOrgUnitScope', + TEI_SEARCH_SET_ORG_UNIT: 'TeiSearchSetOrgUnit', +}; + +export const setOrgUnitScope = (searchId: string, orgUnitScope: string) => + actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT_SCOPE)({ searchId, orgUnitScope }); + +export const setOrgUnit = (searchId: string, orgUnit: ?any) => + actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT)({ searchId, orgUnit }); + +export const requestFilterOrgUnits = (searchId: string, searchText: string) => + actionCreator(actionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS)({ searchId, searchText }); + +export const filteredOrgUnitsRetrieved = (searchId: string, roots: ?Array, searchText: string) => + actionCreator(actionTypes.TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED)({ searchId, roots, searchText }); + +export const filterOrgUnitsFailed = (searchId: string, error: any) => + actionCreator(actionTypes.TEI_SEARCH_FILTER_ORG_UNITS_FAILED)({ searchId, error }); + +export const clearOrgUnitsFilter = (searchId: string) => + actionCreator(actionTypes.TEI_SEARCH_CLEAR_ORG_UNITS_FILTER)({ searchId }); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js new file mode 100644 index 0000000000..f2fd91e6f8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js @@ -0,0 +1,82 @@ +// @flow +import log from 'loglevel'; +import isArray from 'd2-utilizr/lib/isArray'; +import { from } from 'rxjs'; +import { getD2 } from 'capture-core/d2/d2Instance'; +import { errorCreator } from 'capture-core-utils'; +import { ofType } from 'redux-observable'; +import { map, concatMap, takeUntil, filter } from 'rxjs/operators'; +import { + actionTypes as teiSearchActionTypes, +} from '../actions/teiSearch.actions'; + +import { + actionTypes as searchOrgUnitActionTypes, + filterOrgUnitsFailed, + filteredOrgUnitsRetrieved, +} from './searchOrgUnitSelector.actions'; + +import { set as setStoreRoots } from '../../../../FormFields/New/Fields/OrgUnitField/orgUnitRoots.store'; + +const RETRIEVE_ERROR = 'Could not retrieve registering unit list'; + + +const isInitializeTeiSearch = (action: Object, searchId: string) => + action.type === teiSearchActionTypes.INITIALIZE_TEI_SEARCH && + action.payload.searchId === searchId; + +const isRequestFilterOrgUnits = (action: Object, searchId: string) => + action.type === searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS && + action.payload.searchId === searchId; + + +const cancelActionFilter = (action: Object, searchId: string) => { + if (isArray(action.payload)) { + return action.payload.some(innerAction => isInitializeTeiSearch(innerAction, searchId)); + } + return isInitializeTeiSearch(action, searchId) || isRequestFilterOrgUnits(action, searchId); +}; + +// get organisation units based on search criteria +export const teiSearchFilterOrgUnitsEpic = (action$: InputObservable) => + action$.pipe( + ofType(searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS), + concatMap((action) => { + const searchText = action.payload.searchText; + const searchId = action.payload.searchId; + + return from(getD2() + .models + .organisationUnits + .list({ + fields: [ + 'id,displayName,path,publicAccess,access,lastUpdated', + 'children[id,displayName,publicAccess,access,path,children::isNotEmpty]', + ].join(','), + paging: true, + withinUserSearchHierarchy: true, + query: searchText, + pageSize: 15, + }) + .then(orgUnitCollection => ({ regUnitArray: orgUnitCollection.toArray(), searchText, searchId })) + .catch(error => ({ error, searchId })), + ).pipe(takeUntil(action$.pipe(filter(a => cancelActionFilter(a, searchId))))); + }), + map((resultContainer) => { + if (resultContainer.error) { + log.error(errorCreator(RETRIEVE_ERROR)( + { error: resultContainer.error, method: 'searchRegisteringUnitListEpic' }), + ); + return filterOrgUnitsFailed(resultContainer.searchId, RETRIEVE_ERROR); + } + + const regUnitArray = resultContainer.regUnitArray; + setStoreRoots(resultContainer.searchId, regUnitArray); + const regUnits = resultContainer.regUnitArray + .map(unit => ({ + id: unit.id, + path: unit.path, + displayName: unit.displayName, + })); + return filteredOrgUnitsRetrieved(resultContainer.searchId, regUnits, resultContainer.searchText); + })); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js new file mode 100644 index 0000000000..83cb72d687 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js @@ -0,0 +1,43 @@ +// @flow +import * as React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { OptionsSelectVirtualized } from '../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; +import type { + VirtualizedOptionConfig, +} from '../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; +import { withDefaultFieldContainer, withLabel } from '../../../../FormFields/New'; + +const SearchProgramField = withDefaultFieldContainer()(withLabel()(OptionsSelectVirtualized)); + +const programFieldStyles = { + labelContainerStyle: { + paddingTop: 12, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +type Props = { + searchId: string, + selectedProgramId: ?string, + onSetProgram: (searchId: string, programId: ?string) => void, + programOptions: Array, +} +export class SearchProgramSelectorComponent extends React.Component { + onSelectProgram = (programId: ?string) => { + this.props.onSetProgram(this.props.searchId, programId); + } + render() { + return ( + + ); + } +} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js new file mode 100644 index 0000000000..16ec57dea5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js @@ -0,0 +1,26 @@ +// @flow +import { connect } from 'react-redux'; +import { SearchProgramSelectorComponent } from './SearchProgramSelector.component'; +import { startSetProgram } from './searchProgramSelector.actions'; +import { makeProgramOptionsSelector } from './searchProgramSelector.selectors'; + +const makeMapStateToProps = () => { + const getProgramOptions = makeProgramOptionsSelector(); + const mapStateToProps = (state: ReduxState, props: Object) => ({ + selectedProgramId: state.teiSearch[props.searchId]?.selectedProgramId, + programOptions: getProgramOptions(state, props), + }); + // $FlowFixMe[not-an-object] automated comment + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ + onSetProgram: (searchId: string, programId: ?string) => { + dispatch(startSetProgram(searchId, programId)); + }, +}); + +// $FlowFixMe[missing-annot] automated comment +export const SearchProgramSelector = connect(makeMapStateToProps, mapDispatchToProps)( + SearchProgramSelectorComponent, +); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js new file mode 100644 index 0000000000..8fc8ad8358 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js @@ -0,0 +1,15 @@ +// @flow + +import { actionCreator } from '../../../../../actions/actions.utils'; + +export const batchActionTypes = { + BATCH_SET_TEI_SEARCH_PROGRAM: 'BatchSetTeiSearchProgram', +}; + +export const actionTypes = { + TEI_SEARCH_SET_PROGRAM: 'TeiSearchSetProgram', + TEI_SEARCH_START_SET_PROGRAM: 'TeiSearchStartSetProgram', +}; + +export const startSetProgram = (searchId: string, programId: ?string) => + actionCreator(actionTypes.TEI_SEARCH_START_SET_PROGRAM)({ searchId, programId }); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js new file mode 100644 index 0000000000..a207519d39 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js @@ -0,0 +1,19 @@ +// @flow +import { createSelector } from 'reselect'; +import { programCollection } from '../../../../../metaDataMemoryStores'; +import { TrackerProgram } from '../../../../../metaData'; + +const trackedEntityTypeIdSelector = (state, props) => state.teiSearch[props.searchId].selectedTrackedEntityTypeId; + +// $FlowFixMe +export const makeProgramOptionsSelector = () => createSelector( + trackedEntityTypeIdSelector, + (trackedEntityTypeId: string) => + Array.from(programCollection.values()) + .filter(program => + program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.read, + ) + .map(p => ({ value: p.id, label: p.name })), +); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js new file mode 100644 index 0000000000..85f4cc09e0 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js @@ -0,0 +1,162 @@ +// @flow +import React, { type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import withStyles from '@material-ui/core/styles/withStyles'; +import { SearchGroup } from '../../../../metaData'; +import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; +import { TeiSearchResults } from '../../../TeiSearch/TeiSearchResults/TeiSearchResults.container'; +import { SearchProgramSelector } from '../../../TeiSearch/SearchProgramSelector/SearchProgramSelector.container'; +import { Section, SectionHeaderSimple } from '../../../Section'; +import { ResultsPageSizeContext } from '../../../Pages/shared-contexts'; +import type { Props } from './TeiSearch.types'; + +const getStyles = (theme: Theme) => ({ + container: { + margin: theme.typography.pxToRem(10), + }, + programSection: { + backgroundColor: 'white', + maxWidth: theme.typography.pxToRem(900), + marginBottom: theme.typography.pxToRem(20), + }, + formContainerSection: { + maxWidth: theme.typography.pxToRem(900), + marginBottom: theme.typography.pxToRem(20), + }, +}); + +type State = { + programSectionOpen: boolean, +} + +class TeiSearchPlain extends React.Component { + constructor(props) { + super(props); + this.state = { programSectionOpen: true }; + } + + + getFormId = (searchGroupId: string) => { + const contextId = this.props.selectedProgramId || this.props.selectedTrackedEntityTypeId || ''; + return `${this.props.id}-${contextId}-${searchGroupId}`; + } + + handleSearch = (formId: string, searchGroupId: string) => { + const { id } = this.props; + this.props.onSearch(formId, searchGroupId, id); + } + + handleSearchResultsChangePage = (pageNumber: number) => { + this.props.onSearchResultsChangePage(this.props.id, pageNumber); + } + + handleNewSearch = () => { + this.props.onNewSearch(this.props.id); + } + + handleEditSearch = () => { + this.props.onEditSearch(this.props.id); + } + + handleSearchValidationFailed = (...args) => { + const { id } = this.props; + this.props.onSearchValidationFailed(...args, id); + } + + renderSearchForms = (searchGroups: Array) => ( +
+ {this.renderProgramSection()} + {this.renderSearchGroups(searchGroups)} +
+ ); + + renderProgramSection = () => { + const isCollapsed = !this.state.programSectionOpen; + return ( +
{ this.setState({ programSectionOpen: !!isCollapsed }); }} + title={i18n.t('Program')} + /> + } + > + +
+ ); + } + + onChangeSectionCollapseState = (id) => { + if (this.props.openSearchGroupSection === id) { + this.props.onSetOpenSearchGroupSection(this.props.id, null); + return; + } + this.props.onSetOpenSearchGroupSection(this.props.id, id); + } + + renderSearchGroups = (searchGroups: Array) => searchGroups.map((sg, i) => { + const searchGroupId = i.toString(); + const formId = this.getFormId(searchGroupId); + const header = sg.unique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : i18n.t('Search by attributes'); + const collapsed = this.props.openSearchGroupSection !== searchGroupId; + return ( +
{ this.onChangeSectionCollapseState(searchGroupId); }} + isCollapsed={collapsed} + title={header} + />} + > + +
+ ); + }) + renderSearchResult = () => { + const { + id, + searchGroups, + getResultsView, + } = this.props; + return ( + + + + ); + } + + render() { + const searchGroups = this.props.searchGroups; + + if (this.props.showResults) { + return this.renderSearchResult(); + } + + return searchGroups ? this.renderSearchForms(searchGroups) : (
); + } +} + +export const TeiSearchComponent: ComponentType<$Diff> = withStyles(getStyles)(TeiSearchPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js new file mode 100644 index 0000000000..b4cd8592bd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js @@ -0,0 +1,57 @@ +// @flow +import { type ComponentType } from 'react'; +import { connect } from 'react-redux'; +import { TeiSearchComponent } from './TeiSearch.component'; +import { + requestSearchTei, + searchFormValidationFailed, + teiNewSearch, + teiEditSearch, + teiSearchResultsChangePage, + setOpenSearchGroupSection, +} from './actions/teiSearch.actions'; +import { makeSearchGroupsSelector } from './teiSearch.selectors'; +import type { Props, OwnProps } from './TeiSearch.types'; + +const makeMapStateToProps = () => { + const searchGroupsSelector = makeSearchGroupsSelector(); + + const mapStateToProps = (state: ReduxState, props: OwnProps) => { + const searchGroups = searchGroupsSelector(state, props); + const currentTeiSearch = state.teiSearch[props.id]; + return { + searchGroups, + showResults: !!currentTeiSearch.searchResults, + selectedProgramId: currentTeiSearch.selectedProgramId, + selectedTrackedEntityTypeId: currentTeiSearch.selectedTrackedEntityTypeId, + openSearchGroupSection: currentTeiSearch.openSearchGroupSection, + }; + }; + + // $FlowFixMe[not-an-object] automated comment + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: ReduxDispatch, ownProps: OwnProps) => ({ + onSearch: (formId: string, searchGroupId: string, searchId: string) => { + dispatch(requestSearchTei(formId, searchGroupId, searchId, ownProps.resultsPageSize)); + }, + onSearchResultsChangePage: (searchId: string, pageNumber: number) => { + dispatch(teiSearchResultsChangePage(searchId, pageNumber, ownProps.resultsPageSize)); + }, + onSearchValidationFailed: (formId: string, searchGroupId: string, searchId: string) => { + dispatch(searchFormValidationFailed(formId, searchGroupId, searchId)); + }, + onNewSearch: (searchId: string) => { + dispatch(teiNewSearch(searchId)); + }, + onEditSearch: (searchId: string) => { + dispatch(teiEditSearch(searchId)); + }, + onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => { + dispatch(setOpenSearchGroupSection(searchId, searchGroupId)); + }, +}); + +export const TeiSearch: ComponentType = + connect<$Diff, OwnProps, _, _, _, _>(makeMapStateToProps, mapDispatchToProps)(TeiSearchComponent); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js new file mode 100644 index 0000000000..dffb5de485 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js @@ -0,0 +1,27 @@ +// @flow +import { type SearchGroup } from '../../../../metaData'; + +type PropsFromRedux = {| + searchGroups: ?Array, + showResults?: ?boolean, + openSearchGroupSection: ?string, +|} + +type DispatchersFromRedux = {| + onSearch: Function, + onSearchValidationFailed: Function, + onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => void, + onSearchResultsChangePage: (searchId: string, pageNumber: number) => void, + onNewSearch: (searchId: string) => void, + onEditSearch: (searchId: string) => void, +|} + +export type OwnProps = {| + id: string, + getResultsView: Function, + resultsPageSize: number, + selectedProgramId: ?string, + selectedTrackedEntityTypeId: ?string, +|} + +export type Props = {| ...OwnProps, ...DispatchersFromRedux, ...PropsFromRedux, ...CssClasses |} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js new file mode 100644 index 0000000000..fedd33b19d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js @@ -0,0 +1,179 @@ +// @flow +import * as React from 'react'; +import log from 'loglevel'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import classNames from 'classnames'; +import { errorCreator } from 'capture-core-utils'; +import { Button } from '../../../../Buttons'; +import { D2Form } from '../../../../D2Form'; +import { SearchOrgUnitSelector } from '../../../../TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container'; +import type { SearchGroup } from '../../../../../metaData'; +import { withGotoInterface } from '../../../../FormFields/New'; + +const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); + +const getStyles = (theme: Theme) => ({ + orgUnitSection: { + backgroundColor: 'white', + padding: theme.typography.pxToRem(8), + maxWidth: theme.typography.pxToRem(892), + }, + searchButtonContainer: { + padding: theme.typography.pxToRem(10), + display: 'flex', + alignItems: 'center', + }, + minAttributesRequired: { + flexGrow: 1, + textAlign: 'right', + fontSize: theme.typography.pxToRem(14), + }, + minAttribtuesRequiredInvalid: { + color: theme.palette.error.main, + }, +}); + +type Props = { + id: string, + searchGroupId: string, + onSearch: (formId: string, searchGroupId: string) => void, + onSearchValidationFailed: (formId: string, SearchGroupId: string) => void, + searchAttempted: boolean, + searchId: string, + searchGroup: SearchGroup, + attributesWithValuesCount: number, + classes: { + container: string, + searchButtonContainer: string, + orgUnitSection: string, + minAttributesRequired: string, + minAttribtuesRequiredInvalid: string, + }, +}; + +class SearchFormPlain extends React.Component { + formInstance: any; + orgUnitSelectorInstance: SearchOrgUnitSelector; + + static errorMessages = { + NO_ITEM_SELECTED: 'No item selected', + SEARCH_FORM_MISSING: 'search form is missing. see log for details', + }; + + validNumberOfAttributes = () => { + const attributesWithValuesCount = this.props.attributesWithValuesCount; + const minAttributesRequiredToSearch = this.props.searchGroup.minAttributesRequiredToSearch; + return attributesWithValuesCount >= minAttributesRequiredToSearch; + } + + validateForm() { + if (!this.formInstance) { + log.error( + errorCreator( + SearchFormPlain.errorMessages.SEARCH_FORM_MISSING)({ Search: this }), + ); + return { + error: true, + isValid: false, + }; + } + + let isValid = this.formInstance.validateFormScrollToFirstFailedField({}); + + // $FlowFixMe[prop-missing] automated comment + if (isValid && !this.props.searchGroup.unique) isValid = this.orgUnitSelectorInstance.validateAndScrollToIfFailed(); + + if (isValid && !this.props.searchGroup.unique) isValid = this.validNumberOfAttributes(); + + return { + isValid, + error: false, + }; + } + + handleSearchAttempt = () => { + const { error: validateFormError, isValid: isFormValid } = this.validateForm(); + if (validateFormError || !isFormValid) { + this.props.onSearchValidationFailed(this.props.id, this.props.searchGroupId); + return; + } + this.props.onSearch(this.props.id, this.props.searchGroupId); + } + + getUniqueSearchButtonText = (searchForm) => { + const attributeName = searchForm.getElements()[0].formName; + return `Search ${attributeName}`; + } + + renderOrgUnitSelector = () => ( + { + this.orgUnitSelectorInstance = instance; + }} + searchId={this.props.searchId} + searchAttempted={this.props.searchAttempted} + /> + ); + + renderMinAttributesRequired = () => { + const { classes, searchAttempted, searchGroup } = this.props; + const displayInvalidNumberOfAttributes = searchAttempted && !this.validNumberOfAttributes(); + const minAttributesRequiredClass = classNames( + classes.minAttributesRequired, { + [classes.minAttribtuesRequiredInvalid]: displayInvalidNumberOfAttributes, + }, + ); + + return ( +
+ {i18n.t( + 'Fill in at least {{minAttributesRequired}} attributes to search', + { + minAttributesRequired: searchGroup.minAttributesRequiredToSearch, + })} +
+ ); + } + + render() { + const { searchGroup, classes, id } = this.props; + + const searchForm = searchGroup && searchGroup.searchForm; + + if (!searchForm) { + return ( +
+ {SearchFormPlain.errorMessages.SEARCH_FORM_MISSING} +
+ ); + } + const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) : i18n.t('Search by attributes'); + return ( +
+ { this.formInstance = formInstance; }} + formFoundation={searchGroup.searchForm} + id={id} + /> + {!searchGroup.unique && this.renderOrgUnitSelector()} +
+ + {!searchGroup.unique && this.renderMinAttributesRequired()} +
+
+ ); + } +} + +export const TeiSearchFormComponent = withStyles(getStyles)(SearchFormPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js new file mode 100644 index 0000000000..898d2db20c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js @@ -0,0 +1,23 @@ +// @flow +import { connect } from 'react-redux'; +import { TeiSearchFormComponent } from './TeiSearchForm.component'; + +const getAttributesWithValuesCount = (state: ReduxState, formId: string) => { + const formValues = state.formsValues[formId] || {}; + return Object.keys(formValues).filter(key => formValues[key]).length; +}; + +const mapStateToProps = (state: ReduxState, props: Object) => { + const searchId = props.searchId; + const formId = props.id; + const formState = state.teiSearch[searchId] && state.teiSearch[searchId][formId] ? state.teiSearch[searchId][formId] : {}; + + return { + searchAttempted: formState.validationFailed, + attributesWithValuesCount: getAttributesWithValuesCount(state, formId), + }; +}; + +// $FlowSuppress +// $FlowFixMe[missing-annot] automated comment +export const TeiSearchForm = connect(mapStateToProps, () => ({}))(TeiSearchFormComponent); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js new file mode 100644 index 0000000000..0ca0ad0668 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js @@ -0,0 +1,11 @@ +// @flow +import React from 'react'; +import type { Props } from './TeiSearchResults.types'; + + +export const TeiSearchResultsComponent = ({ getResultsView, ...passOnProps }: Props) => ( +
+ { getResultsView && getResultsView(passOnProps)} +
+); + diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js new file mode 100644 index 0000000000..70e5c557dd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js @@ -0,0 +1,31 @@ +// @flow +import { type ComponentType } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { TeiSearchResultsComponent } from './TeiSearchResults.component'; +import { withLoadingIndicator } from '../../../../../HOC'; +import type { OwnProps, Props } from './TeiSearchResults.types'; + +const mapStateToProps = (state: ReduxState, props: OwnProps) => { + const currentTeiSearch = state.teiSearch[props.id] || {}; + const searchResults = currentTeiSearch.searchResults || {}; + const searchValues = state.formsValues[searchResults.formId]; + const searchGroup = props.searchGroups[parseInt(searchResults.searchGroupId, 10)]; + return { + resultsLoading: searchResults.resultsLoading, + teis: searchResults.teis || [], + currentPage: searchResults.currentPage, + searchValues, + selectedProgramId: currentTeiSearch.selectedProgramId, + selectedTrackedEntityTypeId: currentTeiSearch.selectedTrackedEntityTypeId, + searchGroup, + }; +}; + +const mapDispatchToProps = () => ({}); + +export const TeiSearchResults: ComponentType = + compose( + connect(mapStateToProps, mapDispatchToProps), + withLoadingIndicator(() => ({ padding: '100px 0' }), null, props => (!props.resultsLoading)), + )(TeiSearchResultsComponent); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js new file mode 100644 index 0000000000..a9fbfae49d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js @@ -0,0 +1,23 @@ +// @flow +import { type SearchGroup } from '../../../../../metaData'; + +export type OwnProps = {| + id: string, + searchGroups: any, + onChangePage: Function, + onNewSearch: Function, + onEditSearch: Function, + getResultsView: Function, +|} + +type PropsFromRedux = {| + resultsLoading: boolean, + teis: any, + currentPage: number, + searchValues: any, + selectedProgramId: string, + selectedTrackedEntityTypeId: string, + searchGroup: SearchGroup +|} + +export type Props = {|...OwnProps, ...PropsFromRedux |} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js new file mode 100644 index 0000000000..a8c87ce2c7 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js @@ -0,0 +1,70 @@ +// @flow + +import { actionCreator } from '../../../../../actions/actions.utils'; + +export const batchActionTypes = { + BATCH_SET_TEI_SEARCH_PROGRAM_AND_TET: 'BatchSetTeiSearchProgramAndTet', + RESET_SEARCH_FORMS: 'ResetSearchForms', +}; + +export const actionTypes = { + INITIALIZE_TEI_SEARCH: 'InitializeTeiSearch', + REQUEST_SEARCH_TEI: 'RequestSearchTei', + SEARCH_FORM_VALIDATION_FAILED: 'SearchFormValidationFailed', + SEARCH_TEI_FAILED: 'SearchTeiFailed', + SEARCH_TEI_RESULT_RETRIEVED: 'SearchTeiResultRetrieved', + SET_TEI_SEARCH_PROGRAM_AND_TET: 'SetTeiSearchProgramAndTet', + TEI_NEW_SEARCH: 'TeiNewSearch', + TEI_EDIT_SEARCH: 'TeiEditSearch', + TEI_SEARCH_RESULTS_CHANGE_PAGE: 'TeiSearchResultsChangePage', + TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION: 'TeiSearchSetOpenSearchGroupSection', +}; + + +export const initializeTeiSearch = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => + actionCreator(actionTypes.INITIALIZE_TEI_SEARCH)({ searchId, programId, trackedEntityTypeId }); + +export const requestSearchTei = ( + formId: string, + searchGroupId: string, + searchId: string, + resultsPageSize: number, +) => + actionCreator(actionTypes.REQUEST_SEARCH_TEI)({ formId, searchGroupId, searchId, resultsPageSize }); + +export const searchTeiFailed = ( + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_TEI_FAILED)({ formId, searchGroupId, searchId }); + +export const searchTeiResultRetrieved = ( + data: any, + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_TEI_RESULT_RETRIEVED)({ data, formId, searchGroupId, searchId }); + +export const setProgramAndTrackedEntityType = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => + actionCreator(actionTypes.SET_TEI_SEARCH_PROGRAM_AND_TET)({ searchId, programId, trackedEntityTypeId }); + +export const searchFormValidationFailed = ( + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_FORM_VALIDATION_FAILED)({ formId, searchGroupId, searchId }); + +export const teiNewSearch = (searchId: string) => + actionCreator(actionTypes.TEI_NEW_SEARCH)({ searchId }); + +export const teiEditSearch = (searchId: string) => + actionCreator(actionTypes.TEI_EDIT_SEARCH)({ searchId }); + +export const teiSearchResultsChangePage = (searchId: string, pageNumber: number, resultsPageSize: number) => + actionCreator(actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE)({ searchId, pageNumber, resultsPageSize }); + +export const setOpenSearchGroupSection = (searchId: string, searchGroupId: ?string) => + actionCreator(actionTypes.TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION)({ searchId, searchGroupId }); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js new file mode 100644 index 0000000000..79ac1f715e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js @@ -0,0 +1,19 @@ +// @flow +import { + getTrackedEntityTypeThrowIfNotFound, + getTrackerProgramThrowIfNotFound, +} from '../../../../metaData'; +import type { + SearchGroup, +} from '../../../../metaData'; + + +export function getSearchGroups(trackedEntityTypeId: string, programId: ?string): Array { + if (programId) { + const program = getTrackerProgramThrowIfNotFound(programId); + return program.searchGroups; + } + const trackedEntityType = getTrackedEntityTypeThrowIfNotFound(trackedEntityTypeId); + return trackedEntityType.searchGroups; +} + diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js new file mode 100644 index 0000000000..ffec9a743d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js @@ -0,0 +1,23 @@ +// @flow +import { createSelector } from 'reselect'; +import { getSearchGroups } from './getSearchGroups'; + +const trackedEntityTypeIdSelector = (state, props) => state.teiSearch[props.id].selectedTrackedEntityTypeId; +const programIdSelector = (state, props) => state.teiSearch[props.id].selectedProgramId; + +// $FlowFixMe[missing-annot] automated comment +export const makeSearchGroupsSelector = () => createSelector( + trackedEntityTypeIdSelector, + programIdSelector, + (trackedEntityTypeId: string, programId: ?string) => + getSearchGroups(trackedEntityTypeId, programId) + .sort((a, b) => { + if (a.unique === b.unique) { + return 0; + } + if (a.unique) { + return -1; + } + return 1; + }), +); diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index f692861928..ebdc606e6d 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -223,6 +223,9 @@ import { import { scheduleNewEnrollmentEventEpic, } from '../core_modules/capture-core/components/WidgetEventSchedule'; +import { + openRelationshipTeiSearchForWidgetEpic, +} from '../core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics'; export const epics = combineEpics( resetProgramAfterSettingOrgUnitIfApplicableEpic, @@ -273,6 +276,7 @@ export const epics = combineEpics( showRegisteringUnitListIndicatorEpic, openRelationshipTeiSearchEpic, requestRelationshipTeiSearchEpic, + openRelationshipTeiSearchForWidgetEpic, TeiRelationshipNewOrEditSearchEpic, teiSearchEpic, teiSearchChangePageEpic, From f9dd6e1e30f002ffb696855be78d36b0a0cd67c4 Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Fri, 25 Feb 2022 21:31:49 +0100 Subject: [PATCH 27/95] fix: close modal on link complete --- .../NewTrackedEntityRelationship.container.js | 4 +++- .../NewTrackedEntityRelationship.types.js | 1 + .../WidgetTrackedEntityRelationship.js | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index f4f7a20307..ee4c36ee79 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -42,6 +42,7 @@ const styles = { export const NewTrackedEntityRelationshipPlain = ({ renderRef, showDialog, + setShowDialog, hideDialog, relationshipTypes, trackedEntityType, @@ -74,8 +75,9 @@ export const NewTrackedEntityRelationshipPlain = ({ }; dispatch(requestSaveRelationshipForTei({ serverData })); + setShowDialog(); } - }, [dispatch, selectedRelationshipType, teiId]); + }, [dispatch, selectedRelationshipType, setShowDialog, teiId]); const pageStatus = useMemo(() => { if (!selectedRelationshipType) { diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index face528b63..df39ba36ed 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -5,6 +5,7 @@ import type { RelationshipType } from '../WidgetTrackedEntityRelationship.types' export type Props = {| renderRef: Object, relationshipTypes: Array, + setShowDialog: () => void, trackedEntityType: string, showDialog: boolean, hideDialog: () => void, diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js index a2b8420089..11f8683826 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js @@ -23,6 +23,7 @@ export const WidgetTrackedEntityRelationship = ({ ...PassOnProps }: Props) => { From 7d8b4b7f4489947d0cd99c3944e53c6fe3556888 Mon Sep 17 00:00:00 2001 From: EirikHaugstulen Date: Fri, 25 Feb 2022 21:37:22 +0100 Subject: [PATCH 28/95] fix: LGTM --- .../TeiSearch/TeiSearch.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js index 85f4cc09e0..f918e43229 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js +++ b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js @@ -2,7 +2,7 @@ import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import withStyles from '@material-ui/core/styles/withStyles'; -import { SearchGroup } from '../../../../metaData'; +import type { SearchGroup } from '../../../../metaData'; import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; import { TeiSearchResults } from '../../../TeiSearch/TeiSearchResults/TeiSearchResults.container'; import { SearchProgramSelector } from '../../../TeiSearch/SearchProgramSelector/SearchProgramSelector.container'; From 363d2141c250f254a9486e3dfea148cb58e31699 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 23 Mar 2022 15:42:11 +0000 Subject: [PATCH 29/95] chore: [DHIS2-12362] refactor --- .../EnrollmentPageDefault.container.js | 3 +- .../EnrollmentPageDefault.types.js | 1 - .../EnrollmentPageDefault/hooks/constants.js | 2 +- .../hooks/useRelationships.js | 163 ++++++++---------- .../EnrollmentEditEventPage.container.js | 2 +- 5 files changed, 73 insertions(+), 98 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 9efcc905f1..0eefc3792f 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -35,7 +35,7 @@ export const EnrollmentPageDefault = () => { } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - const { teiRelationships, enrollmentRelationships } = useRelationships(teiId, relationships); + const { relationships: teiRelationships } = useRelationships(teiId, relationships); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( @@ -92,7 +92,6 @@ export const EnrollmentPageDefault = () => { events={enrollment?.events} enrollmentId={enrollmentId} teiRelationships={teiRelationships} - enrollmentRelationships={enrollmentRelationships} onAddNew={onAddNew} onDelete={onDelete} onViewAll={onViewAll} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 22afb0c795..36de4b6db0 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -11,7 +11,6 @@ export type Props = {| events: ?Array, stages?: Array, teiRelationships: Array, - enrollmentRelationships: Array, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, orgUnitId: string, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js index ce8a088dba..fd04ea1e90 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js @@ -24,7 +24,7 @@ export const getDisplayFieldsFromAPI = { { id: 'orgUnitName', label: 'Organisation unit' }, { id: 'program', label: 'Program', - convertValue: props => props.programName, + convertValue: props => props?.programName, }, { id: 'eventDate', label: 'Event date', diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js index 30057effc8..313a7b3779 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js @@ -1,5 +1,7 @@ // @flow import { useCallback, useEffect, useState } from 'react'; +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; import { getCachedSingleResourceFromKeyAsync, } from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; @@ -9,107 +11,103 @@ import { userStores } from '../../../../../storageControllers/stores'; import type { InputRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getDisplayFieldsFromAPI, getBaseConfigHeaders, relationshipWidgetTypes } from './constants'; +import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; +import { TrueOnlyField } from '../../../../FormFields/New'; -/* eslint-disable complexity */ -const getRelationshipAttributes = ( +const convertAttributes = (attributes, displayFields, options) => displayFields.map((item) => { + if (item.convertValue) { + return { + id: item.id, + value: item.convertValue(options), + }; + } + const attributeItem = Array.isArray(attributes) + ? attributes.find(({ attribute }) => attribute === item.id)?.value : attributes[item.id]; + return { + id: item.id, + value: attributeItem, + }; +}); + +const getDisplayFields = (type) => { + let displayFields = getDisplayFieldsFromAPI[type]; + if (!displayFields?.length) { + displayFields = getBaseConfigHeaders[type]; + } + return displayFields; +}; + + +const getAttributeConstraintsForTEI = ( relationshipType: Object, teiId: string, from: RelationshipData, to: RelationshipData, - relationship: Object, ) => { const { bidirectional, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; - const getAttributes = (attributes, displayFields, options) => displayFields.map((item) => { - if (item.convertValue) { - return { - id: item.id, - value: item.convertValue(options), - }; - } - const attributeItem = Array.isArray(attributes) - ? attributes.find(({ attribute }) => attribute === item.id)?.value : attributes[item.id]; - return { - id: item.id, - value: attributeItem, - }; - }); - - const getDisplayFields = (type) => { - let displayFields = getDisplayFieldsFromAPI[type]; - if (!displayFields.length) { - displayFields = getBaseConfigHeaders[type]; - } - return displayFields; - }; - if (to?.trackedEntityInstance && to?.trackedEntityInstance?.trackedEntityInstance !== teiId) { - const displayFields = getDisplayFields(toConstraint.relationshipEntity); - const tet = getTrackedEntityTypeThrowIfNotFound(toConstraint.trackedEntityType.id); - const attributes = getAttributes(to.trackedEntityInstance.attributes, displayFields, - { ...relationship, trackedEntityTypeName: tet.name }); - return { id: to.trackedEntityInstance.trackedEntityInstance, + constraint: toConstraint, + attributes: to.trackedEntityInstance.attributes, relationshipName: fromToName, - relationshipProgram: toConstraint.program, - attributes, - displayFields, - widgetType: relationshipWidgetTypes.TET_RELATIONSHIP, + options: null, }; } else if (bidirectional && from?.trackedEntityInstance && from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { - const displayFields = getDisplayFields(fromConstraint.relationshipEntity); - const tet = getTrackedEntityTypeThrowIfNotFound(toConstraint.trackedEntityType.id); - const attributes = getAttributes(from.trackedEntityInstance.attributes, displayFields, - { ...relationship, trackedEntityTypeName: tet.name }); - return { id: from.trackedEntityInstance.trackedEntityInstance, + constraint: fromConstraint, + attributes: from.trackedEntityInstance.attributes, relationshipName: toFromName, - relationshipProgram: fromConstraint.program, - attributes, - displayFields, - widgetType: relationshipWidgetTypes.TET_RELATIONSHIP, + options: null, }; } else if (bidirectional && from?.event) { - const displayFields = getDisplayFields(fromConstraint.relationshipEntity); // $FlowFixMe const { stage, program } = getProgramAndStageFromEvent({ evenId: from.event.event, programId: from.event.program, programStageId: from.event.programStage, }); - const attributes = getAttributes(from.event, displayFields, { - ...from.event, - programName: program?.name, - programStageName: stage?.stageForm?.name, - }); - return { id: from.event.event, + constraint: fromConstraint, + attributes: from.event, relationshipName: toFromName, - relationshipProgram: fromConstraint.program, - attributes, - displayFields, - widgetType: relationshipWidgetTypes.EVENT_RELATIONSHIP, - }; - } else if (from.enrollment && from.enrollment.enrollment) { - const displayFields = getDisplayFields(fromConstraint.relationshipEntity); - const attributes = getAttributes(from.enrollment, displayFields, - { ...from.enrollment }); - - return { - id: from.enrollment.enrollment, - relationshipName: toFromName, - attributes, - displayFields, - widgetType: relationshipWidgetTypes.ENROLLMENT_RELATIONSHIP, + options: { + ...from.event, + programName: program?.name, + programStageName: stage?.stageForm?.name, + }, }; } + log.error(errorCreator('Relationship type is not handled')({ relationshipType })); +}; - return {}; +const getRelationshipAttributes = ( + relationshipType: Object, + teiId: string, + from: RelationshipData, + to: RelationshipData, + relationship: Object, +) => { + const data = getAttributeConstraintsForTEI(relationshipType, teiId, from, to); + if (!data) { return {}; } + const { id, relationshipName, constraint, attributes: constraintAttributes, options: constraintOptions } = data; + let options = constraintOptions; + if (!options) { + const tet = getTrackedEntityTypeThrowIfNotFound(constraint.trackedEntityType.id); + options = { ...relationship, trackedEntityTypeName: tet.name }; + } + const displayFields = getDisplayFields(constraint.relationshipEntity); + const attributes = convertAttributes(constraintAttributes, displayFields, options); + return { + id, + relationshipName, + attributes, + displayFields, + }; }; @@ -127,8 +125,8 @@ export const useRelationships = (teiId: string, relationships?: Array result.response); - const { relationshipName, displayFields, id, attributes, widgetType } = getRelationshipAttributes( - relationshipType, teiId, from, to, { relationship }, + const { relationshipName, displayFields, id, attributes } = getRelationshipAttributes( + relationshipType, teiId, from, to, relationship, ); const typeExist = groupped.find(item => item.id === typeId); @@ -140,7 +138,6 @@ export const useRelationships = (teiId: string, relationships?: Array { - switch (widgetType) { - case relationshipWidgetTypes.TET_RELATIONSHIP: - acc.teiRelationships.push(currentRel); - break; - case relationshipWidgetTypes.ENROLLMENT_RELATIONSHIP: - acc.enrollmentRelationships.push(currentRel); - break; - case relationshipWidgetTypes.EVENT_RELATIONSHIP: - acc.eventRelationships.push(currentRel); - break; - default: - break; - } - return acc; - }, { teiRelationships: [], enrollmentRelationships: [], eventRelationships: [] }); - return { - teiRelationships, - enrollmentRelationships, - eventRelationships, + relationships: relationshipsByType, }; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index bb3a60b413..d1a069b7ec 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -52,7 +52,7 @@ export const EnrollmentEditEventPage = () => { ? (pageStatus = pageStatuses.DEFAULT) : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; - const { teiRelationships } = useRelationships(teiId, relationships); + const { relationships: teiRelationships } = useRelationships(teiId, relationships); return ( Date: Thu, 24 Mar 2022 07:38:27 +0000 Subject: [PATCH 30/95] chore: [DHIS2-12362] rename funcs --- .../EnrollmentPageDefault/EnrollmentPageDefault.container.js | 4 ++-- .../Pages/Enrollment/EnrollmentPageDefault/hooks/index.js | 2 +- .../hooks/{useRelationships.js => useTeiRelationships.js} | 3 +-- .../EnrollmentEditEvent/EnrollmentEditEventPage.container.js | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) rename src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/{useRelationships.js => useTeiRelationships.js} (97%) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 0eefc3792f..e5c87eb79d 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -14,7 +14,7 @@ import { useProgramStages, useOrganisationUnit, useRuleEffects, - useRelationships, + useTeiRelationships, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; @@ -35,7 +35,7 @@ export const EnrollmentPageDefault = () => { } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - const { relationships: teiRelationships } = useRelationships(teiId, relationships); + const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js index a37f7d1a50..872631e10b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js @@ -5,5 +5,5 @@ export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; export { useProgramStages } from './useProgramStages'; export { useOrganisationUnit } from './useOrganisationUnit'; export { useRuleEffects } from './useRuleEffects'; -export { useRelationships } from './useRelationships'; +export { useTeiRelationships } from './useTeiRelationships'; export type { UseRuleEffectsInput } from './useRuleEffects.types'; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js similarity index 97% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js rename to src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js index 313a7b3779..24a19a993a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js @@ -12,7 +12,6 @@ import type { InputRelationship, RelationshipData, } from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; -import { TrueOnlyField } from '../../../../FormFields/New'; const convertAttributes = (attributes, displayFields, options) => displayFields.map((item) => { if (item.convertValue) { @@ -111,7 +110,7 @@ const getRelationshipAttributes = ( }; -export const useRelationships = (teiId: string, relationships?: Array) => { +export const useTeiRelationships = (teiId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); const computeData = useCallback(async () => { diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index d1a069b7ec..39c972cfce 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -9,7 +9,7 @@ import { useProgramInfo } from '../../../hooks/useProgramInfo'; import { pageMode, pageStatuses } from './EnrollmentEditEventPage.constants'; import { EnrollmentEditEventPageComponent } from './EnrollmentEditEventPage.component'; import { useWidgetDataFromStore } from '../EnrollmentAddEvent/hooks'; -import { useHideWidgetByRuleLocations, useRelationships } from '../Enrollment/EnrollmentPageDefault/hooks'; +import { useHideWidgetByRuleLocations, useTeiRelationships } from '../Enrollment/EnrollmentPageDefault/hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { deleteEnrollment } from '../Enrollment/EnrollmentPage.actions'; import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; @@ -52,7 +52,7 @@ export const EnrollmentEditEventPage = () => { ? (pageStatus = pageStatuses.DEFAULT) : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; - const { relationships: teiRelationships } = useRelationships(teiId, relationships); + const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); return ( Date: Thu, 24 Mar 2022 13:46:16 +0000 Subject: [PATCH 31/95] chore: [DHIS2-12362] use new API --- .../hooks/useTeiRelationships.js | 37 ++++++++++--------- .../useCommonEnrollmentDomainData.js | 5 ++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js index 24a19a993a..90fc2cd933 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js @@ -45,24 +45,7 @@ const getAttributeConstraintsForTEI = ( ) => { const { bidirectional, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; - if (to?.trackedEntityInstance && to?.trackedEntityInstance?.trackedEntityInstance !== teiId) { - return { - id: to.trackedEntityInstance.trackedEntityInstance, - constraint: toConstraint, - attributes: to.trackedEntityInstance.attributes, - relationshipName: fromToName, - options: null, - }; - } else if (bidirectional && from?.trackedEntityInstance && - from?.trackedEntityInstance?.trackedEntityInstance !== teiId) { - return { - id: from.trackedEntityInstance.trackedEntityInstance, - constraint: fromConstraint, - attributes: from.trackedEntityInstance.attributes, - relationshipName: toFromName, - options: null, - }; - } else if (bidirectional && from?.event) { + if (bidirectional && from?.event) { // $FlowFixMe const { stage, program } = getProgramAndStageFromEvent({ evenId: from.event.event, @@ -80,8 +63,26 @@ const getAttributeConstraintsForTEI = ( programStageName: stage?.stageForm?.name, }, }; + } else if (to?.trackedEntity && to?.trackedEntity?.trackedEntityInstance !== teiId) { + return { + id: to.trackedEntity.trackedEntity, + constraint: toConstraint, + attributes: to.trackedEntity.attributes, + relationshipName: fromToName, + options: null, + }; + } else if (bidirectional && from?.trackedEntity && + from?.trackedEntity?.trackedEntity !== teiId) { + return { + id: from.trackedEntity.trackedEntity, + constraint: fromConstraint, + attributes: from.trackedEntity.attributes, + relationshipName: toFromName, + options: null, + }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); + return {}; }; const getRelationshipAttributes = ( diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index 7c39437ccd..fd38efa9ec 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -41,9 +41,10 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin useMemo( () => ({ teiRelationships: { - resource: 'relationships', + resource: 'tracker/relationships', params: ({ variables: { teiId: updatedTeiId } }) => ({ tei: updatedTeiId, + fields: ['relationshipType,to,from'], }), }, }), @@ -57,7 +58,7 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin enrollment: data?.trackedEntityInstance?.enrollments ?.find(enrollment => enrollment.enrollment === enrollmentId), attributeValues: data?.trackedEntityInstance?.attributes, - relationships: relationshipsData?.teiRelationships, + relationships: relationshipsData?.teiRelationships?.instances, }; useEffect(() => { From 7dd0f932ce17cd7f05acda7cc88b9f14d85e3e86 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 24 Mar 2022 14:03:21 +0000 Subject: [PATCH 32/95] chore: [DHI2-12362] change type --- .../components/CardList/CardListItem.component.js | 3 ++- .../useCommonEnrollmentDomainData.types.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index 608a8ce0a9..ed7c69c6fc 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -126,7 +126,8 @@ const CardListItemIndex = ({
); }; - const enrollments = item.tei ? item.tei.enrollments : []; + console.log({ item }); + const enrollments = item.tei?.enrollments ?? []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitName, enrolledAt } = deriveEnrollmentOrgUnitAndDate(enrollments, enrollmentType, currentProgramId); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index d28365cb71..994483e5ea 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -57,8 +57,8 @@ export type TEIAttribute = {| |} export type TEIRelationshipData = {| - trackedEntityInstance: { - trackedEntityInstance: string, + trackedEntity: { + trackedEntity: string, attributes: Array } |} From 038ce39139e3a46d3d33e3ece89e6ade14ad113b Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 24 Mar 2022 14:05:45 +0000 Subject: [PATCH 33/95] chore: [DHI2-12362] change type --- .../EnrollmentPageDefault/hooks/useTeiRelationships.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js index 90fc2cd933..81cac65055 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js @@ -63,7 +63,7 @@ const getAttributeConstraintsForTEI = ( programStageName: stage?.stageForm?.name, }, }; - } else if (to?.trackedEntity && to?.trackedEntity?.trackedEntityInstance !== teiId) { + } else if (to?.trackedEntity && to?.trackedEntity?.trackedEntity !== teiId) { return { id: to.trackedEntity.trackedEntity, constraint: toConstraint, From 0e43a26d8ca3394a436048de88845a9ae6763e0b Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Fri, 25 Mar 2022 10:09:28 +0000 Subject: [PATCH 34/95] chore: [DHIS2-12362] fix edit event --- .../capture-core/relationships/relationshipRequests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/relationships/relationshipRequests.js b/src/core_modules/capture-core/relationships/relationshipRequests.js index 2d1d0b216e..f31a3ceab2 100644 --- a/src/core_modules/capture-core/relationships/relationshipRequests.js +++ b/src/core_modules/capture-core/relationships/relationshipRequests.js @@ -16,5 +16,5 @@ export function getRelationshipsForEvent(eventId: string, programId: string, pro const program = getProgramThrowIfNotFound(programId); const stage = program instanceof EventProgram ? program.stage : program.getStage(programStageId); const relationshipTypes = stage?.relationshipTypes || []; - return getRelationships({ event: eventId }, relationshipTypes); + return getRelationships({ event: eventId, fields: ['from,to,relationshipType'] }, relationshipTypes); } From 72b7df57d8792683e82413d2d987f3fd5e213543 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 6 Apr 2022 09:15:31 +0100 Subject: [PATCH 35/95] chore: [DHIS2-12362] minor change --- .../EnrollmentPageDefault/hooks/useTeiRelationships.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js index 81cac65055..2bc35f43b8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js @@ -82,7 +82,7 @@ const getAttributeConstraintsForTEI = ( }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); - return {}; + return undefined; }; const getRelationshipAttributes = ( @@ -93,7 +93,7 @@ const getRelationshipAttributes = ( relationship: Object, ) => { const data = getAttributeConstraintsForTEI(relationshipType, teiId, from, to); - if (!data) { return {}; } + if (!data) { return undefined; } const { id, relationshipName, constraint, attributes: constraintAttributes, options: constraintOptions } = data; let options = constraintOptions; if (!options) { @@ -125,9 +125,11 @@ export const useTeiRelationships = (teiId: string, relationships?: Array result.response); - const { relationshipName, displayFields, id, attributes } = getRelationshipAttributes( + const metadata = getRelationshipAttributes( relationshipType, teiId, from, to, relationship, ); + if (!metadata) { break; } + const { relationshipName, displayFields, id, attributes } = metadata; const typeExist = groupped.find(item => item.id === typeId); if (typeExist) { From 7f97c0f6f8c2b5eee5a7d502442ea2338baa8432 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Fri, 8 Apr 2022 11:39:37 +0100 Subject: [PATCH 36/95] fix: [DHIS2-12362] fix combinations --- .../hooks/useTeiRelationships.js | 100 +++++++++++------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js index 2bc35f43b8..9aaf0f8f89 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js @@ -43,44 +43,68 @@ const getAttributeConstraintsForTEI = ( from: RelationshipData, to: RelationshipData, ) => { - const { bidirectional, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; - - if (bidirectional && from?.event) { - // $FlowFixMe - const { stage, program } = getProgramAndStageFromEvent({ - evenId: from.event.event, - programId: from.event.program, - programStageId: from.event.programStage, - }); - return { - id: from.event.event, - constraint: fromConstraint, - attributes: from.event, - relationshipName: toFromName, - options: { - ...from.event, - programName: program?.name, - programStageName: stage?.stageForm?.name, - }, - }; - } else if (to?.trackedEntity && to?.trackedEntity?.trackedEntity !== teiId) { - return { - id: to.trackedEntity.trackedEntity, - constraint: toConstraint, - attributes: to.trackedEntity.attributes, - relationshipName: fromToName, - options: null, - }; - } else if (bidirectional && from?.trackedEntity && - from?.trackedEntity?.trackedEntity !== teiId) { - return { - id: from.trackedEntity.trackedEntity, - constraint: fromConstraint, - attributes: from.trackedEntity.attributes, - relationshipName: toFromName, - options: null, - }; + const { fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; + // $FlowFixMe + if (to.trackedEntity?.trackedEntity === teiId) { + if (from.event) { + // $FlowFixMe + const { stage, program } = getProgramAndStageFromEvent({ + evenId: from.event.event, + programId: from.event.program, + programStageId: from.event.programStage, + }); + return { + id: from.event.event, + constraint: fromConstraint, + attributes: from.event, + relationshipName: toFromName, + options: { + ...from.event, + programName: program?.name, + programStageName: stage?.stageForm?.name, + }, + }; + } else if (from.trackedEntity) { + return { + id: from.trackedEntity.trackedEntity, + constraint: fromConstraint, + attributes: from.trackedEntity.attributes, + relationshipName: toFromName, + options: null, + }; + } } + // $FlowFixMe + if (from.trackedEntity?.trackedEntity === teiId) { + if (to.event) { + // $FlowFixMe + const { stage, program } = getProgramAndStageFromEvent({ + evenId: to.event.event, + programId: to.event.program, + programStageId: to.event.programStage, + }); + return { + id: to.event.event, + constraint: toConstraint, + attributes: to.event, + relationshipName: fromToName, + options: { + ...to.event, + programName: program?.name, + programStageName: stage?.stageForm?.name, + }, + }; + } else if (to.trackedEntity) { + return { + id: to.trackedEntity.trackedEntity, + constraint: toConstraint, + attributes: to.trackedEntity.attributes, + relationshipName: fromToName, + options: null, + }; + } + } + log.error(errorCreator('Relationship type is not handled')({ relationshipType })); return undefined; }; @@ -128,7 +152,7 @@ export const useTeiRelationships = (teiId: string, relationships?: Array item.id === typeId); From f309105bb6b541cc42415d8eba817aabacd8fa9f Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Sat, 30 Apr 2022 09:21:39 +0100 Subject: [PATCH 37/95] chore: [DHIS2-12362] restructure the components --- i18n/en.pot | 10 +++++----- .../EnrollmentPageDefault.component.js | 10 +++++----- .../EnrollmentPageDefault.container.js | 4 +--- .../EnrollmentPageDefault.types.js | 4 ++-- .../EnrollmentPageDefault/hooks/index.js | 1 - .../EnrollmentEditEventPage.component.js | 10 +++++----- .../EnrollmentEditEventPage.container.js | 5 ++--- .../EnrollmentEditEventPage.types.js | 4 ++-- .../WidgetRelationship/Relationships/index.js | 5 ----- .../components/WidgetRelationship/index.js | 1 - .../widgetRelationship.types.js | 9 --------- .../RelationshipTable.component.js | 0 .../Relationships/RelationshipTable/index.js | 0 .../Relationships/Relationships.component.js | 3 +-- .../RelationshipsWidget.component.js} | 10 +++++----- .../WidgetRelationships/Relationships/index.js | 7 +++++++ .../WidgetTeisRelationships.component.js | 18 ++++++++++++++++++ .../WidgetTeisRelationships/index.js | 5 +++++ .../hooks/useTeiRelationships.js | 10 +++++----- .../components/WidgetRelationships/index.js | 1 + .../widgetRelationships.types.js | 16 ++++++++++++++++ 21 files changed, 80 insertions(+), 53 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationship/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js rename src/core_modules/capture-core/components/{WidgetRelationship => WidgetRelationships}/Relationships/RelationshipTable/RelationshipTable.component.js (100%) rename src/core_modules/capture-core/components/{WidgetRelationship => WidgetRelationships}/Relationships/RelationshipTable/index.js (100%) rename src/core_modules/capture-core/components/{WidgetRelationship => WidgetRelationships}/Relationships/Relationships.component.js (96%) rename src/core_modules/capture-core/components/{WidgetRelationship/WidgetRelationship.component.js => WidgetRelationships/Relationships/RelationshipsWidget.component.js} (71%) create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js rename src/core_modules/capture-core/components/{Pages/Enrollment/EnrollmentPageDefault => WidgetRelationships}/hooks/useTeiRelationships.js (95%) create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js diff --git a/i18n/en.pot b/i18n/en.pot index 8bb11a9845..92889f9b51 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-04-06T12:58:52.162Z\n" -"PO-Revision-Date: 2022-04-06T12:58:52.162Z\n" +"POT-Creation-Date: 2022-04-30T08:21:41.788Z\n" +"PO-Revision-Date: 2022-04-30T08:21:41.788Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -601,9 +601,6 @@ msgstr "Enrollment with id \"{{enrollmentId}}\" does not exist" msgid "Enrollment Dashboard" msgstr "Enrollment Dashboard" -msgid "TEI's Relationships" -msgstr "TEI's Relationships" - msgid "No indicator output for this enrollment yet" msgstr "No indicator output for this enrollment yet" @@ -1166,6 +1163,9 @@ msgstr "Person Profile" msgid "Edit" msgstr "Edit" +msgid "TEI's Relationships" +msgstr "TEI's Relationships" + msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index c7c6d724a1..4e0925fb62 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -13,7 +13,7 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; -import { WidgetRelationship } from '../../../WidgetRelationship'; +import { WidgetTeisRelationships } from '../../../WidgetRelationships'; const getStyles = ({ typography }) => ({ columns: { @@ -49,7 +49,7 @@ export const EnrollmentPageDefaultPlain = ({ orgUnitId, events, enrollmentId, - teiRelationships, + relationships, stages, onDelete, onAddNew, @@ -81,9 +81,9 @@ export const EnrollmentPageDefaultPlain = ({ - {}} /> {!hideWidgets.indicator && ( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 3aa0a06389..6a2dbc804c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -14,7 +14,6 @@ import { useHideWidgetByRuleLocations, useProgramStages, useRuleEffects, - useTeiRelationships, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; @@ -35,7 +34,6 @@ export const EnrollmentPageDefault = () => { } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); - const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); if (programMetaDataError || enrollmentsError) { log.error(errorCreator('Enrollment page could not be loaded')( @@ -95,7 +93,7 @@ export const EnrollmentPageDefault = () => { stages={stages} events={enrollment?.events} enrollmentId={enrollmentId} - teiRelationships={teiRelationships} + relationships={relationships} onAddNew={onAddNew} onDelete={onDelete} onViewAll={onViewAll} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 36de4b6db0..7706629bd5 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,7 +2,7 @@ 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'; -import type { Event, OutputRelationship } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event, InputRelationship } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type Props = {| program: Program, @@ -10,7 +10,7 @@ export type Props = {| teiId: string, events: ?Array, stages?: Array, - teiRelationships: Array, + relationships?: Array, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, orgUnitId: string, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js index 23041ce2e8..3b0cdd75eb 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js @@ -4,5 +4,4 @@ export { useProgramMetadata } from './useProgramMetadata'; export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; export { useProgramStages } from './useProgramStages'; export { useRuleEffects } from './useRuleEffects'; -export { useTeiRelationships } from './useTeiRelationships'; export type { UseRuleEffectsInput } from './useRuleEffects.types'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 8e538c8f61..2a0c0bae11 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -28,7 +28,7 @@ import { SingleLockedSelect } from '../../ScopeSelector/QuickSelector/SingleLock import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { TopBarActions } from '../../TopBarActions'; import { WidgetEventComment } from '../../WidgetEventComment'; -import { WidgetRelationship } from '../../WidgetRelationship'; +import { WidgetTeisRelationships } from '../../WidgetRelationships'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; const styles = ({ typography }) => ({ @@ -64,7 +64,7 @@ const EnrollmentEditEventPagePain = ({ teiId, enrollmentId, programId, - teiRelationships, + relationships, enrollmentsAsOptions, trackedEntityName, teiDisplayName, @@ -187,9 +187,9 @@ const EnrollmentEditEventPagePain = ({ - {}} /> {!hideWidgets.feedback && ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index 39c972cfce..8de092b03d 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -9,7 +9,7 @@ import { useProgramInfo } from '../../../hooks/useProgramInfo'; import { pageMode, pageStatuses } from './EnrollmentEditEventPage.constants'; import { EnrollmentEditEventPageComponent } from './EnrollmentEditEventPage.component'; import { useWidgetDataFromStore } from '../EnrollmentAddEvent/hooks'; -import { useHideWidgetByRuleLocations, useTeiRelationships } from '../Enrollment/EnrollmentPageDefault/hooks'; +import { useHideWidgetByRuleLocations } from '../Enrollment/EnrollmentPageDefault/hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { deleteEnrollment } from '../Enrollment/EnrollmentPage.actions'; import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; @@ -52,7 +52,6 @@ export const EnrollmentEditEventPage = () => { ? (pageStatus = pageStatuses.DEFAULT) : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; - const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); return ( { onGoBack={onGoBack} widgetEffects={outputEffects} hideWidgets={hideWidgets} - teiRelationships={teiRelationships} + relationships={relationships} teiId={teiId} enrollmentId={enrollmentId} enrollmentsAsOptions={enrollmentsAsOptions} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 1a71393b4d..80dc435640 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { OutputRelationship } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type PlainProps = {| programStage: ?ProgramStage, @@ -14,7 +14,7 @@ export type PlainProps = {| orgUnitId: string, trackedEntityName: string, teiDisplayName: string, - teiRelationships: Array, + relationships?: Array, eventDate?: string, enrollmentsAsOptions: Array, onDelete: () => void, diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js b/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js deleted file mode 100644 index f158af4269..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Relationships } from './Relationships.component'; - -export { - Relationships, -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/index.js b/src/core_modules/capture-core/components/WidgetRelationship/index.js deleted file mode 100644 index 2fdfb26a3a..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationship/index.js +++ /dev/null @@ -1 +0,0 @@ -export { WidgetRelationship } from './WidgetRelationship.component'; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js b/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js deleted file mode 100644 index 0b3683897c..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationship/widgetRelationship.types.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import type { OutputRelationship } from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; - -export type Props = {| - relationships: Array, - title: string, - onAddRelationship: () => void, - ...CssClasses, -|}; diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/RelationshipTable.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/RelationshipTable.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/RelationshipTable.component.js diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetRelationship/Relationships/RelationshipTable/index.js rename to src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js diff --git a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js similarity index 96% rename from src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js index 637b22b945..d8a9c45dd3 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js @@ -7,8 +7,7 @@ import { RelationshipTable } from './RelationshipTable'; type Props = { relationships: Object, - headersByType: Object, - onAddRelationship: void, + onAddRelationship: () => void, ...CssClasses, } diff --git a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js similarity index 71% rename from src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js index 8cf90cd4b4..6b6d60d652 100644 --- a/src/core_modules/capture-core/components/WidgetRelationship/WidgetRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js @@ -1,11 +1,11 @@ // @flow import React, { useState, useCallback } from 'react'; import { Chip } from '@dhis2/ui'; -import { Widget } from '../Widget'; -import type { Props } from './widgetRelationship.types'; -import { Relationships } from './Relationships/'; +import { Widget } from '../../Widget'; +import type { Props } from '../widgetRelationships.types'; +import { Relationships } from './Relationships.component'; -export const WidgetRelationship = ({ relationships, title, onAddRelationship }: Props) => { +export const WidgetRelationships = ({ relationships, title, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); const count = relationships.reduce((acc, curr) => { acc += curr.relationshipAttributes.length; return acc; }, 0); return ( @@ -25,7 +25,7 @@ export const WidgetRelationship = ({ relationships, title, onAddRelationship }: onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - + ); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js new file mode 100644 index 0000000000..f9740e6944 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js @@ -0,0 +1,7 @@ +import { Relationships } from './Relationships.component'; +import { RelationshipsWidget } from './RelationshipsWidget.component'; + +export { + Relationships, + RelationshipsWidget, +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js new file mode 100644 index 0000000000..ee202ab785 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -0,0 +1,18 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { useTeiRelationships } from '../hooks/useTeiRelationships'; +import { WidgetRelationships } from '../Relationships'; +import type { WidgetTeisProps } from '../widgetRelationships.types'; + +export const WidgetTeisRelationships = ({ relationships, teiId, onAddRelationship }: WidgetTeisProps) => { + const teiRelationships = useTeiRelationships(teiId, relationships); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js new file mode 100644 index 0000000000..3428fa494d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js @@ -0,0 +1,5 @@ +import { WidgetTeisRelationships } from './WidgetTeisRelationships.component'; + +export { + WidgetTeisRelationships, +}; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js similarity index 95% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js rename to src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js index 9aaf0f8f89..53285b78bb 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js @@ -4,14 +4,14 @@ import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import { getCachedSingleResourceFromKeyAsync, -} from '../../../../../MetaDataStoreUtils/MetaDataStoreUtils'; +} from '../../../MetaDataStoreUtils/MetaDataStoreUtils'; import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } - from '../../../../../metaData'; -import { userStores } from '../../../../../storageControllers/stores'; + from '../../../metaData'; +import { userStores } from '../../../storageControllers/stores'; import type { InputRelationship, RelationshipData, -} from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from './constants'; +} from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from '../../Pages/Enrollment/EnrollmentPageDefault/hooks/constants'; const convertAttributes = (attributes, displayFields, options) => displayFields.map((item) => { if (item.convertValue) { diff --git a/src/core_modules/capture-core/components/WidgetRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/index.js new file mode 100644 index 0000000000..5becf6de33 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/index.js @@ -0,0 +1 @@ +export { WidgetTeisRelationship } from './WidgetTeisRelationships/'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js b/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js new file mode 100644 index 0000000000..77a6446816 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js @@ -0,0 +1,16 @@ +// @flow +import type { OutputRelationship, InputRelationship } from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; + +export type WidgetTeisProps = {| + relationships: Array, + onAddRelationship: () => void, + teiId: string, + ...CssClasses, +|}; + +export type Props = {| + relationships: Array, + title: string, + onAddRelationship: () => void, + ...CssClasses, +|} From 9682ffed47343cfff338669fbd5618f5bbc9dc7d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 2 May 2022 08:36:29 +0100 Subject: [PATCH 38/95] chore: [DHIS2-12362] change await/async loop --- .../Relationships/RelationshipTable/index.js | 5 --- .../Relationships.component.js | 4 +-- .../RelationshipsTable.component.js} | 2 +- .../RelationshipsWidget.component.js | 2 +- .../index.js | 2 -- .../WidgetTeisRelationships.component.js | 6 ++-- .../hooks/useTeiRelationships.js | 32 ++++++++++--------- .../components/WidgetRelationships/index.js | 2 +- 8 files changed, 25 insertions(+), 30 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js rename src/core_modules/capture-core/components/WidgetRelationships/{Relationships => RelationshipsComponent}/Relationships.component.js (91%) rename src/core_modules/capture-core/components/WidgetRelationships/{Relationships/RelationshipTable/RelationshipTable.component.js => RelationshipsComponent/RelationshipsTable.component.js} (96%) rename src/core_modules/capture-core/components/WidgetRelationships/{Relationships => RelationshipsComponent}/RelationshipsWidget.component.js (94%) rename src/core_modules/capture-core/components/WidgetRelationships/{Relationships => RelationshipsComponent}/index.js (58%) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js b/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js deleted file mode 100644 index 444fc01ba0..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { RelationshipTable } from './RelationshipTable.component'; - -export { - RelationshipTable, -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js similarity index 91% rename from src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js index d8a9c45dd3..6e49e01a9f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js @@ -3,7 +3,7 @@ import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; import i18n from '@dhis2/d2-i18n'; import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; -import { RelationshipTable } from './RelationshipTable'; +import { RelationshipsTable } from './RelationshipsTable.component'; type Props = { relationships: Object, @@ -36,7 +36,7 @@ const RelationshipsPlain = ({ relationships, classes, onAddRelationship }: Props const { relationshipName, id, ...passOnProps } = relationship; return (
{relationshipName}
- +
); }) : null } diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/RelationshipTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js similarity index 96% rename from src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/RelationshipTable.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js index badf59fb2c..d0416cd98f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipTable/RelationshipTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js @@ -15,7 +15,7 @@ type Props = { ...CssClasses, } -export const RelationshipTable = (props: Props) => { +export const RelationshipsTable = (props: Props) => { const { headers, relationshipAttributes } = props; function renderHeader() { const headerCells = headers diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js similarity index 94% rename from src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js rename to src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index 6b6d60d652..267c87d9cf 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -5,7 +5,7 @@ import { Widget } from '../../Widget'; import type { Props } from '../widgetRelationships.types'; import { Relationships } from './Relationships.component'; -export const WidgetRelationships = ({ relationships, title, ...passOnProps }: Props) => { +export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); const count = relationships.reduce((acc, curr) => { acc += curr.relationshipAttributes.length; return acc; }, 0); return ( diff --git a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js similarity index 58% rename from src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js rename to src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js index f9740e6944..c381ec2ece 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/Relationships/index.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js @@ -1,7 +1,5 @@ -import { Relationships } from './Relationships.component'; import { RelationshipsWidget } from './RelationshipsWidget.component'; export { - Relationships, RelationshipsWidget, }; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index ee202ab785..0aee79d699 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -2,14 +2,14 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useTeiRelationships } from '../hooks/useTeiRelationships'; -import { WidgetRelationships } from '../Relationships'; +import { RelationshipsWidget } from '../RelationshipsComponent'; import type { WidgetTeisProps } from '../widgetRelationships.types'; export const WidgetTeisRelationships = ({ relationships, teiId, onAddRelationship }: WidgetTeisProps) => { - const teiRelationships = useTeiRelationships(teiId, relationships); + const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); return ( - { - const groupped = []; - if (!relationships) { return; } - // $FlowFixMe - for await (const relationship of relationships) { - const { relationshipType: typeId, from, to } = relationship; - const relationshipType = await getCachedSingleResourceFromKeyAsync( - userStores.RELATIONSHIP_TYPES, typeId, - ).then(result => result.response); + const relationshipTypePromises = relationships + .map(rel => getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, rel.relationshipType)); + + const relationshipTypes = await Promise + .all(relationshipTypePromises) + .then(results => results.map(res => res.response)); + const relationshipGrouppedByType = relationships.reduce((acc, rel) => { + const { relationshipType: typeId, from, to } = rel; + const relationshipType = relationshipTypes.find(item => item.id === typeId); const metadata = getRelationshipAttributes( - relationshipType, teiId, from, to, relationship, + relationshipType, teiId, from, to, rel, ); - if (!metadata) { return; } + if (!metadata) { return acc; } const { relationshipName, displayFields, id, attributes } = metadata; - const typeExist = groupped.find(item => item.id === typeId); - + const typeExist = acc.find(item => item.id === typeId); if (typeExist) { typeExist.relationshipAttributes.push({ id, attributes }); } else { - groupped.push({ + acc.push({ id: typeId, relationshipName, relationshipAttributes: [{ id, attributes }], headers: displayFields, }); } - } - setRelationshipByType(groupped); + return acc; + }, []); + + setRelationshipByType(relationshipGrouppedByType); }, [relationships, teiId]); useEffect(() => { diff --git a/src/core_modules/capture-core/components/WidgetRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/index.js index 5becf6de33..f6afb92e0d 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/index.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/index.js @@ -1 +1 @@ -export { WidgetTeisRelationship } from './WidgetTeisRelationships/'; +export { WidgetTeisRelationships } from './WidgetTeisRelationships/'; From 4e55df30cdc1fe20e040517567d7f4d30912ca62 Mon Sep 17 00:00:00 2001 From: JasmineNg <89806888+jasminenguyennn@users.noreply.github.com> Date: Mon, 2 May 2022 09:31:36 +0100 Subject: [PATCH 39/95] feat: [DHIS2-12377] Event relationship (#2726) * [DHIS2-12377] add event relationships * [DHIS2-12377] add event relationships * feat: [DHIS2-12377] refactor relationships --- i18n/en.pot | 7 ++- .../Enrollment/EnrollmentPage.actions.js | 5 ++ .../EnrollmentEditEventPage.component.js | 7 ++- .../EnrollmentEditEventPage.container.js | 1 + .../EnrollmentEditEventPage.types.js | 1 + .../useEventRelationships.js | 63 +++++++++++++++++++ .../RelationshipsWidget.component.js | 9 ++- .../RelationshipsComponent/index.js | 5 +- .../WidgetEventsRelationships.component.js | 24 +++++++ .../WidgetEventsRelationships/index.js | 1 + .../WidgetTeisRelationships.component.js | 16 +++-- .../WidgetTeisRelationships/index.js | 6 +- .../hooks/useEventsRelationships.js | 63 +++++++++++++++++++ ...eiRelationships.js => useRelationships.js} | 16 ++--- .../components/WidgetRelationships/index.js | 1 + .../widgetRelationships.types.js | 16 ----- .../enrollmentPage.reducerDescription.js | 7 +++ 17 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/hooks/useEventsRelationships.js rename src/core_modules/capture-core/components/WidgetRelationships/hooks/{useTeiRelationships.js => useRelationships.js} (93%) delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js diff --git a/i18n/en.pot b/i18n/en.pot index f1272fc2c9..40eede9c6b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-04-30T08:21:41.788Z\n" -"PO-Revision-Date: 2022-04-30T08:21:41.788Z\n" +"POT-Creation-Date: 2022-05-02T08:15:29.467Z\n" +"PO-Revision-Date: 2022-05-02T08:15:29.467Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1187,6 +1187,9 @@ msgstr "Person Profile" msgid "Edit" msgstr "Edit" +msgid "Event's Relationships" +msgstr "Event's Relationships" + msgid "TEI's Relationships" msgstr "TEI's Relationships" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index 61cfe8762c..80a878ec58 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -18,6 +18,8 @@ export const enrollmentPageActionTypes = { DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', + + SET_EVENT_RELATIONSHIPS_DATA: 'EnrollmentPage.SetEventRelationshipsData', }; export const fetchEnrollmentPageInformation = () => @@ -60,3 +62,6 @@ export const updateTeiDisplayName = (teiDisplayName: string) => actionCreator(enrollmentPageActionTypes.UPDATE_TEI_DISPLAY_NAME)({ teiDisplayName, }); + +export const setEventRelationshipsData = (eventId: string, relationships: Array) => + actionCreator(enrollmentPageActionTypes.SET_EVENT_RELATIONSHIPS_DATA)({ eventId, relationships }); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 2a0c0bae11..8ebf904df4 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -28,7 +28,7 @@ import { SingleLockedSelect } from '../../ScopeSelector/QuickSelector/SingleLock import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { TopBarActions } from '../../TopBarActions'; import { WidgetEventComment } from '../../WidgetEventComment'; -import { WidgetTeisRelationships } from '../../WidgetRelationships'; +import { WidgetTeisRelationships, WidgetEventsRelationships } from '../../WidgetRelationships'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; const styles = ({ typography }) => ({ @@ -62,6 +62,7 @@ const EnrollmentEditEventPagePain = ({ mode, programStage, teiId, + eventId, enrollmentId, programId, relationships, @@ -192,6 +193,10 @@ const EnrollmentEditEventPagePain = ({ relationships={relationships} onAddRelationship={() => {}} /> + {}} + /> {!hideWidgets.feedback && ( { hideWidgets={hideWidgets} relationships={relationships} teiId={teiId} + eventId={eventId} enrollmentId={enrollmentId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 80dc435640..ee29f8b345 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -8,6 +8,7 @@ export type PlainProps = {| widgetEffects: WidgetEffects, hideWidgets: HideWidgets, teiId: string, + eventId: string, enrollmentId: string, programId: string, mode: string, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js new file mode 100644 index 0000000000..7faf1dc483 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js @@ -0,0 +1,63 @@ +// @flow +import { useMemo, useEffect } from 'react'; +// $FlowFixMe +import { useSelector, useDispatch } from 'react-redux'; +import { useDataQuery } from '@dhis2/app-runtime'; +import { setEventRelationshipsData } from '../Enrollment/EnrollmentPage.actions'; + +export const useEventsRelationships = (eventId: string) => { + const dispatch = useDispatch(); + + const { + eventId: storedEventId, + relationships: storedRelationships, + } = useSelector(({ enrollmentPage }) => enrollmentPage); + + const { + data, + error, + refetch, + } = useDataQuery( + useMemo( + () => ({ + eventRelationships: { + resource: 'tracker/relationships', + params: ({ variables: { eventId: updatedEventId } }) => ({ + event: updatedEventId, + fields: ['relationshipType,to,from'], + }), + }, + }), + [], + ), + { lazy: true }, + ); + + const fetchedEventData = { + reference: data, + eventId, + relationships: data?.eventRelationships?.instances, + }; + + useEffect(() => { + if (fetchedEventData.reference) { + dispatch(setEventRelationshipsData( + fetchedEventData.eventId, + fetchedEventData.relationships, + )); + } + }, [ + dispatch, + fetchedEventData.reference, + fetchedEventData.relationships, + fetchedEventData.eventId, + ]); + + useEffect(() => { + if (storedEventId !== eventId) { + refetch({ variables: { eventId } }); + } + }, [storedEventId, eventId, refetch]); + + return { relationships: storedRelationships, error }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index 267c87d9cf..fcdbd9acc8 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -2,8 +2,15 @@ import React, { useState, useCallback } from 'react'; import { Chip } from '@dhis2/ui'; import { Widget } from '../../Widget'; -import type { Props } from '../widgetRelationships.types'; import { Relationships } from './Relationships.component'; +import type { OutputRelationship } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; + +type Props = {| + relationships: Array, + title: string, + onAddRelationship: () => void, + ...CssClasses, +|} export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js index c381ec2ece..f111a734de 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js @@ -1,5 +1,2 @@ -import { RelationshipsWidget } from './RelationshipsWidget.component'; +export { RelationshipsWidget } from './RelationshipsWidget.component'; -export { - RelationshipsWidget, -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js new file mode 100644 index 0000000000..ddefc90ff2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -0,0 +1,24 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { useEventsRelationships } from '../hooks/useEventsRelationships'; +import { useRelationships } from '../hooks/useRelationships'; +import { RelationshipsWidget } from '../RelationshipsComponent'; + +type Props = {| + eventId: string, + onAddRelationship: () => void +|} + +export const WidgetEventsRelationships = ({ eventId, onAddRelationship }: Props) => { + const { relationships } = useEventsRelationships(eventId); + const { relationships: eventsRelationships } = useRelationships(eventId, relationships); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js new file mode 100644 index 0000000000..2c77ef58b8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js @@ -0,0 +1 @@ +export { WidgetEventsRelationships } from './WidgetEventsRelationships.component'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index 0aee79d699..3fd23cf8a7 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -1,12 +1,20 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useTeiRelationships } from '../hooks/useTeiRelationships'; +import { useRelationships } from '../hooks/useRelationships'; import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { WidgetTeisProps } from '../widgetRelationships.types'; +import type { InputRelationship } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -export const WidgetTeisRelationships = ({ relationships, teiId, onAddRelationship }: WidgetTeisProps) => { - const { relationships: teiRelationships } = useTeiRelationships(teiId, relationships); + +type Props = {| + relationships: Array, + onAddRelationship: () => void, + teiId: string, + ...CssClasses, +|}; + +export const WidgetTeisRelationships = ({ relationships, teiId, onAddRelationship }: Props) => { + const { relationships: teiRelationships } = useRelationships(teiId, relationships); return ( { + const dispatch = useDispatch(); + + const { + eventId: storedEventId, + relationships: storedRelationships, + } = useSelector(({ enrollmentPage }) => enrollmentPage); + + const { + data, + error, + refetch, + } = useDataQuery( + useMemo( + () => ({ + eventRelationships: { + resource: 'tracker/relationships', + params: ({ variables: { eventId: updatedEventId } }) => ({ + event: updatedEventId, + fields: ['relationshipType,to,from'], + }), + }, + }), + [], + ), + { lazy: true }, + ); + + const fetchedEventData = { + reference: data, + eventId, + relationships: data?.eventRelationships?.instances, + }; + + useEffect(() => { + if (fetchedEventData.reference) { + dispatch(setEventRelationshipsData( + fetchedEventData.eventId, + fetchedEventData.relationships, + )); + } + }, [ + dispatch, + fetchedEventData.reference, + fetchedEventData.relationships, + fetchedEventData.eventId, + ]); + + useEffect(() => { + if (storedEventId !== eventId) { + refetch({ variables: { eventId } }); + } + }, [storedEventId, eventId, refetch]); + + return { relationships: storedRelationships, error }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useRelationships.js similarity index 93% rename from src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js rename to src/core_modules/capture-core/components/WidgetRelationships/hooks/useRelationships.js index 1d4702c8d8..823d942af2 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useTeiRelationships.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useRelationships.js @@ -39,13 +39,13 @@ const getDisplayFields = (type) => { const getAttributeConstraintsForTEI = ( relationshipType: Object, - teiId: string, + targetId: string, from: RelationshipData, to: RelationshipData, ) => { const { fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; // $FlowFixMe - if (to.trackedEntity?.trackedEntity === teiId) { + if (to.trackedEntity?.trackedEntity === targetId || to.event?.event === targetId) { if (from.event) { // $FlowFixMe const { stage, program } = getProgramAndStageFromEvent({ @@ -75,7 +75,7 @@ const getAttributeConstraintsForTEI = ( } } // $FlowFixMe - if (from.trackedEntity?.trackedEntity === teiId) { + if (from.trackedEntity?.trackedEntity === targetId || from.event?.event === targetId) { if (to.event) { // $FlowFixMe const { stage, program } = getProgramAndStageFromEvent({ @@ -111,12 +111,12 @@ const getAttributeConstraintsForTEI = ( const getRelationshipAttributes = ( relationshipType: Object, - teiId: string, + targetId: string, from: RelationshipData, to: RelationshipData, relationship: Object, ) => { - const data = getAttributeConstraintsForTEI(relationshipType, teiId, from, to); + const data = getAttributeConstraintsForTEI(relationshipType, targetId, from, to); if (!data) { return undefined; } const { id, relationshipName, constraint, attributes: constraintAttributes, options: constraintOptions } = data; let options = constraintOptions; @@ -135,7 +135,7 @@ const getRelationshipAttributes = ( }; -export const useTeiRelationships = (teiId: string, relationships?: Array) => { +export const useRelationships = (targetId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); const computeData = useCallback(async () => { @@ -151,7 +151,7 @@ export const useTeiRelationships = (teiId: string, relationships?: Array item.id === typeId); const metadata = getRelationshipAttributes( - relationshipType, teiId, from, to, rel, + relationshipType, targetId, from, to, rel, ); if (!metadata) { return acc; } const { relationshipName, displayFields, id, attributes } = metadata; @@ -171,7 +171,7 @@ export const useTeiRelationships = (teiId: string, relationships?: Array { computeData(); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/index.js index f6afb92e0d..78171f3413 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/index.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/index.js @@ -1 +1,2 @@ export { WidgetTeisRelationships } from './WidgetTeisRelationships/'; +export { WidgetEventsRelationships } from './WidgetEventsRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js b/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js deleted file mode 100644 index 77a6446816..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/widgetRelationships.types.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import type { OutputRelationship, InputRelationship } from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; - -export type WidgetTeisProps = {| - relationships: Array, - onAddRelationship: () => void, - teiId: string, - ...CssClasses, -|}; - -export type Props = {| - relationships: Array, - title: string, - onAddRelationship: () => void, - ...CssClasses, -|} diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js index 6c55094b62..5f5622e89c 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js @@ -15,6 +15,7 @@ const { MISSING_MESSAGE_VIEW, DELETE_ENROLLMENT, UPDATE_TEI_DISPLAY_NAME, + SET_EVENT_RELATIONSHIPS_DATA, } = enrollmentPageActionTypes; export const enrollmentPageDesc = createReducerDescription({ @@ -57,6 +58,11 @@ export const enrollmentPageDesc = createReducerDescription({ teiDisplayName, }), [PAGE_CLEAN]: () => initialReducerValue, + [SET_EVENT_RELATIONSHIPS_DATA]: (state, { payload: { eventId, relationships } }) => ({ + ...state, + eventId, + relationships, + }), [DELETE_ENROLLMENT]: (state, { payload: { enrollmentId } }) => ({ ...state, enrollments: [ @@ -65,4 +71,5 @@ export const enrollmentPageDesc = createReducerDescription({ ), ], }), + }, 'enrollmentPage', initialReducerValue); From ab89592415ee9a37968d470d2887b928d55b79ac Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 4 May 2022 13:59:45 +0100 Subject: [PATCH 40/95] chore: [DHIS2-12362] address review comments --- .../useCommonEnrollmentDomainData.types.js | 4 +- .../RelationshipsTable.component.js | 8 +- .../RelationshipsWidget.component.js | 2 +- .../WidgetEventsRelationships.component.js | 4 +- .../WidgetTeisRelationships.component.js | 4 +- .../hooks/useRelationships.js | 143 ++++++++---------- 6 files changed, 71 insertions(+), 94 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 994483e5ea..2303b85e4e 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -71,7 +71,7 @@ export type EnrollmentRelationshipData = {| enrollment: EnrollmentData |} -export type RelationshipData = TEIRelationshipData | EventRelationshipData | EnrollmentRelationshipData +export type RelationshipData = TEIRelationshipData | EventRelationshipData export type InputRelationship = {| relationshipType: string, @@ -85,7 +85,7 @@ export type InputRelationship = {| export type OutputRelationship = { id: string, relationshipName: string, - relationshipAttributes: Array<{ id: string, attributes: Array}> + linkedEntityData: Array<{ id: string, attributes: Array}> } export type Output = {| diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js index d0416cd98f..9c9bb094cd 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js @@ -11,12 +11,12 @@ import { type Props = { headers: Array, - relationshipAttributes: Array, + linkedEntityData: Array, ...CssClasses, } export const RelationshipsTable = (props: Props) => { - const { headers, relationshipAttributes } = props; + const { headers, linkedEntityData } = props; function renderHeader() { const headerCells = headers .map(column => ( @@ -34,10 +34,10 @@ export const RelationshipsTable = (props: Props) => { ); } const renderRelationshipRows = () => { - if (!relationshipAttributes) { + if (!linkedEntityData) { return null; } - return relationshipAttributes.map(({ id: teiId, attributes }) => ( + return linkedEntityData.map(({ id: teiId, attributes }) => ( {headers.map(({ id }) => { const attribute = attributes.find(att => att.id === id); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index fcdbd9acc8..db2345af2f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -14,7 +14,7 @@ type Props = {| export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); - const count = relationships.reduce((acc, curr) => { acc += curr.relationshipAttributes.length; return acc; }, 0); + const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); return (
{ const { relationships } = useEventsRelationships(eventId); - const { relationships: eventsRelationships } = useRelationships(eventId, relationships); + const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationships); return ( { - const { relationships: teiRelationships } = useRelationships(teiId, relationships); + const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationships); return ( { return displayFields; }; - -const getAttributeConstraintsForTEI = ( +const determineLinkedEntity = ( relationshipType: Object, targetId: string, from: RelationshipData, to: RelationshipData, ) => { const { fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; - // $FlowFixMe - if (to.trackedEntity?.trackedEntity === targetId || to.event?.event === targetId) { - if (from.event) { - // $FlowFixMe - const { stage, program } = getProgramAndStageFromEvent({ - evenId: from.event.event, - programId: from.event.program, - programStageId: from.event.programStage, - }); - return { - id: from.event.event, - constraint: fromConstraint, - attributes: from.event, - relationshipName: toFromName, - options: { - ...from.event, - programName: program?.name, - programStageName: stage?.stageForm?.name, - }, - }; - } else if (from.trackedEntity) { - return { - id: from.trackedEntity.trackedEntity, - constraint: fromConstraint, - attributes: from.trackedEntity.attributes, - relationshipName: toFromName, - options: null, - }; - } + + if ((to.trackedEntity && to.trackedEntity.trackedEntity === targetId) || (to.event && to.event.event === targetId)) { + return { side: from, constraint: fromConstraint, relationshipName: toFromName }; } - // $FlowFixMe - if (from.trackedEntity?.trackedEntity === targetId || from.event?.event === targetId) { - if (to.event) { - // $FlowFixMe - const { stage, program } = getProgramAndStageFromEvent({ - evenId: to.event.event, - programId: to.event.program, - programStageId: to.event.programStage, - }); - return { - id: to.event.event, - constraint: toConstraint, - attributes: to.event, - relationshipName: fromToName, - options: { - ...to.event, - programName: program?.name, - programStageName: stage?.stageForm?.name, - }, - }; - } else if (to.trackedEntity) { - return { - id: to.trackedEntity.trackedEntity, - constraint: toConstraint, - attributes: to.trackedEntity.attributes, - relationshipName: fromToName, - options: null, - }; - } + + if ((from.trackedEntity && from.trackedEntity.trackedEntity === targetId) || (from.event && from.event.event === targetId)) { + return { side: to, constraint: toConstraint, relationshipName: fromToName }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); return undefined; }; -const getRelationshipAttributes = ( + +const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { + if (linkedEntity.event) { + const { event: eventId, program: programId, programStage, orgUnitName, status } = linkedEntity.event; + /* + * Needs refactoring when moving to Widget Library. + * We will need to either add the program name and program stage name to the relationshipTypes + * or do some kind of callback to get the appropriate information. + */ + // $FlowFixMe + const { stage, program } = getProgramAndStageFromEvent({ + eventId, + programId, + programStageId: programStage, + }); + + return { + id: eventId, + attributes: linkedEntity.event, + options: { + orgUnitName, + status, + programName: program?.name, + programStageName: stage?.stageForm?.name, + }, + }; + } else if (linkedEntity.trackedEntity) { + return { + id: linkedEntity.trackedEntity.trackedEntity, + attributes: linkedEntity.trackedEntity.attributes, + options: { }, + }; + } + log.error(errorCreator('Relationship type is not handled')({ linkedEntity })); + return undefined; +}; + +const getLinkedEntityInfo = ( relationshipType: Object, targetId: string, from: RelationshipData, to: RelationshipData, - relationship: Object, ) => { - const data = getAttributeConstraintsForTEI(relationshipType, targetId, from, to); - if (!data) { return undefined; } - const { id, relationshipName, constraint, attributes: constraintAttributes, options: constraintOptions } = data; - let options = constraintOptions; - if (!options) { - const tet = getTrackedEntityTypeThrowIfNotFound(constraint.trackedEntityType.id); - options = { ...relationship, trackedEntityTypeName: tet.name }; - } - const displayFields = getDisplayFields(constraint.relationshipEntity); - const attributes = convertAttributes(constraintAttributes, displayFields, options); + const linkedEntityData = determineLinkedEntity(relationshipType, targetId, from, to); + if (!linkedEntityData) { return undefined; } + const metadata = getAttributeConstraintsForTEI(linkedEntityData.side); + if (!metadata) { return undefined; } + const { id, attributes, options } = metadata; + const displayFields = getDisplayFields(linkedEntityData.constraint.relationshipEntity); return { id, - relationshipName, - attributes, + relationshipName: linkedEntityData?.relationshipName, + attributes: convertAttributes(attributes, displayFields, options), displayFields, }; }; -export const useRelationships = (targetId: string, relationships?: Array) => { +export const useLinkedEntityGroups = (targetId: string, relationships?: Array) => { const [relationshipsByType, setRelationshipByType] = useState([]); const computeData = useCallback(async () => { @@ -147,22 +126,20 @@ export const useRelationships = (targetId: string, relationships?: Array results.map(res => res.response)); - const relationshipGrouppedByType = relationships.reduce((acc, rel) => { + const linkedEntityGroups = relationships.reduce((acc, rel) => { const { relationshipType: typeId, from, to } = rel; const relationshipType = relationshipTypes.find(item => item.id === typeId); - const metadata = getRelationshipAttributes( - relationshipType, targetId, from, to, rel, - ); + const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to); if (!metadata) { return acc; } const { relationshipName, displayFields, id, attributes } = metadata; const typeExist = acc.find(item => item.id === typeId); if (typeExist) { - typeExist.relationshipAttributes.push({ id, attributes }); + typeExist.linkedEntityData.push({ id, attributes }); } else { acc.push({ id: typeId, relationshipName, - relationshipAttributes: [{ id, attributes }], + linkedEntityData: [{ id, attributes }], headers: displayFields, }); } @@ -170,7 +147,7 @@ export const useRelationships = (targetId: string, relationships?: Array { From dbfe1fb26617ca659b6f54ba4e522c4d1f9583be Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 4 May 2022 14:01:41 +0100 Subject: [PATCH 41/95] chore: [DHIS2-12362] rename files --- .../WidgetEventsRelationships.component.js | 2 +- .../WidgetTeisRelationships.component.js | 2 +- .../hooks/{useRelationships.js => useLinkedEntityGroups.js} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/core_modules/capture-core/components/WidgetRelationships/hooks/{useRelationships.js => useLinkedEntityGroups.js} (100%) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index 97243a25f5..eec467f8ab 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -2,7 +2,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useEventsRelationships } from '../hooks/useEventsRelationships'; -import { useLinkedEntityGroups } from '../hooks/useRelationships'; +import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; type Props = {| diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index 353fa98165..867884dd18 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useLinkedEntityGroups } from '../hooks/useRelationships'; +import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; import type { InputRelationship } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useRelationships.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetRelationships/hooks/useRelationships.js rename to src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js From 5a7eec4d969ca1cebc4d2bc27d0c98a2dd241d78 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 5 May 2022 10:16:55 +0100 Subject: [PATCH 42/95] chore: [DHIS2-12362] minor change --- .../capture-core/components/CardList/CardListItem.component.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index ed7c69c6fc..14730305d1 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -126,7 +126,6 @@ const CardListItemIndex = ({
); }; - console.log({ item }); const enrollments = item.tei?.enrollments ?? []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitName, enrolledAt } = deriveEnrollmentOrgUnitAndDate(enrollments, enrollmentType, currentProgramId); From e92c9b267f45c96d6235838efb57d0fe1a3d4030 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 5 May 2022 19:09:07 +0100 Subject: [PATCH 43/95] feat: [DHIS2-12362] add display fields --- .../EnrollmentPageDefault.component.js | 2 + .../EnrollmentPageDefault.container.js | 4 + .../EnrollmentPageDefault.types.js | 3 +- .../EnrollmentPageDefault/hooks/constants.js | 37 +---- .../EnrollmentEditEventPage.component.js | 5 + .../EnrollmentEditEventPage.container.js | 7 + .../EnrollmentEditEventPage.types.js | 4 +- ...tionships.js => useEventsRelationships.js} | 0 .../useCommonEnrollmentDomainData.types.js | 26 +++- .../useRelationshipTypesMetadata.js | 90 +++++++++++ .../RelationshipsTable.component.js | 10 +- .../WidgetEventsRelationships.component.js | 9 +- .../WidgetTeisRelationships.component.js | 8 +- .../hooks/useEventsRelationships.js | 63 -------- .../hooks/useLinkedEntityGroups.js | 143 ++++++++++-------- 15 files changed, 237 insertions(+), 174 deletions(-) rename src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/{useEventRelationships.js => useEventsRelationships.js} (100%) create mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/hooks/useEventsRelationships.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 4e0925fb62..54b5aeac3e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -50,6 +50,7 @@ export const EnrollmentPageDefaultPlain = ({ events, enrollmentId, relationships, + relationshipTypes, stages, onDelete, onAddNew, @@ -83,6 +84,7 @@ export const EnrollmentPageDefaultPlain = ({ {}} /> diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 6a2dbc804c..03edb6e907 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -18,6 +18,7 @@ import { import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useRelationshipTypesMetadata } from '../../common/EnrollmentOverviewDomain/useRelationshipTypesMetadata'; export const EnrollmentPageDefault = () => { const history = useHistory(); @@ -80,6 +81,8 @@ export const EnrollmentPageDefault = () => { history.push(`/new?${buildUrlQueryString({ orgUnitId, programId, teiId })}`); }; + const relationshipTypes = useRelationshipTypesMetadata(relationships); + if (error) { return error.errorComponent; } @@ -94,6 +97,7 @@ export const EnrollmentPageDefault = () => { events={enrollment?.events} enrollmentId={enrollmentId} relationships={relationships} + relationshipTypes={relationshipTypes} onAddNew={onAddNew} onDelete={onDelete} onViewAll={onViewAll} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 7706629bd5..002909241b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,7 +2,7 @@ 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'; -import type { Event, InputRelationship } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event, InputRelationship, RelationshipType } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type Props = {| program: Program, @@ -11,6 +11,7 @@ export type Props = {| events: ?Array, stages?: Array, relationships?: Array, + relationshipTypes?: Array, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, orgUnitId: string, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js index fd04ea1e90..54bda7d29b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js @@ -14,56 +14,29 @@ export const relationshipWidgetTypes = Object.freeze({ EVENT_RELATIONSHIP: 'EVENT_RELATIONSHIP', }); -// this will change after https://jira.dhis2.org/browse/DHIS2-12249 is done -export const getDisplayFieldsFromAPI = { - [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [ - { id: 'w75KJ2mc4zz', label: 'First name' }, - { id: 'zDhUuAYrxNC', label: 'Last name' }, - ], - [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [ - { id: 'orgUnitName', label: 'Organisation unit' }, - { id: 'program', - label: 'Program', - convertValue: props => props?.programName, - }, - { id: 'eventDate', - label: 'Event date', - convertValue: props => moment(props.eventDate).format('YYYY-MM-DD'), - }, - { id: 'status', label: 'Status' }, - ], - [relationshipEntities.PROGRAM_INSTANCE]: [ - { id: 'orgUnitName', label: 'Organisation unit' }, - { id: 'enrollmentDate', - label: 'Enrollment date', - convertValue: props => moment(props.enrollmentDate).format('YYYY-MM-DD'), - }, - ], -}; - export const getBaseConfigHeaders = { [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ id: 'tetName', - label: i18n.t('TET name'), + displayName: i18n.t('TET name'), convertValue: props => props.trackedEntityTypeName, }, { id: 'createdDate', - label: i18n.t('Created date'), + displayName: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', - label: i18n.t('Program stage name'), + displayName: i18n.t('Program stage name'), convertValue: props => props.programStageName, }, { id: 'createdDate', - label: i18n.t('Created date'), + displayName: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], [relationshipEntities.PROGRAM_INSTANCE]: [{ id: 'createdDate', - label: i18n.t('Created date'), + displayName: i18n.t('Created date'), convertValue: props => moment(props.created).format('YYYY-MM-DD'), }], }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 8ebf904df4..161d1970bf 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -66,6 +66,8 @@ const EnrollmentEditEventPagePain = ({ enrollmentId, programId, relationships, + eventRelationships, + relationshipTypes, enrollmentsAsOptions, trackedEntityName, teiDisplayName, @@ -191,10 +193,13 @@ const EnrollmentEditEventPagePain = ({ {}} /> {}} /> {!hideWidgets.feedback && ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index 37fefbdc1a..3f30618a78 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom'; import { useCommonEnrollmentDomainData } from '../common/EnrollmentOverviewDomain'; import { useTeiDisplayName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; import { useProgramInfo } from '../../../hooks/useProgramInfo'; +import { useEventsRelationships } from './useEventsRelationships'; import { pageMode, pageStatuses } from './EnrollmentEditEventPage.constants'; import { EnrollmentEditEventPageComponent } from './EnrollmentEditEventPage.component'; import { useWidgetDataFromStore } from '../EnrollmentAddEvent/hooks'; @@ -15,6 +16,7 @@ import { deleteEnrollment } from '../Enrollment/EnrollmentPage.actions'; import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; import { convertValue } from '../../../converters/clientToView'; import { dataElementTypes } from '../../../metaData/DataElement'; +import { useRelationshipTypesMetadata } from '../common/EnrollmentOverviewDomain/useRelationshipTypesMetadata'; export const EnrollmentEditEventPage = () => { const history = useHistory(); @@ -52,6 +54,9 @@ export const EnrollmentEditEventPage = () => { ? (pageStatus = pageStatuses.DEFAULT) : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; + const { relationships: eventRelationships } = useEventsRelationships(eventId); + const relationshipTypes = useRelationshipTypesMetadata(relationships); + const eventRelationshipTypes = useRelationshipTypesMetadata(eventRelationships); return ( { widgetEffects={outputEffects} hideWidgets={hideWidgets} relationships={relationships} + eventRelationships={eventRelationships} + relationshipTypes={[...relationshipTypes, ...eventRelationshipTypes]} teiId={teiId} eventId={eventId} enrollmentId={enrollmentId} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index ee29f8b345..69ebb762f6 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { InputRelationship } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship, RelationshipType } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; export type PlainProps = {| programStage: ?ProgramStage, @@ -16,6 +16,8 @@ export type PlainProps = {| trackedEntityName: string, teiDisplayName: string, relationships?: Array, + eventRelationships?: Array, + relationshipTypes?: Array, eventDate?: string, enrollmentsAsOptions: Array, onDelete: () => void, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js similarity index 100% rename from src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventRelationships.js rename to src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 2303b85e4e..872ab73147 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -58,6 +58,7 @@ export type TEIAttribute = {| export type TEIRelationshipData = {| trackedEntity: { + trackedEntityType: string, trackedEntity: string, attributes: Array } @@ -85,7 +86,30 @@ export type InputRelationship = {| export type OutputRelationship = { id: string, relationshipName: string, - linkedEntityData: Array<{ id: string, attributes: Array}> + linkedEntityData: Array<{ id: string, values: Array}> +} + +export type RelationshipConstraintDataView = string | {id: string, displayName: string, valueType: string} + +export type RelationshipConstraint = { + relationshipEntity: string, + trackedEntityType?: ?{ id: string }, + program?: ?{ id: string }, + programStage?: ?{ id: string }, + trackerDataView: { + attributes: Array, + dataElements: Array, + } +} + +export type RelationshipType = { + id: string, + bidirectional: boolean, + displayName: string, + fromConstraint: RelationshipConstraint, + fromToName: string, + toConstraint: RelationshipConstraint, + toFromName: string, } export type Output = {| diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js new file mode 100644 index 0000000000..cb0d0536a4 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -0,0 +1,90 @@ +// @flow +import { useEffect, useState } from 'react'; +import { + getCachedSingleResourceFromKeyAsync, +} from '../../../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { userStores } from '../../../../storageControllers/stores'; +import { getApi } from '../../../../d2/d2Instance'; +import type { InputRelationship } from './useCommonEnrollmentDomainData'; + +export const useRelationshipTypesMetadata = (relationships?: Array) => { + const [relationshipTypesMetadata, setRelationshipTypesMetadata] = useState([]); + + const fetchDataView = async (relType) => { + const { fromConstraint, toConstraint } = relType; + + const fetchValues = async (constraint) => { + let requestPromise; + if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { + if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { + requestPromise = getApi().get('trackedEntityAttributes', { + filter: `id:in:[${constraint.trackerDataView.attributes.join(',')}]`, + fields: ['id,valueType,displayName'], + + }); + } + } else if (constraint.relationshipEntity === 'PROGRAM_STAGE_INSTANCE') { + if (!constraint.trackerDataView.dataElements.every(att => att.valueType)) { + requestPromise = getApi().get('dataElements', { + filter: `id:in:[${constraint.trackerDataView.dataElements.join(',')}]`, + fields: ['id,valueType,displayName'], + }); + } + } + return requestPromise; + }; + + const mergeConstraintDataValues = (constraint, dataValues) => { + if (!dataValues) { return constraint; } + if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { + if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { + return { + ...constraint, + trackerDataView: { + ...constraint.trackerDataView, + attributes: constraint.trackerDataView.attributes + .map(id => dataValues.trackedEntityAttributes.find(item => item.id === id)) + .filter(item => item), + } }; + } + } else if (constraint.relationshipEntity === 'PROGRAM_STAGE_INSTANCE') { + if (!constraint.trackerDataView.dataElements.every(att => att.valueType)) { + return { + ...constraint, + trackerDataView: { + ...constraint.trackerDataView, + dataElements: constraint.trackerDataView.dataElements + .map(id => dataValues.dataElements.find(item => item.id === id)) + .filter(item => item), + }, + }; + } + } + }; + + const [fromDataValues, toDataValues] = await Promise + .all([fetchValues(fromConstraint), fetchValues(toConstraint)]); + + return { + ...relType, + fromConstraint: mergeConstraintDataValues(fromConstraint, fromDataValues), + toConstraint: mergeConstraintDataValues(toConstraint, toDataValues), + }; + }; + + useEffect(() => { + if (relationships) { + const relationshipTypePromises = relationships + .map(rel => getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, rel.relationshipType)); + Promise + .all(relationshipTypePromises) + .then(results => Promise.all(results.map(res => fetchDataView(res.response)))) + .then((res) => { + setRelationshipTypesMetadata(res); + }); + } + }, [relationships]); + + + return relationshipTypesMetadata; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js index 9c9bb094cd..eb42748262 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js @@ -24,7 +24,7 @@ export const RelationshipsTable = (props: Props) => { key={column.id} name={column.id} > - {column.label} + {column.displayName} )); return ( @@ -37,12 +37,12 @@ export const RelationshipsTable = (props: Props) => { if (!linkedEntityData) { return null; } - return linkedEntityData.map(({ id: teiId, attributes }) => ( - + return linkedEntityData.map(({ id: targetId, values }) => ( + {headers.map(({ id }) => { - const attribute = attributes.find(att => att.id === id); + const entity = values.find(item => item.id === id); return ( - {attribute?.value} + {entity?.value} ); })} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index eec467f8ab..0f58d3d267 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -1,18 +1,19 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useEventsRelationships } from '../hooks/useEventsRelationships'; import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; +import type { InputRelationship, RelationshipType } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; type Props = {| eventId: string, + relationships: Array, + relationshipsTypes: Array, onAddRelationship: () => void |} -export const WidgetEventsRelationships = ({ eventId, onAddRelationship }: Props) => { - const { relationships } = useEventsRelationships(eventId); - const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationships); +export const WidgetEventsRelationships = ({ eventId, relationships, relationshipsTypes, onAddRelationship }: Props) => { + const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipsTypes, relationships); return ( , + relationshipTypes: Array, onAddRelationship: () => void, teiId: string, ...CssClasses, |}; -export const WidgetTeisRelationships = ({ relationships, teiId, onAddRelationship }: Props) => { - const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationships); +export const WidgetTeisRelationships = ({ relationships, relationshipTypes, teiId, onAddRelationship }: Props) => { + const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); return ( { - const dispatch = useDispatch(); - - const { - eventId: storedEventId, - relationships: storedRelationships, - } = useSelector(({ enrollmentPage }) => enrollmentPage); - - const { - data, - error, - refetch, - } = useDataQuery( - useMemo( - () => ({ - eventRelationships: { - resource: 'tracker/relationships', - params: ({ variables: { eventId: updatedEventId } }) => ({ - event: updatedEventId, - fields: ['relationshipType,to,from'], - }), - }, - }), - [], - ), - { lazy: true }, - ); - - const fetchedEventData = { - reference: data, - eventId, - relationships: data?.eventRelationships?.instances, - }; - - useEffect(() => { - if (fetchedEventData.reference) { - dispatch(setEventRelationshipsData( - fetchedEventData.eventId, - fetchedEventData.relationships, - )); - } - }, [ - dispatch, - fetchedEventData.reference, - fetchedEventData.relationships, - fetchedEventData.eventId, - ]); - - useEffect(() => { - if (storedEventId !== eventId) { - refetch({ variables: { eventId } }); - } - }, [storedEventId, eventId, refetch]); - - return { relationships: storedRelationships, error }; -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index c729b2e9c0..4e8449f893 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -2,54 +2,70 @@ import { useCallback, useEffect, useState } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; -import { - getCachedSingleResourceFromKeyAsync, -} from '../../../MetaDataStoreUtils/MetaDataStoreUtils'; -import { getProgramAndStageFromEvent } +import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } from '../../../metaData'; -import { userStores } from '../../../storageControllers/stores'; import type { - InputRelationship, RelationshipData, + InputRelationship, + RelationshipType, + RelationshipData, + TEIAttribute, + DataValue, } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getDisplayFieldsFromAPI, getBaseConfigHeaders } from '../../Pages/Enrollment/EnrollmentPageDefault/hooks/constants'; - -const convertAttributes = (attributes, displayFields, options) => displayFields.map((item) => { - if (item.convertValue) { +import { getBaseConfigHeaders, relationshipEntities } from '../../Pages/Enrollment/EnrollmentPageDefault/hooks/constants'; +import { convertServerToClient, convertClientToServer } from '../../../converters'; + +const convertAttributes = ( + attributes: Array | Array, + displayFields: Array, + options: Object, +): Array<{id: string, value: any}> => displayFields.map((field) => { + if (field.convertValue) { return { - id: item.id, - value: item.convertValue(options), + id: field.id, + value: field.convertValue(options), }; } - const attributeItem = Array.isArray(attributes) - ? attributes.find(({ attribute }) => attribute === item.id)?.value : attributes[item.id]; + + const attributeItem = attributes.find((att) => { + if (att.attribute) { return att.attribute === field.id; } + if (att.dataElement) { return att.dataElement === field.id; } + return undefined; + })?.value; + return { - id: item.id, - value: attributeItem, + id: field.id, + value: convertClientToServer(convertServerToClient(attributeItem, field.valueType), field.valueType), }; }); -const getDisplayFields = (type) => { - let displayFields = getDisplayFieldsFromAPI[type]; +const getDisplayFields = (linkedEntity) => { + let displayFields; + if (linkedEntity.relationshipEntity === relationshipEntities.TRACKED_ENTITY_INSTANCE) { + displayFields = linkedEntity.trackerDataView.attributes; + } else if (linkedEntity.relationshipEntity === relationshipEntities.PROGRAM_STAGE_INSTANCE) { + displayFields = linkedEntity.trackerDataView.dataElements; + } if (!displayFields?.length) { - displayFields = getBaseConfigHeaders[type]; + displayFields = getBaseConfigHeaders[linkedEntity.relationshipEntity]; } + return displayFields; }; const determineLinkedEntity = ( - relationshipType: Object, + relationshipType: RelationshipType, targetId: string, from: RelationshipData, to: RelationshipData, ) => { - const { fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; + const { id, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; if ((to.trackedEntity && to.trackedEntity.trackedEntity === targetId) || (to.event && to.event.event === targetId)) { - return { side: from, constraint: fromConstraint, relationshipName: toFromName }; + return { side: from, constraint: fromConstraint, relationshipName: toFromName, groupId: `${id}-from` }; } if ((from.trackedEntity && from.trackedEntity.trackedEntity === targetId) || (from.event && from.event.event === targetId)) { - return { side: to, constraint: toConstraint, relationshipName: fromToName }; + return { side: to, constraint: toConstraint, relationshipName: fromToName, groupId: `${id}-to` }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); @@ -74,7 +90,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { return { id: eventId, - attributes: linkedEntity.event, + values: linkedEntity.event.dataValues, options: { orgUnitName, status, @@ -83,10 +99,12 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { }, }; } else if (linkedEntity.trackedEntity) { + const { trackedEntityType, trackedEntity, attributes } = linkedEntity.trackedEntity; + const tet = getTrackedEntityTypeThrowIfNotFound(trackedEntityType); return { - id: linkedEntity.trackedEntity.trackedEntity, - attributes: linkedEntity.trackedEntity.attributes, - options: { }, + id: trackedEntity, + values: attributes, + options: { trackedEntityTypeName: tet.name }, }; } log.error(errorCreator('Relationship type is not handled')({ linkedEntity })); @@ -103,52 +121,51 @@ const getLinkedEntityInfo = ( if (!linkedEntityData) { return undefined; } const metadata = getAttributeConstraintsForTEI(linkedEntityData.side); if (!metadata) { return undefined; } - const { id, attributes, options } = metadata; - const displayFields = getDisplayFields(linkedEntityData.constraint.relationshipEntity); + const { id, values, options } = metadata; + const displayFields = getDisplayFields(linkedEntityData.constraint); return { id, - relationshipName: linkedEntityData?.relationshipName, - attributes: convertAttributes(attributes, displayFields, options), displayFields, + groupId: linkedEntityData.groupId, + relationshipName: linkedEntityData?.relationshipName, + values: convertAttributes(values, displayFields, options), }; }; -export const useLinkedEntityGroups = (targetId: string, relationships?: Array) => { +export const useLinkedEntityGroups = ( + targetId: string, + relationshipTypes: Array, + relationships?: Array, +) => { const [relationshipsByType, setRelationshipByType] = useState([]); const computeData = useCallback(async () => { - if (!relationships) { return; } - const relationshipTypePromises = relationships - .map(rel => getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, rel.relationshipType)); - - const relationshipTypes = await Promise - .all(relationshipTypePromises) - .then(results => results.map(res => res.response)); - - const linkedEntityGroups = relationships.reduce((acc, rel) => { - const { relationshipType: typeId, from, to } = rel; - const relationshipType = relationshipTypes.find(item => item.id === typeId); - const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to); - if (!metadata) { return acc; } - const { relationshipName, displayFields, id, attributes } = metadata; - const typeExist = acc.find(item => item.id === typeId); - if (typeExist) { - typeExist.linkedEntityData.push({ id, attributes }); - } else { - acc.push({ - id: typeId, - relationshipName, - linkedEntityData: [{ id, attributes }], - headers: displayFields, - }); - } - - return acc; - }, []); - - setRelationshipByType(linkedEntityGroups); - }, [relationships, targetId]); + if (relationships?.length && relationshipTypes?.length) { + const linkedEntityGroups = relationships.reduce((acc, rel) => { + const { relationshipType: typeId, from, to } = rel; + const relationshipType = relationshipTypes.find(item => item.id === typeId); + const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to); + if (!metadata) { return acc; } + const { relationshipName, displayFields, id, values, groupId } = metadata; + const typeExist = acc.find(item => item.id === groupId); + if (typeExist) { + typeExist.linkedEntityData.push({ id, values }); + } else { + acc.push({ + id: groupId, + relationshipName, + linkedEntityData: [{ id, values }], + headers: displayFields, + }); + } + + return acc; + }, []); + + setRelationshipByType(linkedEntityGroups); + } + }, [relationships, relationshipTypes, targetId]); useEffect(() => { computeData(); From 594ca2afc271bfcae1a2e2ff3439db48f4ab5804 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 5 May 2022 19:10:34 +0100 Subject: [PATCH 44/95] feat: [DHIS2-12362] add display fields --- .../EnrollmentOverviewDomain/useRelationshipTypesMetadata.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index cb0d0536a4..27bad5be8e 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -35,7 +35,6 @@ export const useRelationshipTypesMetadata = (relationships?: Array { - if (!dataValues) { return constraint; } if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { return { @@ -60,6 +59,7 @@ export const useRelationshipTypesMetadata = (relationships?: Array Date: Thu, 5 May 2022 19:13:05 +0100 Subject: [PATCH 45/95] feat: [DHIS2-12362] add display fields --- .../EnrollmentOverviewDomain/useRelationshipTypesMetadata.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index 27bad5be8e..445aa2952d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -35,6 +35,7 @@ export const useRelationshipTypesMetadata = (relationships?: Array { + if (!dataValues) { return constraint; } if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { return { From 015c4b34c8838ab1570fb70864648d6302d3aa6c Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 5 May 2022 19:45:01 +0100 Subject: [PATCH 46/95] chore: [DHIS2-12362] minor change --- .../capture-core/components/CardList/CardListItem.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index 14730305d1..608a8ce0a9 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -126,7 +126,7 @@ const CardListItemIndex = ({ ); }; - const enrollments = item.tei?.enrollments ?? []; + const enrollments = item.tei ? item.tei.enrollments : []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitName, enrolledAt } = deriveEnrollmentOrgUnitAndDate(enrollments, enrollmentType, currentProgramId); From 2d1af84b2b0a49139fd6772a010e9bc48a97317b Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 10 May 2022 08:31:41 +0100 Subject: [PATCH 47/95] chore:[DHIS2-12362] update user storage --- .../EnrollmentOverviewDomain/useRelationshipTypesMetadata.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index 445aa2952d..aa4f6a7561 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -4,6 +4,7 @@ import { getCachedSingleResourceFromKeyAsync, } from '../../../../MetaDataStoreUtils/MetaDataStoreUtils'; import { userStores } from '../../../../storageControllers/stores'; +import { getUserStorageController } from '../../../../storageControllers'; import { getApi } from '../../../../d2/d2Instance'; import type { InputRelationship } from './useCommonEnrollmentDomainData'; @@ -81,7 +82,9 @@ export const useRelationshipTypesMetadata = (relationships?: Array Promise.all(results.map(res => fetchDataView(res.response)))) .then((res) => { + const storageController = getUserStorageController(); setRelationshipTypesMetadata(res); + storageController.setAll(userStores.RELATIONSHIP_TYPES, res); }); } }, [relationships]); From 6f0973632355a50bdb03fcd65bbaca4990784264 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 10 May 2022 13:59:22 +0100 Subject: [PATCH 48/95] chore: [DHIS2-12362] minor change --- .../useRelationshipTypesMetadata.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index aa4f6a7561..475b703ad0 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -14,23 +14,22 @@ export const useRelationshipTypesMetadata = (relationships?: Array { const { fromConstraint, toConstraint } = relType; + const getPromise = (key, attributes) => { + if (attributes.length && !attributes.some(att => att.valueType)) { + return getApi().get(key, { + filter: `id:in:[${attributes.join(',')}]`, + fields: ['id,valueType,displayName'], + }); + } + return undefined; + }; + const fetchValues = async (constraint) => { let requestPromise; if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { - if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { - requestPromise = getApi().get('trackedEntityAttributes', { - filter: `id:in:[${constraint.trackerDataView.attributes.join(',')}]`, - fields: ['id,valueType,displayName'], - - }); - } + requestPromise = getPromise('trackedEntityAttributes', constraint.trackerDataView.attributes); } else if (constraint.relationshipEntity === 'PROGRAM_STAGE_INSTANCE') { - if (!constraint.trackerDataView.dataElements.every(att => att.valueType)) { - requestPromise = getApi().get('dataElements', { - filter: `id:in:[${constraint.trackerDataView.dataElements.join(',')}]`, - fields: ['id,valueType,displayName'], - }); - } + requestPromise = getPromise('dataElements', constraint.trackerDataView.dataElements); } return requestPromise; }; @@ -38,7 +37,8 @@ export const useRelationshipTypesMetadata = (relationships?: Array { if (!dataValues) { return constraint; } if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { - if (!constraint.trackerDataView.attributes.every(att => att.valueType)) { + if (constraint.trackerDataView.attributes.length + && !constraint.trackerDataView.attributes.some(att => att.valueType)) { return { ...constraint, trackerDataView: { @@ -49,7 +49,8 @@ export const useRelationshipTypesMetadata = (relationships?: Array att.valueType)) { + if (constraint.trackerDataView.dataElements.length + && !constraint.trackerDataView.dataElements.some(att => att.valueType)) { return { ...constraint, trackerDataView: { From 7efe0efcbd8f0af3398c073466f0cdaa785515cb Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 19 May 2022 13:04:04 +0100 Subject: [PATCH 49/95] chore: [DHIS2-12362] add creation date --- .../useCommonEnrollmentDomainData.js | 2 +- .../useCommonEnrollmentDomainData.types.js | 1 + .../hooks/useLinkedEntityGroups.js | 12 +++++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index fd38efa9ec..0677654f22 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -44,7 +44,7 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin resource: 'tracker/relationships', params: ({ variables: { teiId: updatedTeiId } }) => ({ tei: updatedTeiId, - fields: ['relationshipType,to,from'], + fields: ['relationshipType,to,from,createdAt'], }), }, }), diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 872ab73147..b00e7a2643 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -77,6 +77,7 @@ export type RelationshipData = TEIRelationshipData | EventRelationshipData export type InputRelationship = {| relationshipType: string, relationshipName: string, + createdAt: string, relationship: string, bidirectional: boolean, from: RelationshipData, diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 4e8449f893..c18b20607b 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -73,7 +73,7 @@ const determineLinkedEntity = ( }; -const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { +const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, createdAt: string) => { if (linkedEntity.event) { const { event: eventId, program: programId, programStage, orgUnitName, status } = linkedEntity.event; /* @@ -96,6 +96,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { status, programName: program?.name, programStageName: stage?.stageForm?.name, + created: createdAt, }, }; } else if (linkedEntity.trackedEntity) { @@ -104,7 +105,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData) => { return { id: trackedEntity, values: attributes, - options: { trackedEntityTypeName: tet.name }, + options: { trackedEntityTypeName: tet.name, created: createdAt }, }; } log.error(errorCreator('Relationship type is not handled')({ linkedEntity })); @@ -116,10 +117,11 @@ const getLinkedEntityInfo = ( targetId: string, from: RelationshipData, to: RelationshipData, + createdAt: string, ) => { const linkedEntityData = determineLinkedEntity(relationshipType, targetId, from, to); if (!linkedEntityData) { return undefined; } - const metadata = getAttributeConstraintsForTEI(linkedEntityData.side); + const metadata = getAttributeConstraintsForTEI(linkedEntityData.side, createdAt); if (!metadata) { return undefined; } const { id, values, options } = metadata; const displayFields = getDisplayFields(linkedEntityData.constraint); @@ -143,9 +145,9 @@ export const useLinkedEntityGroups = ( const computeData = useCallback(async () => { if (relationships?.length && relationshipTypes?.length) { const linkedEntityGroups = relationships.reduce((acc, rel) => { - const { relationshipType: typeId, from, to } = rel; + const { relationshipType: typeId, from, to, createdAt } = rel; const relationshipType = relationshipTypes.find(item => item.id === typeId); - const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to); + const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); if (!metadata) { return acc; } const { relationshipName, displayFields, id, values, groupId } = metadata; const typeExist = acc.find(item => item.id === groupId); From 4e21fc48125839608e8262f5a8b71ce664343afd Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 23 May 2022 11:20:20 +0100 Subject: [PATCH 50/95] chore: [DHIS2-12362] update event --- .../EnrollmentEditEventPage.component.js | 7 ++++--- .../EnrollmentEditEventPage.container.js | 5 +++-- .../EnrollmentEditEvent/EnrollmentEditEventPage.types.js | 3 ++- .../WidgetEventsRelationships.component.js | 6 +++--- .../capture-core/relationships/convertServerToClient.js | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 161d1970bf..d2cf310869 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -67,7 +67,8 @@ const EnrollmentEditEventPagePain = ({ programId, relationships, eventRelationships, - relationshipTypes, + teiRelationshipTypes, + eventRelationshipTypes, enrollmentsAsOptions, trackedEntityName, teiDisplayName, @@ -193,13 +194,13 @@ const EnrollmentEditEventPagePain = ({ {}} /> {}} /> {!hideWidgets.feedback && ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index 3f30618a78..daa0dab847 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -55,7 +55,7 @@ export const EnrollmentEditEventPage = () => { : (pageStatus = pageStatuses.MISSING_DATA); } else pageStatus = pageStatuses.WITHOUT_ORG_UNIT_SELECTED; const { relationships: eventRelationships } = useEventsRelationships(eventId); - const relationshipTypes = useRelationshipTypesMetadata(relationships); + const teiRelationshipTypes = useRelationshipTypesMetadata(relationships); const eventRelationshipTypes = useRelationshipTypesMetadata(eventRelationships); return ( @@ -68,7 +68,8 @@ export const EnrollmentEditEventPage = () => { hideWidgets={hideWidgets} relationships={relationships} eventRelationships={eventRelationships} - relationshipTypes={[...relationshipTypes, ...eventRelationshipTypes]} + teiRelationshipTypes={teiRelationshipTypes} + eventRelationshipTypes={eventRelationshipTypes} teiId={teiId} eventId={eventId} enrollmentId={enrollmentId} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 69ebb762f6..b8ed1c4c08 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -17,7 +17,8 @@ export type PlainProps = {| teiDisplayName: string, relationships?: Array, eventRelationships?: Array, - relationshipTypes?: Array, + eventRelationshipTypes?: Array, + teiRelationshipTypes?: Array, eventDate?: string, enrollmentsAsOptions: Array, onDelete: () => void, diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index 0f58d3d267..3430428606 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -8,12 +8,12 @@ import type { InputRelationship, RelationshipType } from '../../Pages/common/Enr type Props = {| eventId: string, relationships: Array, - relationshipsTypes: Array, + relationshipTypes: Array, onAddRelationship: () => void |} -export const WidgetEventsRelationships = ({ eventId, relationships, relationshipsTypes, onAddRelationship }: Props) => { - const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipsTypes, relationships); +export const WidgetEventsRelationships = ({ eventId, relationships, relationshipTypes, onAddRelationship }: Props) => { + const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipTypes, relationships); return ( Date: Mon, 23 May 2022 17:43:38 +0100 Subject: [PATCH 51/95] chore: [DHIS2-12362] move type def --- i18n/en.pot | 22 +++--- .../EnrollmentPageDefault.types.js | 3 +- .../EnrollmentEditEventPage.types.js | 2 +- .../useCommonEnrollmentDomainData.types.js | 64 +----------------- .../useRelationshipTypesMetadata.js | 8 +-- .../RelationshipsWidget.component.js | 2 +- .../WidgetEventsRelationships.component.js | 2 +- .../WidgetTeisRelationships.component.js | 2 +- .../constants.js | 14 ++-- .../hooks/useLinkedEntityGroups.js | 12 ++-- .../components/WidgetRelationships/types.js | 67 +++++++++++++++++++ 11 files changed, 100 insertions(+), 98 deletions(-) rename src/core_modules/capture-core/components/{Pages/Enrollment/EnrollmentPageDefault/hooks => WidgetRelationships}/constants.js (68%) create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/types.js diff --git a/i18n/en.pot b/i18n/en.pot index a3171a8a21..e293d9bab6 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-05-02T08:15:29.467Z\n" -"PO-Revision-Date: 2022-05-02T08:15:29.467Z\n" +"POT-Creation-Date: 2022-05-23T16:15:34.379Z\n" +"PO-Revision-Date: 2022-05-23T16:15:34.379Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -649,15 +649,6 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" -msgid "TET name" -msgstr "TET name" - -msgid "Created date" -msgstr "Created date" - -msgid "Program stage name" -msgstr "Program stage name" - msgid "Program stage not found" msgstr "Program stage not found" @@ -1199,6 +1190,15 @@ msgstr "Event's Relationships" msgid "TEI's Relationships" msgstr "TEI's Relationships" +msgid "TET name" +msgstr "TET name" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 002909241b..24124ec92c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,7 +2,8 @@ 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'; -import type { Event, InputRelationship, RelationshipType } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship, RelationshipType } from '../../../WidgetRelationships/types'; export type Props = {| program: Program, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index b8ed1c4c08..70e0c4b06f 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { InputRelationship, RelationshipType } from '../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship, RelationshipType } from '../../WidgetRelationships/types'; export type PlainProps = {| programStage: ?ProgramStage, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index b00e7a2643..7521c72f7f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,4 +1,5 @@ // @flow +import type { InputRelationship } from '../../../../WidgetRelationships/types'; export type DataValue = { dataElement: string, @@ -49,69 +50,6 @@ export type AttributeValue = {| value: string, |}; -export type TEIAttribute = {| - attribute: string, - displayName: string, - value: string, - valueType: string, -|} - -export type TEIRelationshipData = {| - trackedEntity: { - trackedEntityType: string, - trackedEntity: string, - attributes: Array - } -|} - -export type EventRelationshipData = {| - event: Event -|} - -export type EnrollmentRelationshipData = {| - enrollment: EnrollmentData -|} - -export type RelationshipData = TEIRelationshipData | EventRelationshipData - -export type InputRelationship = {| - relationshipType: string, - relationshipName: string, - createdAt: string, - relationship: string, - bidirectional: boolean, - from: RelationshipData, - to: RelationshipData -|} - -export type OutputRelationship = { - id: string, - relationshipName: string, - linkedEntityData: Array<{ id: string, values: Array}> -} - -export type RelationshipConstraintDataView = string | {id: string, displayName: string, valueType: string} - -export type RelationshipConstraint = { - relationshipEntity: string, - trackedEntityType?: ?{ id: string }, - program?: ?{ id: string }, - programStage?: ?{ id: string }, - trackerDataView: { - attributes: Array, - dataElements: Array, - } -} - -export type RelationshipType = { - id: string, - bidirectional: boolean, - displayName: string, - fromConstraint: RelationshipConstraint, - fromToName: string, - toConstraint: RelationshipConstraint, - toFromName: string, -} export type Output = {| error?: any, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index 475b703ad0..085f84824f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -6,7 +6,7 @@ import { import { userStores } from '../../../../storageControllers/stores'; import { getUserStorageController } from '../../../../storageControllers'; import { getApi } from '../../../../d2/d2Instance'; -import type { InputRelationship } from './useCommonEnrollmentDomainData'; +import type { InputRelationship } from '../../../WidgetRelationships/types'; export const useRelationshipTypesMetadata = (relationships?: Array) => { const [relationshipTypesMetadata, setRelationshipTypesMetadata] = useState([]); @@ -15,7 +15,7 @@ export const useRelationshipTypesMetadata = (relationships?: Array { - if (attributes.length && !attributes.some(att => att.valueType)) { + if (attributes.length && !attributes.every(att => att.valueType)) { return getApi().get(key, { filter: `id:in:[${attributes.join(',')}]`, fields: ['id,valueType,displayName'], @@ -38,7 +38,7 @@ export const useRelationshipTypesMetadata = (relationships?: Array att.valueType)) { + && !constraint.trackerDataView.attributes.every(att => att.valueType)) { return { ...constraint, trackerDataView: { @@ -50,7 +50,7 @@ export const useRelationshipTypesMetadata = (relationships?: Array att.valueType)) { + && !constraint.trackerDataView.dataElements.every(att => att.valueType)) { return { ...constraint, trackerDataView: { diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index db2345af2f..7b192caeda 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -3,7 +3,7 @@ import React, { useState, useCallback } from 'react'; import { Chip } from '@dhis2/ui'; import { Widget } from '../../Widget'; import { Relationships } from './Relationships.component'; -import type { OutputRelationship } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { OutputRelationship } from '../types'; type Props = {| relationships: Array, diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index 3430428606..4e7d89470b 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -3,7 +3,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { InputRelationship, RelationshipType } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship, RelationshipType } from '../types'; type Props = {| eventId: string, diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index 393b1359f1..32976a09c1 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -3,7 +3,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { InputRelationship, RelationshipType } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { InputRelationship, RelationshipType } from '../types'; type Props = {| relationships: Array, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js b/src/core_modules/capture-core/components/WidgetRelationships/constants.js similarity index 68% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js rename to src/core_modules/capture-core/components/WidgetRelationships/constants.js index 54bda7d29b..0eb48232b9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/constants.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/constants.js @@ -1,6 +1,8 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; +import { dataElementTypes } from '../../metaData'; +import { convertServerToClient, convertClientToList } from '../../converters'; export const relationshipEntities = Object.freeze({ TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', @@ -8,12 +10,6 @@ export const relationshipEntities = Object.freeze({ PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', }); -export const relationshipWidgetTypes = Object.freeze({ - TET_RELATIONSHIP: 'TET_RELATIONSHIP', - ENROLLMENT_RELATIONSHIP: 'ENROLLMENT_RELATIONSHIP', - EVENT_RELATIONSHIP: 'EVENT_RELATIONSHIP', -}); - export const getBaseConfigHeaders = { [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ id: 'tetName', @@ -22,7 +18,7 @@ export const getBaseConfigHeaders = { }, { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => moment(props.created).format('YYYY-MM-DD'), + convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', @@ -32,11 +28,11 @@ export const getBaseConfigHeaders = { { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => moment(props.created).format('YYYY-MM-DD'), + convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), }], [relationshipEntities.PROGRAM_INSTANCE]: [{ id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => moment(props.created).format('YYYY-MM-DD'), + convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), }], }; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index c18b20607b..0afa1511fd 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -9,10 +9,10 @@ import type { RelationshipType, RelationshipData, TEIAttribute, - DataValue, -} from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getBaseConfigHeaders, relationshipEntities } from '../../Pages/Enrollment/EnrollmentPageDefault/hooks/constants'; -import { convertServerToClient, convertClientToServer } from '../../../converters'; +} from '../../WidgetRelationships/types'; +import type { DataValue } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import { getBaseConfigHeaders, relationshipEntities } from '../constants'; +import { convertServerToClient, convertClientToList } from '../../../converters'; const convertAttributes = ( attributes: Array | Array, @@ -34,7 +34,7 @@ const convertAttributes = ( return { id: field.id, - value: convertClientToServer(convertServerToClient(attributeItem, field.valueType), field.valueType), + value: convertClientToList(convertServerToClient(attributeItem, field.valueType), field.valueType), }; }); @@ -137,7 +137,7 @@ const getLinkedEntityInfo = ( export const useLinkedEntityGroups = ( targetId: string, - relationshipTypes: Array, + relationshipTypes: Array, relationships?: Array, ) => { const [relationshipsByType, setRelationshipByType] = useState([]); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/types.js new file mode 100644 index 0000000000..4204b4a89c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/types.js @@ -0,0 +1,67 @@ +// @flow +import type { EnrollmentData, Event } + from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types'; + +export type TEIAttribute = {| + attribute: string, + displayName: string, + value: string, + valueType: string, +|} + +export type TEIRelationshipData = {| + trackedEntity: { + trackedEntityType: string, + trackedEntity: string, + attributes: Array + } +|} + +export type EventRelationshipData = {| + event: Event +|} + +export type EnrollmentRelationshipData = {| + enrollment: EnrollmentData +|} + +export type RelationshipData = TEIRelationshipData | EventRelationshipData + +export type InputRelationship = {| + relationshipType: string, + relationshipName: string, + createdAt: string, + relationship: string, + bidirectional: boolean, + from: RelationshipData, + to: RelationshipData +|} + +export type OutputRelationship = { + id: string, + relationshipName: string, + linkedEntityData: Array<{ id: string, values: Array}> +} + +export type RelationshipConstraintDataView = string | {id: string, displayName: string, valueType: string} + +export type RelationshipConstraint = { + relationshipEntity: string, + trackedEntityType?: ?{ id: string }, + program?: ?{ id: string }, + programStage?: ?{ id: string }, + trackerDataView: { + attributes: Array, + dataElements: Array, + } +} + +export type RelationshipType = { + id: string, + bidirectional: boolean, + displayName: string, + fromConstraint: RelationshipConstraint, + fromToName: string, + toConstraint: RelationshipConstraint, + toFromName: string, +} From 3bce66506c365d7f0e155ffc9264094b8771dda8 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 23 May 2022 17:46:50 +0100 Subject: [PATCH 52/95] chore: [DHIS2-12362] move type def --- .../EnrollmentPageDefault.types.js | 2 +- .../EnrollmentEditEventPage.types.js | 2 +- .../useCommonEnrollmentDomainData.types.js | 2 +- .../useRelationshipTypesMetadata.js | 2 +- .../RelationshipsWidget.component.js | 2 +- .../WidgetEventsRelationships.component.js | 9 +-------- .../WidgetEventsRelationships/types.js | 10 ++++++++++ .../WidgetTeisRelationships.component.js | 10 +--------- .../WidgetTeisRelationships/types.js | 10 ++++++++++ .../WidgetRelationships/{types.js => common.types.js} | 0 .../WidgetRelationships/hooks/useLinkedEntityGroups.js | 2 +- 11 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js create mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js rename src/core_modules/capture-core/components/WidgetRelationships/{types.js => common.types.js} (100%) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 24124ec92c..49655f3595 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -3,7 +3,7 @@ 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'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import type { InputRelationship, RelationshipType } from '../../../WidgetRelationships/types'; +import type { InputRelationship, RelationshipType } from '../../../WidgetRelationships/common.types'; export type Props = {| program: Program, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 70e0c4b06f..4f49bae71b 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { InputRelationship, RelationshipType } from '../../WidgetRelationships/types'; +import type { InputRelationship, RelationshipType } from '../../WidgetRelationships/common.types'; export type PlainProps = {| programStage: ?ProgramStage, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 7521c72f7f..5369783615 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,5 +1,5 @@ // @flow -import type { InputRelationship } from '../../../../WidgetRelationships/types'; +import type { InputRelationship } from '../../../../WidgetRelationships/common.types'; export type DataValue = { dataElement: string, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index 085f84824f..e575481cd4 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -6,7 +6,7 @@ import { import { userStores } from '../../../../storageControllers/stores'; import { getUserStorageController } from '../../../../storageControllers'; import { getApi } from '../../../../d2/d2Instance'; -import type { InputRelationship } from '../../../WidgetRelationships/types'; +import type { InputRelationship } from '../../../WidgetRelationships/common.types'; export const useRelationshipTypesMetadata = (relationships?: Array) => { const [relationshipTypesMetadata, setRelationshipTypesMetadata] = useState([]); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index 7b192caeda..6749e11151 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -3,7 +3,7 @@ import React, { useState, useCallback } from 'react'; import { Chip } from '@dhis2/ui'; import { Widget } from '../../Widget'; import { Relationships } from './Relationships.component'; -import type { OutputRelationship } from '../types'; +import type { OutputRelationship } from '../common.types'; type Props = {| relationships: Array, diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index 4e7d89470b..1a65780884 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -3,14 +3,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { InputRelationship, RelationshipType } from '../types'; - -type Props = {| - eventId: string, - relationships: Array, - relationshipTypes: Array, - onAddRelationship: () => void -|} +import type { Props } from './types'; export const WidgetEventsRelationships = ({ eventId, relationships, relationshipTypes, onAddRelationship }: Props) => { const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipTypes, relationships); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js new file mode 100644 index 0000000000..8c03663e88 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js @@ -0,0 +1,10 @@ +// @flow + +import type { InputRelationship, RelationshipType } from '../common.types'; + +export type Props = {| + eventId: string, + relationships: Array, + relationshipTypes: Array, + onAddRelationship: () => void +|} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index 32976a09c1..4b771f677f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -3,15 +3,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { InputRelationship, RelationshipType } from '../types'; - -type Props = {| - relationships: Array, - relationshipTypes: Array, - onAddRelationship: () => void, - teiId: string, - ...CssClasses, -|}; +import type { Props } from './types'; export const WidgetTeisRelationships = ({ relationships, relationshipTypes, teiId, onAddRelationship }: Props) => { const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js new file mode 100644 index 0000000000..65df23b52f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js @@ -0,0 +1,10 @@ +// @flow +import type { InputRelationship, RelationshipType } from '../common.types'; + +export type Props = {| + relationships: Array, + relationshipTypes: Array, + onAddRelationship: () => void, + teiId: string, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/common.types.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetRelationships/types.js rename to src/core_modules/capture-core/components/WidgetRelationships/common.types.js diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 0afa1511fd..cca0de7d4c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -9,7 +9,7 @@ import type { RelationshipType, RelationshipData, TEIAttribute, -} from '../../WidgetRelationships/types'; +} from '../common.types'; import type { DataValue } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import { getBaseConfigHeaders, relationshipEntities } from '../constants'; import { convertServerToClient, convertClientToList } from '../../../converters'; From 59e10264778b7981e2176e97dcad43da15378d1a Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 23 May 2022 18:54:15 +0100 Subject: [PATCH 53/95] chore: [DHIS2-12362] minor fix --- .../capture-core/components/WidgetRelationships/constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/constants.js b/src/core_modules/capture-core/components/WidgetRelationships/constants.js index 0eb48232b9..a7f12b64ae 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/constants.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/constants.js @@ -1,5 +1,3 @@ - -import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { dataElementTypes } from '../../metaData'; import { convertServerToClient, convertClientToList } from '../../converters'; From f3d5d2d6b9305b2c18b3e0dd424aa44196bb17ba Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 24 May 2022 11:03:39 +0100 Subject: [PATCH 54/95] chore: [DHIS2-12362] return all types --- .../useRelationshipTypesMetadata.js | 40 ++++++++++--------- .../hooks/useLinkedEntityGroups.js | 3 +- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js index e575481cd4..0bfd7855ff 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js @@ -1,24 +1,29 @@ // @flow -import { useEffect, useState } from 'react'; -import { - getCachedSingleResourceFromKeyAsync, -} from '../../../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { useCallback, useEffect, useState } from 'react'; +import { useDataEngine } from '@dhis2/app-runtime'; +import { makeQuerySingleResource } from 'capture-core/utils/api'; import { userStores } from '../../../../storageControllers/stores'; import { getUserStorageController } from '../../../../storageControllers'; -import { getApi } from '../../../../d2/d2Instance'; import type { InputRelationship } from '../../../WidgetRelationships/common.types'; export const useRelationshipTypesMetadata = (relationships?: Array) => { const [relationshipTypesMetadata, setRelationshipTypesMetadata] = useState([]); + const dataEngine = useDataEngine(); - const fetchDataView = async (relType) => { + const fetchDataView = useCallback(async (relType) => { + if (!relationships?.find(({ relationshipType }) => relationshipType === relType.id)) { return relType; } const { fromConstraint, toConstraint } = relType; - const getPromise = (key, attributes) => { + const querySingleResource = makeQuerySingleResource(dataEngine.query.bind(dataEngine)); + + const getPromise = (resource, attributes) => { if (attributes.length && !attributes.every(att => att.valueType)) { - return getApi().get(key, { - filter: `id:in:[${attributes.join(',')}]`, - fields: ['id,valueType,displayName'], + return querySingleResource({ + resource, + params: { + filter: `id:in:[${attributes.join(',')}]`, + fields: ['id,valueType,displayName'], + }, }); } return undefined; @@ -73,23 +78,20 @@ export const useRelationshipTypesMetadata = (relationships?: Array { if (relationships) { - const relationshipTypePromises = relationships - .map(rel => getCachedSingleResourceFromKeyAsync(userStores.RELATIONSHIP_TYPES, rel.relationshipType)); - Promise - .all(relationshipTypePromises) - .then(results => Promise.all(results.map(res => fetchDataView(res.response)))) + const storageController = getUserStorageController(); + const allRelationshipTypePromises = storageController.getAll(userStores.RELATIONSHIP_TYPES); + allRelationshipTypePromises + .then(results => Promise.all(results.map(res => fetchDataView(res)))) .then((res) => { - const storageController = getUserStorageController(); setRelationshipTypesMetadata(res); storageController.setAll(userStores.RELATIONSHIP_TYPES, res); }); } - }, [relationships]); - + }, [relationships, fetchDataView]); return relationshipTypesMetadata; }; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index cca0de7d4c..51c70b21cc 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -113,7 +113,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, createdAt }; const getLinkedEntityInfo = ( - relationshipType: Object, + relationshipType: RelationshipType, targetId: string, from: RelationshipData, to: RelationshipData, @@ -147,6 +147,7 @@ export const useLinkedEntityGroups = ( const linkedEntityGroups = relationships.reduce((acc, rel) => { const { relationshipType: typeId, from, to, createdAt } = rel; const relationshipType = relationshipTypes.find(item => item.id === typeId); + if (!relationshipType) { return acc; } const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); if (!metadata) { return acc; } const { relationshipName, displayFields, id, values, groupId } = metadata; From 0607b79447b39d4bb1aeac063398ccd08818ce55 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Sat, 4 Jun 2022 13:45:33 +0100 Subject: [PATCH 55/95] chore: [DHIS2-12362] add created relationship date --- .../capture-core/relationships/relationshipRequests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/relationships/relationshipRequests.js b/src/core_modules/capture-core/relationships/relationshipRequests.js index 160d863f95..4652529041 100644 --- a/src/core_modules/capture-core/relationships/relationshipRequests.js +++ b/src/core_modules/capture-core/relationships/relationshipRequests.js @@ -16,5 +16,5 @@ export function getRelationshipsForEvent(eventId: string, programId: string, pro const program = getProgramThrowIfNotFound(programId); const stage = program instanceof EventProgram ? program.stage : program.getStage(programStageId); const relationshipTypes = stage?.relationshipTypes || []; - return getRelationships({ event: eventId, fields: ['from,to,relationshipType,relationship'] }, relationshipTypes); + return getRelationships({ event: eventId, fields: ['from,to,relationshipType,relationship,createdAt'] }, relationshipTypes); } From c68a66618a63a9789b3acbefb5d3ea2b8c08cd3d Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Thu, 9 Jun 2022 08:43:12 +0100 Subject: [PATCH 56/95] chore: [DHIS2-12362] use display name --- .../hooks/useLinkedEntityGroups.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 51c70b21cc..88187eb5d2 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -58,14 +58,14 @@ const determineLinkedEntity = ( from: RelationshipData, to: RelationshipData, ) => { - const { id, fromToName, toFromName, toConstraint, fromConstraint } = relationshipType; + const { id, toConstraint, fromConstraint } = relationshipType; if ((to.trackedEntity && to.trackedEntity.trackedEntity === targetId) || (to.event && to.event.event === targetId)) { - return { side: from, constraint: fromConstraint, relationshipName: toFromName, groupId: `${id}-from` }; + return { side: from, constraint: fromConstraint, groupId: `${id}-from` }; } if ((from.trackedEntity && from.trackedEntity.trackedEntity === targetId) || (from.event && from.event.event === targetId)) { - return { side: to, constraint: toConstraint, relationshipName: fromToName, groupId: `${id}-to` }; + return { side: to, constraint: toConstraint, groupId: `${id}-to` }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); @@ -129,7 +129,6 @@ const getLinkedEntityInfo = ( id, displayFields, groupId: linkedEntityData.groupId, - relationshipName: linkedEntityData?.relationshipName, values: convertAttributes(values, displayFields, options), }; }; @@ -150,14 +149,14 @@ export const useLinkedEntityGroups = ( if (!relationshipType) { return acc; } const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); if (!metadata) { return acc; } - const { relationshipName, displayFields, id, values, groupId } = metadata; + const { displayFields, id, values, groupId } = metadata; const typeExist = acc.find(item => item.id === groupId); if (typeExist) { typeExist.linkedEntityData.push({ id, values }); } else { acc.push({ id: groupId, - relationshipName, + relationshipName: relationshipType.displayName, linkedEntityData: [{ id, values }], headers: displayFields, }); From 419ae846059cf8370e06d7c624ba37aa5b0e7f90 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Fri, 15 Jul 2022 07:40:18 +0100 Subject: [PATCH 57/95] fix: [DHIS2-13202] fix occurred at label in stages --- i18n/en.pot | 7 ++----- .../EnrollmentPageDefault.component.js | 1 + .../StageDetail/StageDetail.component.js | 5 ++++- .../Stage/StageDetail/hooks/useEventList.js | 19 +++++++++++-------- .../Stages/Stage/stage.types.js | 1 + .../types/common.types.js | 1 + 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9eb0ef7589..8a557b2659 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-06-20T08:58:04.154Z\n" -"PO-Revision-Date: 2022-06-20T08:58:04.154Z\n" +"POT-Creation-Date: 2022-07-15T06:40:21.486Z\n" +"PO-Revision-Date: 2022-07-15T06:40:21.486Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1230,9 +1230,6 @@ msgstr "This stage can only have one event" msgid "Events could not be retrieved. Please try again later." msgstr "Events could not be retrieved. Please try again later." -msgid "Report date" -msgstr "Report date" - msgid "Registering unit" msgstr "Registering unit" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 62028c309c..3720ba601b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -68,6 +68,7 @@ export const EnrollmentPageDefaultPlain = ({ events={events} /> { events, eventName, stageId, + programId, dataElements, hideDueDate = false, repeatable = false, @@ -71,7 +73,8 @@ const StageDetailPlain = (props: Props) => { columnName: 'status', sortDirection: SORT_DIRECTION.DESC, }; - const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate); + const { stage } = getProgramAndStageForProgram(programId, stageId); + const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, stage?.stageForm); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElements, events); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js index c64ae10bd6..a0e6b1ffb7 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js @@ -23,19 +23,21 @@ const basedFieldTypes = [ { type: dataElementTypes.DATE }, { type: dataElementTypes.UNKNOWN, resolveValue: convertCommentForView }, ]; -const baseColumnHeaders = [ +const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: i18n.t('Report date'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { - header: i18n.t('Due date'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true, + header: props.formFoundation.getLabel('occurredAt'), + sortDirection: SORT_DIRECTION.DEFAULT, + isPredefined: true, }, + { header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: i18n.t('Due date'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, ]; const baseFields = baseKeys.map((key, index) => ({ ...key, ...basedFieldTypes[index] })); // $FlowFixMe -const baseColumns = baseFields.map((key, index) => ({ ...key, ...baseColumnHeaders[index] })); +const getBaseColumns = props => baseFields.map((key, index) => ({ ...key, ...getBaseColumnHeaders(props)[index] })); const getAllFieldsWithValue = ( eventId: string, @@ -103,7 +105,8 @@ const useComputeDataFromEvent = (dataElements: Array, events: return { value, error, loading }; }; -const useComputeHeaderColumn = (dataElements: Array, hideDueDate: boolean) => { + +const useComputeHeaderColumn = (dataElements: Array, hideDueDate: boolean, formFoundation: Object) => { const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { const { id, name, type } = currDataElement; @@ -113,9 +116,9 @@ const useComputeHeaderColumn = (dataElements: Array, hideDueDa return acc; }, []); return [ - ...baseColumns.filter(col => (hideDueDate ? col.id !== 'scheduledAt' : true)), + ...getBaseColumns({ formFoundation }).filter(col => (hideDueDate ? col.id !== 'scheduledAt' : true)), ...dataElementHeaders]; - }, [dataElements, hideDueDate]); + }, [dataElements, hideDueDate, formFoundation]); return headerColumns; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js index 83e55ecf85..6511bf615a 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.js @@ -2,6 +2,7 @@ import type { Stage, StageCommonProps } from '../../types/common.types'; type ExtractedProps = {| + programId: string, stage: Stage, events: Array, className?: string, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js index e3a148678c..377251ce90 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js @@ -24,6 +24,7 @@ export type Stage = { export type StageCommonProps = {| ready?: boolean, + programId: string, onViewAll: (stageId: string) => void, onCreateNew: (stageId: string) => void, onEventClick: (eventId: string, stageId: string) => void From 8c1fd1e6249654ae9a2e57cd60d47f25dc464d11 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Wed, 20 Jul 2022 13:09:21 +0100 Subject: [PATCH 58/95] fix: [DHIS2-12362] cypress --- .../integration/EnrollmentPage/StagesAndEventsWidget.feature | 4 ++-- .../integration/EnrollmentPage/StagesAndEventsWidget/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature b/cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature index bced1102d7..7f191039c7 100644 --- a/cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature +++ b/cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature @@ -37,8 +37,8 @@ Feature: User interacts with Stages and Events Widget Scenario: User can sort the list of events Given you open the enrollment page which has multiples events and stages Then the default list should be displayed - When you sort list asc by Report date - Then the sorted list by Report date asc should be displayed + When you sort list asc by Date of visit + Then the sorted list by Date of visit asc should be displayed # Scenario: User can go to Program Stage list by clicking Go to full # Given you open the enrollment page which has multiples events and stages diff --git a/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js b/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js index c573307fc8..378dbb5f92 100644 --- a/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js +++ b/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js @@ -147,7 +147,7 @@ When(/^you sort list asc by (.*)$/, (columnName) => { }); }); -Then('the sorted list by Report date asc should be displayed', () => { +Then(/^the sorted list by (.*) asc should be displayed$/, () => { const rows = [ '2021-07-13|Bumbeh MCHP', '2021-07-12|Bumbeh MCHP', From 5fd15f858a9427153fc55786e5fad037c8dd77de Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 2 Aug 2022 12:25:06 +0100 Subject: [PATCH 59/95] fix: [DHIS2-12362] convert date --- .../components/WidgetRelationships/constants.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/constants.js b/src/core_modules/capture-core/components/WidgetRelationships/constants.js index a7f12b64ae..e76b14b6db 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/constants.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/constants.js @@ -16,7 +16,9 @@ export const getBaseConfigHeaders = { }, { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), + convertValue: props => convertClientToList( + convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, + ), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', @@ -26,11 +28,15 @@ export const getBaseConfigHeaders = { { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), + convertValue: props => convertClientToList( + convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, + ), }], [relationshipEntities.PROGRAM_INSTANCE]: [{ id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList(convertServerToClient(props.created, dataElementTypes.DATE)), + convertValue: props => convertClientToList( + convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, + ), }], }; From c689e4de5eac2465f4e953a3d1d1cfb08a34c6f2 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 29 Aug 2022 11:12:30 +0100 Subject: [PATCH 60/95] chore: [DHIS2-12362] UI improvements --- i18n/en.pot | 10 +-- .../Relationships.component.js | 11 ++-- .../RelationshipsTable.component.js | 61 +++++++++++++++---- .../RelationshipsWidget.component.js | 22 +++++-- .../hooks/useLinkedEntityGroups.js | 14 +++-- 5 files changed, 88 insertions(+), 30 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3db338a4d1..49fded3208 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-07-18T12:33:35.059Z\n" -"PO-Revision-Date: 2022-07-18T12:33:35.059Z\n" +"POT-Creation-Date: 2022-08-29T08:45:32.053Z\n" +"PO-Revision-Date: 2022-08-29T08:45:32.053Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1215,6 +1215,9 @@ msgstr "{{TETName}} profile" msgid "Edit" msgstr "Edit" +msgid "Show {{ rest }} more" +msgstr "Show {{ rest }} more" + msgid "Event's Relationships" msgstr "Event's Relationships" @@ -1236,9 +1239,6 @@ msgstr "New {{ eventName }} event" msgid "This event is not yet preserved and cannot be edited" msgstr "This event is not yet preserved and cannot be edited" -msgid "Show {{ rest }} more" -msgstr "Show {{ rest }} more" - msgid "Reset list" msgstr "Reset list" diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js index 6e49e01a9f..dde1dcf1ca 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js @@ -1,7 +1,7 @@ // @flow import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; -import i18n from '@dhis2/d2-i18n'; +// import i18n from '@dhis2/d2-i18n'; import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; @@ -19,9 +19,12 @@ const styles = { fontWeight: 500, fontSize: 16, color: colors.grey800, - paddingBottom: spacersNum.dp8, + paddingBottom: spacersNum.dp16, }, wrapper: { + '&:not(:last-first)': { + paddingTop: spacersNum.dp24, + }, paddingBottom: spacersNum.dp16, overflow: 'scroll', }, @@ -40,9 +43,9 @@ const RelationshipsPlain = ({ relationships, classes, onAddRelationship }: Props ); }) : null } - + */} ); export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js index eb42748262..3b723af541 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useState, type ComponentType } from 'react'; import { DataTableBody, DataTableHead, @@ -7,16 +7,29 @@ import { DataTableRow, DataTableCell, DataTableColumnHeader, + Button, + spacers, } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; type Props = { headers: Array, linkedEntityData: Array, ...CssClasses, } +const DEFAULT_NUMBER_OF_ROW = 5; + +const styles = { + button: { + marginTop: `${spacers.dp8}`, + }, +}; + +const RelationshipsTablePlain = (props: Props) => { + const { headers, linkedEntityData, classes } = props; + const [displayedRowNumber, setDisplayedRowNumber] = useState(DEFAULT_NUMBER_OF_ROW); -export const RelationshipsTable = (props: Props) => { - const { headers, linkedEntityData } = props; function renderHeader() { const headerCells = headers .map(column => ( @@ -37,7 +50,7 @@ export const RelationshipsTable = (props: Props) => { if (!linkedEntityData) { return null; } - return linkedEntityData.map(({ id: targetId, values }) => ( + return linkedEntityData.slice(0, displayedRowNumber).map(({ id: targetId, values }) => ( {headers.map(({ id }) => { const entity = values.find(item => item.id === id); @@ -49,15 +62,39 @@ export const RelationshipsTable = (props: Props) => { )); }; + const renderShowMoreButton = () => { + const shouldShowMore = linkedEntityData.length > DEFAULT_NUMBER_OF_ROW + && displayedRowNumber < linkedEntityData.length; + return shouldShowMore ? : null; + }; return ( - - - {renderHeader()} - - - {renderRelationshipRows()} - - +
+ + + {renderHeader()} + + + {renderRelationshipRows()} + + + + {renderShowMoreButton()} +
); }; + +export const RelationshipsTable: ComponentType = withStyles(styles)(RelationshipsTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index 6749e11151..d90febd765 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -1,6 +1,7 @@ // @flow -import React, { useState, useCallback } from 'react'; -import { Chip } from '@dhis2/ui'; +import React, { type ComponentType, useState, useCallback } from 'react'; +import { Chip, IconLink24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; import { Widget } from '../../Widget'; import { Relationships } from './Relationships.component'; import type { OutputRelationship } from '../common.types'; @@ -12,7 +13,17 @@ type Props = {| ...CssClasses, |} -export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Props) => { +const styles = { + header: { + display: 'flex', + alignItems: 'center', + }, + icon: { + paddingRight: spacers.dp8, + }, +}; + +const RelationshipsWidgetPlain = ({ relationships, title, classes, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); return ( @@ -20,7 +31,8 @@ export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Pr data-test="relationship-widget" > + header={
+ {title} {relationships && {count} @@ -37,3 +49,5 @@ export const RelationshipsWidget = ({ relationships, title, ...passOnProps }: Pr
); }; + +export const RelationshipsWidget: ComponentType = withStyles(styles)(RelationshipsWidgetPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 88187eb5d2..498847050f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -58,14 +58,14 @@ const determineLinkedEntity = ( from: RelationshipData, to: RelationshipData, ) => { - const { id, toConstraint, fromConstraint } = relationshipType; + const { id, toConstraint, fromConstraint, toFromName, fromToName } = relationshipType; if ((to.trackedEntity && to.trackedEntity.trackedEntity === targetId) || (to.event && to.event.event === targetId)) { - return { side: from, constraint: fromConstraint, groupId: `${id}-from` }; + return { side: from, constraint: fromConstraint, groupId: `${id}-from`, name: toFromName }; } if ((from.trackedEntity && from.trackedEntity.trackedEntity === targetId) || (from.event && from.event.event === targetId)) { - return { side: to, constraint: toConstraint, groupId: `${id}-to` }; + return { side: to, constraint: toConstraint, groupId: `${id}-to`, name: fromToName }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); @@ -125,11 +125,13 @@ const getLinkedEntityInfo = ( if (!metadata) { return undefined; } const { id, values, options } = metadata; const displayFields = getDisplayFields(linkedEntityData.constraint); + return { id, displayFields, groupId: linkedEntityData.groupId, values: convertAttributes(values, displayFields, options), + name: linkedEntityData.name, }; }; @@ -146,17 +148,19 @@ export const useLinkedEntityGroups = ( const linkedEntityGroups = relationships.reduce((acc, rel) => { const { relationshipType: typeId, from, to, createdAt } = rel; const relationshipType = relationshipTypes.find(item => item.id === typeId); + if (!relationshipType) { return acc; } const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); if (!metadata) { return acc; } - const { displayFields, id, values, groupId } = metadata; + const { displayFields, id, values, groupId, name } = metadata; + const typeExist = acc.find(item => item.id === groupId); if (typeExist) { typeExist.linkedEntityData.push({ id, values }); } else { acc.push({ id: groupId, - relationshipName: relationshipType.displayName, + relationshipName: name || relationshipType.displayName, linkedEntityData: [{ id, values }], headers: displayFields, }); From 63bc722ad62e6ed5de7fd74b9583d0a26cf56ea7 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 29 Aug 2022 11:14:02 +0100 Subject: [PATCH 61/95] chore: [DHIS2-12362] minor --- .../RelationshipsComponent/Relationships.component.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js index dde1dcf1ca..db2633a0c4 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js @@ -1,7 +1,7 @@ // @flow import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; -// import i18n from '@dhis2/d2-i18n'; +import i18n from '@dhis2/d2-i18n'; import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; @@ -43,9 +43,9 @@ const RelationshipsPlain = ({ relationships, classes, onAddRelationship }: Props ); }) : null } - {/* */} + ); export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); From 83cbc5269715af112d7b86a5d657f5c0ed373209 Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Mon, 29 Aug 2022 13:30:27 +0100 Subject: [PATCH 62/95] fix: [DHIS2-12362] fix flow --- .../RelationshipsComponent/RelationshipsWidget.component.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index d90febd765..a5cec88ab9 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -44,7 +44,11 @@ const RelationshipsWidgetPlain = ({ relationships, title, classes, ...passOnProp onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - +
); From d184468bb2314514ffa0424d70cf92c2ef36567f Mon Sep 17 00:00:00 2001 From: jasminenguyennn Date: Tue, 30 Aug 2022 11:00:38 +0100 Subject: [PATCH 63/95] chore: [DHIS2-12362] sort most recent event --- .../hooks/useLinkedEntityGroups.js | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 498847050f..45a744eb4c 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; +import moment from 'moment'; import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } from '../../../metaData'; import type { @@ -145,29 +146,31 @@ export const useLinkedEntityGroups = ( const computeData = useCallback(async () => { if (relationships?.length && relationshipTypes?.length) { - const linkedEntityGroups = relationships.reduce((acc, rel) => { - const { relationshipType: typeId, from, to, createdAt } = rel; - const relationshipType = relationshipTypes.find(item => item.id === typeId); - - if (!relationshipType) { return acc; } - const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); - if (!metadata) { return acc; } - const { displayFields, id, values, groupId, name } = metadata; - - const typeExist = acc.find(item => item.id === groupId); - if (typeExist) { - typeExist.linkedEntityData.push({ id, values }); - } else { - acc.push({ - id: groupId, - relationshipName: name || relationshipType.displayName, - linkedEntityData: [{ id, values }], - headers: displayFields, - }); - } - - return acc; - }, []); + const linkedEntityGroups = relationships + .sort((a, b) => moment.utc(b.createdAt).diff(moment.utc(a.createdAt))) + .reduce((acc, rel) => { + const { relationshipType: typeId, from, to, createdAt } = rel; + const relationshipType = relationshipTypes.find(item => item.id === typeId); + + if (!relationshipType) { return acc; } + const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); + if (!metadata) { return acc; } + const { displayFields, id, values, groupId, name } = metadata; + + const typeExist = acc.find(item => item.id === groupId); + if (typeExist) { + typeExist.linkedEntityData.push({ id, values }); + } else { + acc.push({ + id: groupId, + relationshipName: name || relationshipType.displayName, + linkedEntityData: [{ id, values }], + headers: displayFields, + }); + } + + return acc; + }, []); setRelationshipByType(linkedEntityGroups); } From 320259ca86adc6ac1763d93ecc8c96e2858d19a8 Mon Sep 17 00:00:00 2001 From: JasmineNg <89806888+jasminenguyennn@users.noreply.github.com> Date: Tue, 30 Aug 2022 14:17:02 +0100 Subject: [PATCH 64/95] feat: [DHIS2-12372] view linked record (#2780) * feat: [DHIS2-12372] view linked records * chore: [DHIS2-12372] minor * chore: [DHIS2-12372] update type * chore: [DHIS2-12372] hover effect * chore: [DHIS2-12372] update types * chore: [DHIS2-12372] post merge * chore: [DHIS2-12372] post merge * chore: [DHIS2-12372] remove programid on enrollment edit page * fix: [DHIS2-12372] post merge * fix: [DHIS2-12362] post merge * fix: [DHIS2-12372] post merge * fix: [DHIS2-12372] add orgUnitId when navigating --- .../Enrollment/EnrollmentPage.actions.js | 5 +++ .../Pages/Enrollment/EnrollmentPage.epics.js | 36 +++++++++++++++- .../EnrollmentPageDefault.component.js | 2 + .../EnrollmentPageDefault.container.js | 8 +++- .../EnrollmentPageDefault.types.js | 2 + .../EnrollmentEditEventPage.component.js | 3 ++ .../EnrollmentEditEventPage.container.js | 6 ++- .../EnrollmentEditEventPage.types.js | 2 + .../useEventsRelationships.js | 2 +- .../Relationships/Relationships.component.js | 2 +- .../Relationships.component.js | 6 ++- .../RelationshipsTable.component.js | 42 ++++++++++++------- .../RelationshipsWidget.component.js | 2 + .../WidgetEventsRelationships.component.js | 4 +- .../WidgetTeisRelationships.component.js | 4 +- .../WidgetRelationships/common.types.js | 3 +- .../hooks/useLinkedEntityGroups.js | 33 ++++++++++++--- src/epics/trackerCapture.epics.js | 2 + 18 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index 80a878ec58..3f7700b020 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -1,5 +1,6 @@ // @flow import { actionCreator } from '../../../actions/actions.utils'; +import type { Url } from '../../../utils/url'; export const enrollmentPageActionTypes = { INFORMATION_FETCH: 'EnrollmentPage.Fetch', @@ -20,6 +21,7 @@ export const enrollmentPageActionTypes = { UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', SET_EVENT_RELATIONSHIPS_DATA: 'EnrollmentPage.SetEventRelationshipsData', + LINKED_RECORD_CLICK: 'EnrollmentPage.LinkedRecordClick', }; export const fetchEnrollmentPageInformation = () => @@ -65,3 +67,6 @@ export const updateTeiDisplayName = (teiDisplayName: string) => export const setEventRelationshipsData = (eventId: string, relationships: Array) => actionCreator(enrollmentPageActionTypes.SET_EVENT_RELATIONSHIPS_DATA)({ eventId, relationships }); + +export const clickLinkedRecord = (parameters: Url) => + actionCreator(enrollmentPageActionTypes.LINKED_RECORD_CLICK)({ ...parameters }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index 8e268e54a8..a0d41a4744 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -2,7 +2,7 @@ import { ofType } from 'redux-observable'; import { catchError, flatMap, map, startWith } from 'rxjs/operators'; import i18n from '@dhis2/d2-i18n'; -import { from, of } from 'rxjs'; +import { EMPTY, from, of } from 'rxjs'; import moment from 'moment'; import { enrollmentPageActionTypes, @@ -15,6 +15,8 @@ import { } from './EnrollmentPage.actions'; import { buildUrlQueryString, getLocationQuery } from '../../../utils/routing'; import { deriveTeiName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; +import { getProgramFromProgramIdThrowIfNotFound, EventProgram } + from '../../../metaData'; const sortByDate = (enrollments = []) => enrollments.sort((a, b) => moment.utc(b.enrolledAt).diff(moment.utc(a.enrolledAt))); @@ -122,3 +124,35 @@ export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxSto }, ), ); + +export const clickLinkedRecordEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.LINKED_RECORD_CLICK), + flatMap(({ payload }) => { + let url; + const { programId, orgUnitId } = payload; + if (payload.eventId) { + const recordProgram = getProgramFromProgramIdThrowIfNotFound(programId); + if (recordProgram instanceof EventProgram) { + url = `/viewEvent?viewEventId=${payload.eventId}`; + } else { + url = `/enrollmentEventEdit?${buildUrlQueryString({ + orgUnitId, + eventId: payload.eventId, + })}`; + } + } else if (payload.teiId) { + url = `/enrollment?${buildUrlQueryString({ + programId, + orgUnitId, + teiId: payload.teiId, + enrollmentId: 'AUTO', + })}`; + } + if (url) { + history.push(url); + } + return EMPTY; + }, + ), + ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 619a5dfbcf..325c87bc2a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -60,6 +60,7 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, + onLinkedRecordClick, onUpdateTeiAttributeValues, onEnrollmentError, }: PlainProps) => ( @@ -89,6 +90,7 @@ export const EnrollmentPageDefaultPlain = ({ relationshipTypes={relationshipTypes} teiId={teiId} onAddRelationship={() => {}} + onLinkedRecordClick={onLinkedRecordClick} /> {!hideWidgets.indicator && ( { const onEventClick = (eventId: string) => { history.push(`/enrollmentEventEdit?${buildUrlQueryString({ orgUnitId, eventId })}`); }; + + const onLinkedRecordClick = (parameters) => { + dispatch(clickLinkedRecord(parameters)); + }; + const onUpdateTeiAttributeValues = useCallback((updatedAttributeValues, teiDisplayName) => { dispatch(updateEnrollmentAttributeValues(updatedAttributeValues)); dispatch(updateTeiDisplayName(teiDisplayName)); @@ -110,6 +115,7 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} + onLinkedRecordClick={onLinkedRecordClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} onEnrollmentError={onEnrollmentError} /> diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 38fad27af7..f94cbdd562 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,6 +2,7 @@ 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'; +import type { Url } from '../../../../utils/url'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import type { InputRelationship, RelationshipType } from '../../../WidgetRelationships/common.types'; @@ -22,6 +23,7 @@ export type Props = {| onCreateNew: (stageId: string) => void, onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, + onLinkedRecordClick: (parameters: Url) => void, onEnrollmentError: (message: string) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index a0f0039793..377f4fed9b 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -66,6 +66,7 @@ const EnrollmentEditEventPagePain = ({ onAddNew, classes, onGoBack, + onLinkedRecordClick, orgUnitId, eventDate, eventStatus, @@ -122,12 +123,14 @@ const EnrollmentEditEventPagePain = ({ relationships={relationships} relationshipTypes={teiRelationshipTypes} onAddRelationship={() => {}} + onLinkedRecordClick={onLinkedRecordClick} /> {}} + onLinkedRecordClick={onLinkedRecordClick} /> {!hideWidgets.feedback && ( history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); + const onLinkedRecordClick = (parameters) => { + dispatch(clickLinkedRecord(parameters)); + }; const { teiDisplayName } = useTeiDisplayName(teiId, programId); // $FlowFixMe const trackedEntityName = program?.trackedEntityType?.name; @@ -125,6 +128,7 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm onAddNew={onAddNew} orgUnitId={orgUnitId} eventDate={eventDate} + onLinkedRecordClick={onLinkedRecordClick} onEnrollmentError={onEnrollmentError} eventStatus={event?.status} /> diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index c2d6ffb214..68b90ef920 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -2,6 +2,7 @@ import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; import type { InputRelationship, RelationshipType } from '../../WidgetRelationships/common.types'; +import type { Url } from '../../../utils/url'; export type PlainProps = {| programStage: ?ProgramStage, @@ -24,6 +25,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, + onLinkedRecordClick: (parameters: Url) => void, onEnrollmentError: (message: string) => void, pageStatus: string, eventStatus?: string, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js index 7faf1dc483..9409d91bbf 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js @@ -24,7 +24,7 @@ export const useEventsRelationships = (eventId: string) => { resource: 'tracker/relationships', params: ({ variables: { eventId: updatedEventId } }) => ({ event: updatedEventId, - fields: ['relationshipType,to,from'], + fields: ['relationshipType,to,from,createdAt'], }), }, }), diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/Relationships/Relationships.component.js index 121463a764..706356c8f0 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.js @@ -112,7 +112,7 @@ class RelationshipsPlain extends React.Component { return numberOfChanges > 0; } - renderRelationships = () => this.props.relationships.map(r => this.renderRelationship(r)) + renderRelationships = () => this.props.relationships.map(r => r && this.renderRelationship(r)) renderRelationship = (relationship: Relationship) => { const { classes, onRemoveRelationship } = this.props; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js index db2633a0c4..1a1e2542ab 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js @@ -4,10 +4,12 @@ import { withStyles } from '@material-ui/core'; import i18n from '@dhis2/d2-i18n'; import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; +import type { Url } from '../../../utils/url'; type Props = { relationships: Object, onAddRelationship: () => void, + onLinkedRecordClick: (parameters: Url) =>void, ...CssClasses, } @@ -29,7 +31,7 @@ const styles = { overflow: 'scroll', }, }; -const RelationshipsPlain = ({ relationships, classes, onAddRelationship }: Props) => ( +const RelationshipsPlain = ({ relationships, classes, onAddRelationship, onLinkedRecordClick }: Props) => (
{relationshipName}
- +
); }) : null } diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js index 3b723af541..cee2fc09f6 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js @@ -1,5 +1,6 @@ // @flow import React, { useState, type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; import { DataTableBody, DataTableHead, @@ -11,23 +12,30 @@ import { spacers, } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -import { withStyles } from '@material-ui/core'; +import type { Url } from '../../../utils/url'; + type Props = { headers: Array, linkedEntityData: Array, - ...CssClasses, + onLinkedRecordClick: (parameters: Url) => void, + ...CssClasses, } const DEFAULT_NUMBER_OF_ROW = 5; const styles = { + row: { + '&:hover': { + cursor: 'pointer', + }, + }, button: { marginTop: `${spacers.dp8}`, }, }; const RelationshipsTablePlain = (props: Props) => { - const { headers, linkedEntityData, classes } = props; + const { headers, linkedEntityData, classes, onLinkedRecordClick } = props; const [displayedRowNumber, setDisplayedRowNumber] = useState(DEFAULT_NUMBER_OF_ROW); function renderHeader() { @@ -50,17 +58,23 @@ const RelationshipsTablePlain = (props: Props) => { if (!linkedEntityData) { return null; } - return linkedEntityData.slice(0, displayedRowNumber).map(({ id: targetId, values }) => ( - - {headers.map(({ id }) => { - const entity = values.find(item => item.id === id); - return ( - {entity?.value} - - ); - })} - - )); + return linkedEntityData + .slice(0, displayedRowNumber) + .map(({ id: targetId, values, parameters }) => ( + + {headers.map(({ id }) => { + const entity = values.find(item => item.id === id); + return ( onLinkedRecordClick(parameters)} + > + {entity?.value} + + ); + })} + + )); }; const renderShowMoreButton = () => { const shouldShowMore = linkedEntityData.length > DEFAULT_NUMBER_OF_ROW diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js index a5cec88ab9..2c2e2bddb5 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js @@ -5,11 +5,13 @@ import { withStyles } from '@material-ui/core'; import { Widget } from '../../Widget'; import { Relationships } from './Relationships.component'; import type { OutputRelationship } from '../common.types'; +import type { Url } from '../../../utils/url'; type Props = {| relationships: Array, title: string, onAddRelationship: () => void, + onLinkedRecordClick: (parameters: Url) => void, ...CssClasses, |} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js index 1a65780884..c520e3182e 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js @@ -5,14 +5,14 @@ import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; import type { Props } from './types'; -export const WidgetEventsRelationships = ({ eventId, relationships, relationshipTypes, onAddRelationship }: Props) => { +export const WidgetEventsRelationships = ({ eventId, relationships, relationshipTypes, ...passOnProps }: Props) => { const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipTypes, relationships); return ( ); }; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js index 4b771f677f..305a07140f 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js @@ -5,14 +5,14 @@ import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; import { RelationshipsWidget } from '../RelationshipsComponent'; import type { Props } from './types'; -export const WidgetTeisRelationships = ({ relationships, relationshipTypes, teiId, onAddRelationship }: Props) => { +export const WidgetTeisRelationships = ({ relationships, relationshipTypes, teiId, ...passOnProps }: Props) => { const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); return ( ); }; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/common.types.js b/src/core_modules/capture-core/components/WidgetRelationships/common.types.js index 4204b4a89c..66cd80b50d 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/common.types.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/common.types.js @@ -13,7 +13,8 @@ export type TEIRelationshipData = {| trackedEntity: { trackedEntityType: string, trackedEntity: string, - attributes: Array + attributes: Array, + orgUnit: string } |} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js index 45a744eb4c..4e5916dde5 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js @@ -74,7 +74,23 @@ const determineLinkedEntity = ( }; -const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, createdAt: string) => { +const getLinkedRecordURLParameters = (linkedEntity: RelationshipData, relationshipType: Object) => { + if (linkedEntity.event) { + const { + event: eventId, + program: programId, + orgUnit: orgUnitId, + } = linkedEntity.event; + return { eventId, programId, orgUnitId }; + } else if (linkedEntity.trackedEntity) { + const programId = relationshipType.program?.id; + const { trackedEntity: teiId, orgUnit: orgUnitId } = linkedEntity.trackedEntity; + return { programId, orgUnitId, teiId }; + } + return {}; +}; + +const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, relationshipType: Object, createdAt: string) => { if (linkedEntity.event) { const { event: eventId, program: programId, programStage, orgUnitName, status } = linkedEntity.event; /* @@ -92,6 +108,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, createdAt return { id: eventId, values: linkedEntity.event.dataValues, + parameters: getLinkedRecordURLParameters(linkedEntity, relationshipType), options: { orgUnitName, status, @@ -106,6 +123,7 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, createdAt return { id: trackedEntity, values: attributes, + parameters: getLinkedRecordURLParameters(linkedEntity, relationshipType), options: { trackedEntityTypeName: tet.name, created: createdAt }, }; } @@ -122,14 +140,17 @@ const getLinkedEntityInfo = ( ) => { const linkedEntityData = determineLinkedEntity(relationshipType, targetId, from, to); if (!linkedEntityData) { return undefined; } - const metadata = getAttributeConstraintsForTEI(linkedEntityData.side, createdAt); + + const metadata = getAttributeConstraintsForTEI(linkedEntityData.side, linkedEntityData.constraint, createdAt); if (!metadata) { return undefined; } - const { id, values, options } = metadata; + + const { id, values, options, parameters } = metadata; const displayFields = getDisplayFields(linkedEntityData.constraint); return { id, displayFields, + parameters, groupId: linkedEntityData.groupId, values: convertAttributes(values, displayFields, options), name: linkedEntityData.name, @@ -155,16 +176,16 @@ export const useLinkedEntityGroups = ( if (!relationshipType) { return acc; } const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); if (!metadata) { return acc; } - const { displayFields, id, values, groupId, name } = metadata; + const { displayFields, id, values, parameters, groupId, name } = metadata; const typeExist = acc.find(item => item.id === groupId); if (typeExist) { - typeExist.linkedEntityData.push({ id, values }); + typeExist.linkedEntityData.push({ id, values, parameters }); } else { acc.push({ id: groupId, relationshipName: name || relationshipType.displayName, - linkedEntityData: [{ id, values }], + linkedEntityData: [{ id, values, parameters }], headers: displayFields, }); } diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index 09eb246c3b..184b7b0c2e 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -184,6 +184,7 @@ import { startFetchingTeiFromEnrollmentIdEpic, startFetchingTeiFromTeiIdEpic, openEnrollmentPageEpic, + clickLinkedRecordEpic, } from '../core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics'; import { saveNewEventSucceededEpic, @@ -322,6 +323,7 @@ export const epics = combineEpics( updateTeiEpic, updateTeiSucceededEpic, updateTeiFailedEpic, + clickLinkedRecordEpic, navigateToNewUserPageEpic, requestDeleteEventDataEntryEpic, ); From 2c91cc6464f97c8bb2a2be55038a102fff94a004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Thu, 8 Dec 2022 14:13:23 +0100 Subject: [PATCH 65/95] chore: useMetadataQuery utility method --- flow-typed/npm/react-query_v3.x.x.js | 1299 +++++++++++++++++ package.json | 1 + .../utils/reactQueryHelpers/index.js | 2 + .../utils/reactQueryHelpers/query/index.js | 2 + .../query/useMetadataQuery.js | 29 + .../query/useMetadataQuery.types.js | 11 + yarn.lock | 8 +- 7 files changed, 1348 insertions(+), 4 deletions(-) create mode 100644 flow-typed/npm/react-query_v3.x.x.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/index.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js diff --git a/flow-typed/npm/react-query_v3.x.x.js b/flow-typed/npm/react-query_v3.x.x.js new file mode 100644 index 0000000000..4c6bd13ad0 --- /dev/null +++ b/flow-typed/npm/react-query_v3.x.x.js @@ -0,0 +1,1299 @@ +// flow-typed signature: fa0e8e361269bd4b07d94a58120cf9ad +// flow-typed version: 2d06cbbb04/react-query_v3.x.x/flow_>=v0.104.x + +// @flow + +declare module "react-query" { + declare export class Subscribable { + subscribe(listener?: TListener): () => void; + hasListeners(): boolean; + } + declare export type QueryCacheListener = (query?: Query) => void; + + declare export class QueryCache extends Subscribable { + build( + client: QueryClient, + options: QueryOptions, + state?: QueryState + ): Query; + add(query: Query): void; + remove(query: Query): void; + clear(): void; + get( + queryHash: string + ): ?Query; + getAll(): Query[]; + find( + queryKey: QueryKey, + filters?: QueryFilters + ): ?Query; + findAll( + queryKey?: QueryKey, + filters?: QueryFilters + ): Query[]; + findAll( + filters?: QueryFilters + ): Query[]; + notify( + query?: Query + ): void; + onFocus(): void; + onOnline(): void; + } + + declare export type QueryFilters = {| + /** + * Include or exclude active queries + */ + active?: boolean, + /** + * Match query key exactly + */ + exact?: boolean, + /** + * Include or exclude inactive queries + */ + inactive?: boolean, + /** + * Include queries matching this predicate function + */ + predicate?: ( + query: Query + ) => boolean, + /** + * Include queries matching this query key + */ + queryKey?: QueryKey, + /** + * Include or exclude stale queries + */ + stale?: boolean, + /** + * Include or exclude fetching queries + */ + fetching?: boolean, + |}; + declare export type InvalidateQueryFilters = {| + ...QueryFilters, + refetchActive?: boolean, + refetchInactive?: boolean, + |}; + + declare export type QueryClientConfig = {| + queryCache?: QueryCache, + mutationCache?: MutationCache, + defaultOptions?: DefaultOptions, + |}; + + declare export class QueryClient { + constructor(config?: QueryClientConfig): this; + mount(): void; + unmount(): void; + + isFetching(filters?: QueryFilters): number; + isFetching(queryKey?: QueryKey, filters?: QueryFilters): number; + + getQueryData(queryKey: QueryKey, filters?: QueryFilters): void | TData; + setQueryData( + queryKey: QueryKey, + updater: Updater, + options?: SetDataOptions + ): TData; + getQueryState( + queryKey: QueryKey, + filters?: QueryFilters + ): ?QueryState; + + removeQueries(filters?: QueryFilters): void; + removeQueries(queryKey?: QueryKey, filters?: QueryFilters): void; + + cancelQueries( + filters?: QueryFilters, + options?: CancelOptions + ): Promise; + cancelQueries( + queryKey?: QueryKey, + filters?: QueryFilters, + options?: CancelOptions + ): Promise; + + invalidateQueries( + filters?: InvalidateQueryFilters, + options?: InvalidateOptions + ): Promise; + invalidateQueries( + queryKey: QueryKey, + filters?: InvalidateQueryFilters, + options?: InvalidateOptions + ): Promise; + + refetchQueries( + filters?: QueryFilters, + options?: RefetchOptions + ): Promise; + refetchQueries( + queryKey: QueryKey, + filters?: QueryFilters, + options?: RefetchOptions + ): Promise; + + fetchQuery( + options: FetchQueryOptions + ): Promise; + fetchQuery( + queryKey: QueryKey, + options?: FetchQueryOptions + ): Promise; + fetchQuery( + queryKey: QueryKey, + queryFn: QueryFunction, + options?: FetchQueryOptions + ): Promise; + + prefetchQuery( + options: FetchQueryOptions + ): Promise; + prefetchQuery( + queryKey: QueryKey, + options?: FetchQueryOptions + ): Promise; + prefetchQuery( + queryKey: QueryKey, + queryFn: QueryFunction, + options?: FetchQueryOptions + ): Promise; + + cancelMutations(): Promise; + resumePausedMutations(): Promise; + executeMutation( + options: MutationOptions + ): Promise; + + getQueryCache(): QueryCache; + getMutationCache(): MutationCache; + getDefaultOptions(): DefaultOptions; + setDefaultOptions(options: DefaultOptions): void; + setQueryDefaults( + queryKey: QueryKey, + options: QueryObserverOptions + ): void; + getQueryDefaults( + queryKey?: QueryKey + ): ?QueryObserverOptions; + setMutationDefaults( + mutationKey: MutationKey, + options: MutationObserverOptions + ): void; + getMutationDefaults( + mutationKey?: MutationKey + ): ?MutationObserverOptions; + defaultQueryOptions>(options?: T): T; + defaultQueryObserverOptions>( + options?: T + ): T; + defaultMutationOptions>( + options?: T + ): T; + clear(): void; + } + + declare export type CancelOptions = {| + revert?: boolean, + silent?: boolean, + |}; + declare export type ResultOptions = {| + throwOnError?: boolean, + cancelRefetch?: boolean, + |}; + declare export type RefetchOptions = ResultOptions; + declare export type InvalidateOptions = ResultOptions; + declare export type FetchNextPageOptions = {| + ...ResultOptions, + pageParam?: T, + |}; + declare export type FetchPreviousPageOptions = FetchNextPageOptions; + + // retry related + declare export type RetryValue = + | boolean + | number + | ShouldRetryFunction; + declare export type ShouldRetryFunction = ( + failureCount: number, + error: TError + ) => boolean; + declare export type RetryDelayValue = + | number + | RetryDelayFunction; + declare export type RetryDelayFunction = ( + failureCount: number, + error: TError + ) => number; + + // updater + declare export type DataUpdateFunction = (input: TInput) => TOutput; + declare export type Updater = + | TOutput + | DataUpdateFunction; + + // Action + declare export type FailedAction = {| + type: "failed", + |}; + + declare export type FetchAction = {| + type: "fetch", + meta?: mixed, + |}; + + declare export type SuccessAction = {| + data: void | TData, + type: "success", + dataUpdatedAt?: number, + |}; + + declare export type ErrorAction = {| + type: "error", + error: TError, + |}; + + declare export type InvalidateAction = {| + type: "invalidate", + |}; + + declare export type PauseAction = {| + type: "pause", + |}; + + declare export type ContinueAction = {| + type: "continue", + |}; + + declare export type SetStateAction = {| + type: "setState", + state: MutationState, + |}; + + declare export type SetQueryStateAction = {| + type: "setState", + state: QueryState, + |}; + + declare export type Action = + | ContinueAction + | ErrorAction + | FailedAction + | LoadingAction + | PauseAction + | SetStateAction + | SuccessAction; + + declare export type QueryAction = + | ContinueAction + | ErrorAction + | FailedAction + | FetchAction + | InvalidateAction + | PauseAction + | SetQueryStateAction + | SuccessAction; + + // query types + declare export type FetchOptions = {| + cancelRefresh?: boolean, + meta?: mixed, + |}; + declare export type FetchContext = {| + fetchFn: () => mixed | Promise, + fetchOptions?: FetchOptions, + options: QueryOptions, + queryKey: QueryKey, + state: QueryState, + |}; + declare export type QueryBehavior = {| + onFetch: (context: FetchContext) => void, + |}; + declare export type QueryConfig = {| + cache: QueryCache, + queryKey: QueryKey, + queryHash: string, + options?: QueryOptions, + defaultOptions?: QueryOptions, + state?: QueryState, + |}; + declare export type EnumStatus = "idle" | "loading" | "success" | "error"; + declare export type QueryStatus = EnumStatus; + declare export type QueryState = {| + data: void | TData, + dataUpdateCount: number, + dataUpdatedAt: number, + error: TError | null, + errorUpdateCount: number, + errorUpdatedAt: number, + fetchFailureCount: number, + fetchMeta: mixed, + isFetching: boolean, + isInvalidated: boolean, + isPaused: boolean, + status: QueryStatus, + |}; + + declare export type InitialDataFunction = () => void | TData; + declare export type QueryKey = string | mixed[] | $ReadOnlyArray; + declare export type PageParam = mixed; + declare export type QueryKeyHashFunction = (queryKey: TQueryKey) => string; + declare export type QueryFunctionContext = {| + queryKey: TQueryKey, + pageParam?: TPageParam, + |}; + declare export type QueryFunction = (context: QueryFunctionContext) => Promise | TQueryFnData; + declare export type GetPreviousPageParamFunction = ( + firstPage: TQueryFnData, + allPages: TQueryFnData[] + ) => U; + declare export type GetNextPageParamFunction = ( + lastPage: TQueryFnData, + allPages: TQueryFnData[] + ) => U; + declare export type QueryOptions = {| + retry?: RetryValue, + retryDelay?: RetryDelayValue, + cacheTime?: number, + isDataEqual?: (oldData: void | TData, newData: TData) => boolean, + queryFn?: QueryFunction, + queryHash?: string, + queryKey?: TQueryKey, + queryKeyHashFn?: QueryKeyHashFunction, + initialData?: TData | InitialDataFunction, + behavior?: QueryBehavior, + /** + * Set this to `false` to disable structural sharing between query results. + * Defaults to `true`. + */ + structuralSharing?: boolean, + /** + * This function can be set to automatically get the previous cursor for infinite queries. + * The result will also be used to determine the value of `hasPreviousPage`. + */ + getPreviousPageParam?: GetPreviousPageParamFunction, + /** + * This function can be set to automatically get the next cursor for infinite queries. + * The result will also be used to determine the value of `hasNextPage`. + */ + getNextPageParam?: GetNextPageParamFunction, + |}; + declare export type FetchQueryOptions = {| + ...QueryOptions, + staleTime?: number, + |}; + declare export type PlaceholderDataFunction = () => void | TResult; + + declare export type QueryObserverOptions< + TQueryFnData, + TError = mixed, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey = QueryKey, + > = {| + ...QueryOptions, + /** + * Set this to `false` to disable automatic refetching when the query mounts or changes query keys. + * To refetch the query, use the `refetch` method returned from the `useQuery` instance. + * Defaults to `true`. + */ + enabled?: boolean, + /** + * The time in milliseconds after data is considered stale. + * If set to `Infinity`, the data will never be considered stale. + */ + staleTime?: number, + /** + * If set to a number, the query will continuously refetch at this frequency in milliseconds. + * Defaults to `false`. + */ + refetchInterval?: number | false, + /** + * If set to `true`, the query will continue to refetch while their tab/window is in the background. + * Defaults to `false`. + */ + refetchIntervalInBackground?: boolean, + /** + * If set to `true`, the query will refetch on window focus if the data is stale. + * If set to `false`, the query will not refetch on window focus. + * If set to `'always'`, the query will always refetch on window focus. + * Defaults to `true`. + */ + refetchOnWindowFocus?: boolean | "always", + /** + * If set to `true`, the query will refetch on reconnect if the data is stale. + * If set to `false`, the query will not refetch on reconnect. + * If set to `'always'`, the query will always refetch on reconnect. + * Defaults to `true`. + */ + refetchOnReconnect?: boolean | "always", + /** + * If set to `true`, the query will refetch on mount if the data is stale. + * If set to `false`, will disable additional instances of a query to trigger background refetches. + * If set to `'always'`, the query will always refetch on mount. + * Defaults to `true`. + */ + refetchOnMount?: boolean | "always", + /** + * If set to `false`, the query will not be retried on mount if it contains an error. + * Defaults to `true`. + */ + retryOnMount?: boolean, + /** + * If set, the component will only re-render if any of the listed properties change. + * When set to `['data', 'error']`, the component will only re-render when the `data` or `error` properties change. + */ + notifyOnChangeProps?: string[] | "tracked", + /** + * If set, the component will not re-render if any of the listed properties change. + */ + notifyOnChangePropsExclusions?: string[], + /** + * This callback will fire any time the query successfully fetches new data. + */ + onSuccess?: (data: TData) => void, + /** + * This callback will fire if the query encounters an error and will be passed the error. + */ + onError?: (err: TError) => void, + /** + * This callback will fire any time the query is either successfully fetched or errors and be passed either the data or error. + */ + onSettled?: (data: void | TData, error: TError | null) => void, + /** + * Whether errors should be thrown instead of setting the `error` property. + * Defaults to `false`. + */ + useErrorBoundary?: boolean, + /** + * This option can be used to transform or select a part of the data returned by the query function. + */ + select?: (data: TQueryData) => TData, + /** + * If set to `true`, the query will suspend when `status === 'loading'` + * and throw errors when `status === 'error'`. + * Defaults to `false`. + */ + suspense?: boolean, + /** + * Set this to `true` to keep the previous `data` when fetching based on a new query key. + * Defaults to `false`. + */ + keepPreviousData?: boolean, + /** + * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. + */ + placeholderData?: TData | PlaceholderDataFunction, + |}; + + declare export type InfiniteQueryObserverOptions = + QueryObserverOptions< + TQueryFnData, + TError, + InfiniteData, + InfiniteData, + TQueryKey, + >; + + declare export type QueryObserverBaseResult = {| + data: void | TData, + dataUpdatedAt: number, + error: TError | null, + errorUpdatedAt: number, + failureCount: number, + isError: boolean, + isFetched: boolean, + isFetchedAfterMount: boolean, + isFetching: boolean, + isIdle: boolean, + isLoading: boolean, + isLoadingError: boolean, + isPlaceholderData: boolean, + isPreviousData: boolean, + isRefetchError: boolean, + isStale: boolean, + isSuccess: boolean, + refetch: ( + options?: RefetchOptions + ) => Promise>, + remove: () => void, + status: QueryStatus, + |}; + + declare export type QueryObserverIdleResult = {| + ...QueryObserverBaseResult, + error: null, + isError: false, + isIdle: true, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "idle", + |}; + + declare export type QueryObserverLoadingResult = {| + ...QueryObserverBaseResult, + error: null, + isError: false, + isIdle: false, + isLoading: true, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "loading", + |}; + + declare export type QueryObserverLoadingErrorResult = {| + ...QueryObserverBaseResult, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: true, + isRefetchError: false, + isSuccess: false, + status: "error", + |}; + + declare export type QueryObserverRefetchErrorResult = {| + ...QueryObserverBaseResult, + data: TData, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: true, + isSuccess: false, + status: "error", + |}; + + declare export type QueryObserverSuccessResult = {| + ...QueryObserverBaseResult, + data: TData, + error: null, + isError: false, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: "success", + |}; + + // exported for testing + declare export type QueryObserverResult = + | QueryObserverIdleResult + | QueryObserverLoadingErrorResult + | QueryObserverLoadingResult + | QueryObserverRefetchErrorResult + | QueryObserverSuccessResult; + + declare export type InfiniteData = {| + pages: TData[], + pageParams: mixed[], + |}; + + declare export type InfiniteQueryObserverBaseResult = {| + ...QueryObserverBaseResult, TError>, + fetchNextPage: ( + options?: FetchNextPageOptions + ) => Promise>, + fetchPreviousPage: ( + options?: FetchPreviousPageOptions + ) => Promise>, + hasNextPage?: boolean, + hasPreviousPage?: boolean, + isFetchingNextPage: boolean, + isFetchingPreviousPage: boolean, + |}; + + declare export type InfiniteQueryObserverIdleResult = {| + ...InfiniteQueryObserverBaseResult, + error: null, + isError: false, + isIdle: true, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "idle", + |}; + + declare export type InfiniteQueryObserverLoadingResult = {| + ...InfiniteQueryObserverBaseResult, + error: null, + isError: false, + isIdle: false, + isLoading: true, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "loading", + |}; + + declare export type InfiniteQueryObserverLoadingErrorResult = {| + ...InfiniteQueryObserverBaseResult, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: true, + isRefetchError: false, + isSuccess: false, + status: "error", + |}; + + declare export type InfiniteQueryObserverRefetchErrorResult = {| + ...InfiniteQueryObserverBaseResult, + data: InfiniteData, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: true, + isSuccess: false, + status: "error", + |}; + + declare export type InfiniteQueryObserverSuccessResult = {| + ...InfiniteQueryObserverBaseResult, + data: InfiniteData, + error: null, + isError: false, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: "success", + |}; + + declare export type InfiniteQueryObserverResult = + | InfiniteQueryObserverIdleResult + | InfiniteQueryObserverLoadingErrorResult + | InfiniteQueryObserverLoadingResult + | InfiniteQueryObserverRefetchErrorResult + | InfiniteQueryObserverSuccessResult; + + declare export type QueryObserverListener = ( + result: QueryObserverResult + ) => void; + + declare export type ObserverFetchOptions = {| + ...FetchOptions, + throwOnError?: boolean, + |}; + + declare export class QueryObserver< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + TPageParam = PageParam, + > + extends Subscribable> { + +options: QueryObserverOptions; + + constructor( + client: QueryClient, + options: QueryObserverOptions + ): this; + + willLoadOnMount(): boolean; + willRefetchOnMount(): boolean; + willFetchOnMount(): boolean; + willFetchOnReconnect(): boolean; + willFetchOnWindowFocus(): boolean; + destroy(): void; + setOptions( + options?: QueryObserverOptions + ): void; + getCurrentResult(): QueryObserverResult; + getNextResult( + options?: ResultOptions + ): Promise>; + getCurrentQuery(): Query; + remove(): void; + refetch( + options?: RefetchOptions + ): Promise>; + onQueryUpdate(action: Action): void; + } + + declare export type QueriesObserverListener = ( + result: $ReadOnlyArray> + ) => void; + declare export class QueriesObserver< + TQueryFnData, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > + extends Subscribable> { + constructor( + client: QueryClient, + queries?: QueryObserverOptions[] + ): this; + destroy(): void; + setQueries( + queries: QueryObserverOptions[] + ): void; + getCurrentResult(): QueryObserverResult[]; + } + + declare export type InfiniteQueryObserverListener = ( + result: InfiniteQueryObserverResult + ) => void; + + declare export class InfiniteQueryObserver< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > + extends Subscribable> { + constructor( + client: QueryClient, + options: InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + > + ): this; + + // Type override + subscribe( + listener?: InfiniteQueryObserverListener + ): () => void; + + // Type override + getCurrentResult(): InfiniteQueryObserverResult; + + willLoadOnMount(): boolean; + willRefetchOnMount(): boolean; + willFetchOnMount(): boolean; + willFetchOnReconnect(): boolean; + willFetchOnWindowFocus(): boolean; + destroy(): void; + getCurrentResult(): InfiniteQueryObserverResult; + getNextResult( + options?: ResultOptions + ): Promise>; + getCurrentQuery(): Query; + remove(): void; + refetch( + options?: RefetchOptions + ): Promise>; + onQueryUpdate(action: Action): void; + + //////////////////// + + setOptions( + options?: InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + > + ): void; + fetchNextPage( + options?: FetchNextPageOptions + ): Promise>; + fetchPreviousPage( + options?: FetchPreviousPageOptions + ): Promise>; + } + + declare export type SetDataOptions = {| updatedAt?: number |}; + + declare export class Query { + +queryKey: QueryKey; + +queryHash: string; + +options: QueryOptions; + +state: QueryState; + +cacheTime: number; + + constructor(config: QueryConfig): this; + setDefaultOptions(options: QueryOptions): void; + setData(updater: Updater, options?: SetDataOptions): TData; + setState(state: QueryState): void; + cancel(options?: CancelOptions): Promise; + destroy(): void; + isActive(): boolean; + isFetching(): boolean; + isStale(): boolean; + isStaleByTime(staleTime?: number): boolean; + onFocus(): void; + onOnline(): void; + addObserver(observer: QueryObserver): void; + removeObserver(observer: QueryObserver): void; + invalidate(): void; + fetch( + options?: QueryOptions, + fetchOptions?: FetchOptions + ): Promise; + } + + /** + * + * Mutations + * + */ + + declare export type MutationConfig = {| + mutationId: number, + mutationCache: MutationCache, + options: MutationOptions, + defaultOptions?: MutationOptions, + state?: MutationState, + |}; + + declare export type MutationKey = QueryKey; + declare export type MutationStatus = EnumStatus; + + declare export type MutationFunction = ( + variables: TVariables + ) => Promise; + + declare export type MutationOptions = {| + mutationFn?: MutationFunction, + mutationKey?: MutationKey, + variables?: TVariables, + onMutate?: (variables: TVariables) => Promise | TContext, + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: TContext + ) => Promise | void, + retry?: RetryValue, + retryDelay?: RetryDelayValue, + |}; + + declare export type MutationObserverOptions< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + ...MutationOptions, + useErrorBoundary?: boolean, + |}; + + declare export type MutateOptions = {| + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: void | TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: void | TContext + ) => Promise | void, + |}; + + declare export type MutateFunction = ( + variables: TVariables, + options?: MutateOptions + ) => Promise; + + declare export type MutationObserverResult< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + ...MutationState, + isError: boolean, + isIdle: boolean, + isLoading: boolean, + isSuccess: boolean, + mutate: MutateFunction, + reset: () => void, + |}; + + declare export type DefaultOptions = {| + queries?: QueryObserverOptions, + mutations?: MutationObserverOptions, + |}; + declare export type MutationState = {| + context: void | TContext, + data: void | TData, + error: TError | null, + failureCount: number, + isPaused: boolean, + status: MutationStatus, + variables: void | TVariables, + |}; + + declare export type LoadingAction = {| + type: "loading", + variables?: TVariables, + context?: TContext, + |}; + + declare export type SetMutationStateAction< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + type: "setState", + state: MutationState, + |}; + + declare export type MutationAction = + | ContinueAction + | ErrorAction + | FailedAction + | LoadingAction + | PauseAction + | SetMutationStateAction + | SuccessAction; + + declare export class Mutation { + +state: MutationState; + +options: MutationOptions; + +mutationId: number; + constructor( + config: MutationConfig + ): this; + setState(state: MutationState): void; + addObserver(observer: MutationObserver): void; + removeObserver(observer: MutationObserver): void; + cancel(): Promise; + continue(): Promise; + execute(): Promise; + } + + declare export type MutationObserverListener< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = ( + result: MutationObserverResult + ) => void; + declare export class MutationObserver< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > + extends + Subscribable< + MutationObserverListener + > { + +options: MutationObserverOptions; + constructor( + client: QueryClient, + options: MutationObserverOptions + ): this; + setOptions( + options?: MutationObserverOptions + ): void; + onMutationUpdate(action: Action): void; + getCurrentResult(): MutationObserverResult< + TData, + TError, + TVariables, + TContext + >; + reset(): void; + mutate( + variables?: TVariables, + options?: MutateOptions + ): Promise; + } + + declare export type MutationCacheListener = ( + mutation?: Mutation + ) => void; + declare export class MutationCache + extends Subscribable { + build( + client: QueryClient, + options: MutationOptions, + state?: MutationState + ): Mutation; + add(mutation: Mutation): void; + remove(mutation: Mutation): void; + clear(): void; + getAll(): Mutation[]; + notify(mutation?: Mutation): void; + onFocus(): void; + onOnline(): void; + resumePausedMutations(): Promise; + } + + declare export type UseBaseQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = QueryObserverOptions; + + declare export type UseQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = UseBaseQueryOptions; + + declare export type UseInfiniteQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = InfiniteQueryObserverOptions; + + declare export type UseBaseQueryResult = QueryObserverResult< + TData, + TError>; + + declare export type UseQueryResult = UseBaseQueryResult< + TData, + TError>; + + declare export type UseInfiniteQueryResult< + TData, + TError + > = InfiniteQueryObserverResult; + + declare export type UseMutationOptions< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + mutationKey?: MutationKey, + onMutate?: (variables: TVariables) => Promise | TContext, + onSuccess?: ( + data: TData, + variables: TVariables, + context: ?TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: ?TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: ?TContext + ) => Promise | void, + retry?: RetryValue, + retryDelay?: RetryDelayValue, + useErrorBoundary?: boolean, + |}; + + declare export type UseMutateFunction = ( + variables: TVariables, + options?: MutateOptions + ) => void; + + declare export type UseMutateAsyncFunction< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = ( + variables: TVariables, + options?: MutateOptions + ) => Promise; + + declare export type UseMutationResult< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + context: ?TContext, + data: void | TData, + error: TError | null, + failureCount: number, + isError: boolean, + isIdle: boolean, + isLoading: boolean, + isPaused: boolean, + isSuccess: boolean, + mutate: UseMutateFunction, + mutateAsync: UseMutateAsyncFunction, + reset: () => void, + status: MutationStatus, + variables: ?TVariables, + |}; + + declare export function useQuery( + options: UseQueryOptions + ): UseQueryResult; + declare export function useQuery( + queryKey: TQueryKey, + options?: UseQueryOptions + ): UseQueryResult; + declare export function useQuery( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseQueryOptions + ): UseQueryResult; + + declare export function useQueries( + queries: UseQueryOptions[] + ): UseQueryResult[]; + + declare export function useInfiniteQuery(options: UseInfiniteQueryOptions): UseInfiniteQueryResult; + declare export function useInfiniteQuery( + queryKey: TQueryKey, + options?: UseInfiniteQueryOptions + ): UseInfiniteQueryResult; + declare export function useInfiniteQuery< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey + >( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseInfiniteQueryOptions + ): UseInfiniteQueryResult; + + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + options: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationFn: MutationFunction, + options?: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationKey: MutationKey, + options?: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationKey: MutationKey, + mutationFn?: MutationFunction, + options?: UseMutationOptions + ): UseMutationResult; + + declare export function useIsFetching(filters?: QueryFilters): number; + declare export function useIsFetching( + queryKey?: QueryKey, + filters?: QueryFilters + ): number; + + declare export function useQueryClient(): QueryClient; + declare export class QueryClientProvider + extends + React$Component<{| + client: QueryClient, + children?: React$Node, + |}> {} + + declare export type QueryErrorResetBoundaryValue = {| + clearReset: () => void, + isReset: () => boolean, + reset: () => void, + |}; + declare export function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue; + declare export class QueryErrorResetBoundary + extends + React$Component<{| + children: + | React$Node + | ((value: QueryErrorResetBoundaryValue) => React$Node), + |}> {} + declare export function setLogger(logger: {| + log: (...args: mixed[]) => void, + warn: (...args: mixed[]) => void, + error: (...args: mixed[]) => void, + |}): void; + + declare export type NotifyCallback = () => void; + declare export type NotifyFunction = (callback: () => void) => void; + declare export type BatchNotifyFunction = (callback: () => void) => void; + + declare export class NotifyManager { + batch(callback: () => T): T; + schedule(callback: NotifyCallback): void; + batchCalls(callback: T): T; + flush(): void; + + /** + * Use this method to set a custom notify function. + * This can be used to for example wrap notifications with `React.act` while running tests. + */ + setNotifyFunction(fn: NotifyFunction): void; + + /** + * Use this method to set a custom function to batch notifications together into a single tick. + * By default React Query will use the batch function provided by ReactDOM or React Native. + */ + setBatchNotifyFunction(fn: BatchNotifyFunction): void; + } + declare export var notifyManager: NotifyManager; + declare export var focusManager: {| + setEventListener( + setup: (onFocus: () => void) => (focused?: boolean) => void + ): void, + setFocused(focused?: boolean): void, + isFocused: boolean, + |}; + declare export var onlineManager: {| + setEventListener( + setup: (setOnline: () => void) => (online?: boolean) => void + ): void, + setOnline(online?: boolean): void, + isOnline: boolean, + |}; + declare export function hashQueryKey(queryKey: QueryKey): string; + declare export function isError(value: any): boolean; + declare export function isCancelledError(value: any): boolean; +} diff --git a/package.json b/package.json index 42073c0d0d..6dbdccc6fa 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-leaflet-draw": "0.19.0", "react-leaflet-search-unpolyfilled": "^0.1.0", "react-popper": "^2.2.4", + "react-query": "3", "react-redux": "^7.2.8", "react-router-dom": "^5.3.0", "react-rte": "^0.16.5", diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js new file mode 100644 index 0000000000..872a70d2c0 --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js @@ -0,0 +1,2 @@ +// @flow +export { useMetadataCustomQuery } from './query'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js new file mode 100644 index 0000000000..3cb5e70b4d --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js @@ -0,0 +1,2 @@ +// @flow +export { useMetadataCustomQuery } from './useMetadataQuery'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js new file mode 100644 index 0000000000..20799983f5 --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -0,0 +1,29 @@ +// @flow +import { useQuery } from 'react-query'; +import type { QueryKey, QueryFunction } from 'react-query'; +import type { CustomOptions, Result } from './useMetadataQuery.types'; + +const useAsyncMetadata = ( + queryKey: QueryKey, + queryFn: QueryFunction, + { cacheTime, enabled }): Result => { + const { data, isLoading, isError } = useQuery(queryKey, queryFn, { + enabled, + cacheTime, + staleTime: Infinity, + }); + + return { + data, + loading: isLoading, + failed: isError, + }; +}; + +export const useMetadataCustomQuery = ( + queryKey: QueryKey, + queryFn: QueryFunction, { + cacheTime = 0, + enabled = true, + }: CustomOptions = {}): Result => + useAsyncMetadata(queryKey, queryFn, { cacheTime, enabled }); diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js new file mode 100644 index 0000000000..6b8adaac81 --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -0,0 +1,11 @@ +// @flow +export type CustomOptions = $ReadOnly<{| + useCache?: boolean, + enabled?: boolean, +|}>; + +export type Result = $ReadOnly<{| + data?: TResultData, + loading: boolean, + failed?: boolean, +|}>; diff --git a/yarn.lock b/yarn.lock index 5b1e428c25..95cb7c33e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15260,10 +15260,10 @@ react-popper@^2.2.4, react-popper@^2.2.5: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.13.11: - version "3.25.1" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.25.1.tgz#221ff17406518a7689378dcbbdc986b0ba2c3919" - integrity sha512-tGZkap921d9dJD2F8+NpEu3djLRP+tpZKHKhQvqUMYMfWT5R18iRtGAG5ZeUMlRKuhzNaZx3cHiYj3DsyZ1SWw== +react-query@3, react-query@^3.13.11: + version "3.39.2" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.2.tgz#9224140f0296f01e9664b78ed6e4f69a0cc9216f" + integrity sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" From a87bb7639327f34d20c3716c4edca13a1066fd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Thu, 8 Dec 2022 15:04:55 +0100 Subject: [PATCH 66/95] chore: useIndexedDB utility method --- .../capture-core/utils/reactQueryHelpers/index.js | 2 +- .../capture-core/utils/reactQueryHelpers/query/index.js | 2 +- .../utils/reactQueryHelpers/query/useMetadataQuery.js | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js index 872a70d2c0..ad3d2857ad 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js @@ -1,2 +1,2 @@ // @flow -export { useMetadataCustomQuery } from './query'; +export { useMetadataCustomQuery, useIndexedDBQuery } from './query'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js index 3cb5e70b4d..61d232b976 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js @@ -1,2 +1,2 @@ // @flow -export { useMetadataCustomQuery } from './useMetadataQuery'; +export { useMetadataCustomQuery, useIndexedDBQuery } from './useMetadataQuery'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index 20799983f5..0e9e9afb73 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -27,3 +27,9 @@ export const useMetadataCustomQuery = ( enabled = true, }: CustomOptions = {}): Result => useAsyncMetadata(queryKey, queryFn, { cacheTime, enabled }); + +export const useIndexedDBQuery = ( + queryKey: QueryKey, + queryFn: QueryFunction, +): Result => + useMetadataCustomQuery(queryKey, queryFn); From 0f8ece05802d243d528d230c2001da0237a5c780 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Tue, 13 Dec 2022 21:05:31 +0100 Subject: [PATCH 67/95] feat: changed a hook to implement the custom hooks --- .../useProgramFromIndexedDB.js | 30 +++++++------------ .../query/useMetadataQuery.js | 12 ++------ .../query/useMetadataQuery.types.js | 8 ++--- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js index 0b8b44cb38..64030015bb 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js @@ -1,30 +1,20 @@ // @flow -import { useMemo, useRef, useState } from 'react'; -import { - getCachedSingleResourceFromKeyAsync, -} from '../../MetaDataStoreUtils/MetaDataStoreUtils'; import { userStores } from '../../storageControllers/stores'; +import { getUserStorageController } from '../../storageControllers'; +import { useIndexedDBQuery } from '../reactQueryHelpers'; export const useProgramFromIndexedDB = (programId: string) => { - const [cachedProgram, setCachedProgram] = useState(); - const [loading, setLoading] = useState(true); - const error = useRef(); + const storageController = getUserStorageController(); - useMemo(() => { - getCachedSingleResourceFromKeyAsync(userStores.PROGRAMS, programId) - .then((singleProgram) => { - setCachedProgram(singleProgram?.response); - setLoading(false); - }) - .catch((e) => { - error.current = e; - }); - }, [programId]); + const { data, isLoading, isError } = useIndexedDBQuery( + ['programs', programId], + () => storageController.get(userStores.PROGRAMS, programId), + ); return { - loading, - error: error.current, - program: cachedProgram, + program: data, + loading: isLoading, + error: isError, }; }; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index 0e9e9afb73..093915d07a 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -1,25 +1,17 @@ // @flow import { useQuery } from 'react-query'; -import type { QueryKey, QueryFunction } from 'react-query'; +import type { QueryFunction, QueryKey } from 'react-query'; import type { CustomOptions, Result } from './useMetadataQuery.types'; const useAsyncMetadata = ( queryKey: QueryKey, queryFn: QueryFunction, - { cacheTime, enabled }): Result => { - const { data, isLoading, isError } = useQuery(queryKey, queryFn, { + { cacheTime, enabled }): Result => useQuery(queryKey, queryFn, { enabled, cacheTime, staleTime: Infinity, }); - return { - data, - loading: isLoading, - failed: isError, - }; -}; - export const useMetadataCustomQuery = ( queryKey: QueryKey, queryFn: QueryFunction, { diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js index 6b8adaac81..3e93f08869 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -1,11 +1,9 @@ // @flow +import type { UseQueryResult } from 'react-query'; + export type CustomOptions = $ReadOnly<{| useCache?: boolean, enabled?: boolean, |}>; -export type Result = $ReadOnly<{| - data?: TResultData, - loading: boolean, - failed?: boolean, -|}>; +export type Result = UseQueryResult; From 7fa38a23905388bd3af0c2c3932ea750ddf2c2dd Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 21 Dec 2022 20:13:35 +0100 Subject: [PATCH 68/95] fix: added error handling to the useIndexedDBQuery --- .../storage/StorageController.js | 27 +++++++---- .../ProgramStageSelector.container.js | 4 +- .../hooks/useAvailableProgramStages.js | 4 +- .../useProgramFromIndexedDB.js | 7 ++- .../utils/reactQueryHelpers/index.js | 2 +- .../utils/reactQueryHelpers/query/index.js | 2 +- .../query/useMetadataQuery.js | 45 ++++++++++++------- .../query/useMetadataQuery.types.js | 5 --- 8 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/core_modules/capture-core-utils/storage/StorageController.js b/src/core_modules/capture-core-utils/storage/StorageController.js index c97dcb6a46..0227cfe8f6 100644 --- a/src/core_modules/capture-core-utils/storage/StorageController.js +++ b/src/core_modules/capture-core-utils/storage/StorageController.js @@ -4,6 +4,13 @@ import isArray from 'd2-utilizr/lib/isArray'; import log from 'loglevel'; import { errorCreator } from '../errorCreator'; +export class IndexedDBError extends Error { + constructor(error) { + super(error.message); + this.error = error; + } +} + export class StorageController { static errorMessages = { INVALID_NAME: 'A valid database name must be provided', @@ -44,7 +51,7 @@ export class StorageController { const ValidAdapters = Adapters .filter((Adapter) => { if (!StorageController.isAdapterValid(Adapter)) { - throw new Error( + throw new IndexedDBError( errorCreator(StorageController.errorMessages.INVALID_ADAPTER_PROVIDED)({ Adapter })); } return Adapter.isSupported(); @@ -72,15 +79,17 @@ export class StorageController { throwIfNotOpen() { const openError = this.getOpenStatusError(); if (openError) { - throw Error( - errorCreator(StorageController.errorMessages.STORAGE_NOT_OPEN)({ adapter: this.adapter }), + throw new IndexedDBError( + errorCreator( + StorageController.errorMessages.STORAGE_NOT_OPEN)( + { adapter: this.adapter }), ); } } throwIfStoreNotFound(store, caller) { if (!store || !this.adapter.objectStoreNames.includes(store)) { - throw Error( + throw new IndexedDBError( errorCreator( StorageController.errorMessages.INVALID_OBJECTSTORE)( { storageContainer: this, adapter: this.adapter, method: caller }), @@ -90,7 +99,7 @@ export class StorageController { throwIfDataObjectError = (dataObject) => { if (!dataObject || !dataObject[this.adapter.keyPath]) { - throw Error( + throw new IndexedDBError( errorCreator(StorageController.errorMessages.INVALID_STORAGE_OBJECT)({ adapter: this.adapter }), ); } @@ -98,7 +107,7 @@ export class StorageController { throwIfDataArrayError(dataArray) { if (!dataArray) { - throw Error( + throw new IndexedDBError( errorCreator(StorageController.errorMessages.INVALID_STORAGE_ARRAY)({ adapter: this.adapter }), ); } @@ -111,7 +120,7 @@ export class StorageController { const currentAdapterIndex = this.AvailableAdapters.findIndex(AA => AA === this.adapterType); const nextAdapterIndex = currentAdapterIndex + 1; if (this.AvailableAdapters.length <= nextAdapterIndex) { - throw new Error(StorageController.errorMessages.OPEN_FAILED); + throw new IndexedDBError(StorageController.errorMessages.OPEN_FAILED); } const Adapter = this.AvailableAdapters[nextAdapterIndex]; @@ -129,13 +138,13 @@ export class StorageController { // using async ensures that the the return value is wrapped in a promise async open(...args) { if (this.adapter.isOpen()) { - throw new Error( + throw new IndexedDBError( errorCreator(StorageController.errorMessages.STORAGE_ALREADY_OPEN)({ adapter: this.adapter }), ); } const objectStores = this.adapter.objectStoreNames; if (!objectStores || !isArray(objectStores) || objectStores.length === 0) { - throw new Error(StorageController.errorMessages.NO_OBJECTSTORES_DEFINED); + throw new IndexedDBError(StorageController.errorMessages.NO_OBJECTSTORES_DEFINED); } try { diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js index bffb8c4a3d..cbe6b8dbd6 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js @@ -17,9 +17,9 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId const { tab } = useLocationQuery(); const { error: enrollmentsError, enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { - loading: programLoading, - error: programError, program, + isLoading: programLoading, + isError: programError, } = useProgramFromIndexedDB(programId); diff --git a/src/core_modules/capture-core/hooks/useAvailableProgramStages.js b/src/core_modules/capture-core/hooks/useAvailableProgramStages.js index 9a64c3de6b..e7f615193f 100644 --- a/src/core_modules/capture-core/hooks/useAvailableProgramStages.js +++ b/src/core_modules/capture-core/hooks/useAvailableProgramStages.js @@ -9,8 +9,8 @@ import { useProgramFromIndexedDB } from '../utils/cachedDataHooks/useProgramFrom export const useAvailableProgramStages = (programStage: ProgramStage, teiId: string, enrollmentId: string, programId: string) => { const { error: enrollmentsError, enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { - loading: programLoading, - error: programError, + isLoading: programLoading, + isError: programError, program, } = useProgramFromIndexedDB(programId); diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js index 64030015bb..68f3eeed79 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js @@ -10,11 +10,14 @@ export const useProgramFromIndexedDB = (programId: string) => { const { data, isLoading, isError } = useIndexedDBQuery( ['programs', programId], () => storageController.get(userStores.PROGRAMS, programId), + { + enabled: !!programId, + }, ); return { program: data, - loading: isLoading, - error: isError, + isLoading, + isError, }; }; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js index ad3d2857ad..9d5121c8e5 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js @@ -1,2 +1,2 @@ // @flow -export { useMetadataCustomQuery, useIndexedDBQuery } from './query'; +export { useIndexedDBQuery } from './query'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js index 61d232b976..951b7145ea 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js @@ -1,2 +1,2 @@ // @flow -export { useMetadataCustomQuery, useIndexedDBQuery } from './useMetadataQuery'; +export { useIndexedDBQuery } from './useMetadataQuery'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index 093915d07a..fe88e433a6 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -1,27 +1,40 @@ // @flow import { useQuery } from 'react-query'; -import type { QueryFunction, QueryKey } from 'react-query'; -import type { CustomOptions, Result } from './useMetadataQuery.types'; +import log from 'loglevel'; +import type { QueryFunction, QueryKey, UseQueryOptions } from 'react-query'; +import type { Result } from './useMetadataQuery.types'; +import { IndexedDBError } from '../../../../capture-core-utils/storage/StorageController'; + +const throwErrorForIndexedDB = (error) => { + if (error instanceof IndexedDBError) { + log.error(error.error); + } else if (error instanceof Error) { + log.error(error.message); + } else { + log.error(error); + } + throw Error('There was an error fetching metadata'); +}; const useAsyncMetadata = ( queryKey: QueryKey, queryFn: QueryFunction, - { cacheTime, enabled }): Result => useQuery(queryKey, queryFn, { - enabled, - cacheTime, - staleTime: Infinity, - }); - -export const useMetadataCustomQuery = ( - queryKey: QueryKey, - queryFn: QueryFunction, { - cacheTime = 0, - enabled = true, - }: CustomOptions = {}): Result => - useAsyncMetadata(queryKey, queryFn, { cacheTime, enabled }); + queryOptions: UseQueryOptions, +): Result => useQuery(queryKey, queryFn, { + staleTime: Infinity, + ...queryOptions, +}); export const useIndexedDBQuery = ( queryKey: QueryKey, queryFn: QueryFunction, + queryOptions: UseQueryOptions, ): Result => - useMetadataCustomQuery(queryKey, queryFn); + useAsyncMetadata(queryKey, queryFn, { + cacheTime: 0, + ...queryOptions, + onError: (error) => { + queryOptions?.onError && queryOptions.onError(error); + throwErrorForIndexedDB(error); + }, + }); diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js index 3e93f08869..3aac3d847f 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -1,9 +1,4 @@ // @flow import type { UseQueryResult } from 'react-query'; -export type CustomOptions = $ReadOnly<{| - useCache?: boolean, - enabled?: boolean, -|}>; - export type Result = UseQueryResult; From 25cec6e7290a4632d1aadf72138e6ba82ee53777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Thu, 16 Jun 2022 16:06:01 +0200 Subject: [PATCH 69/95] feat: init relationships --- flow-typed/npm/react-query_v3.x.x.js | 1299 +++++++++++++++++ i18n/en.pot | 13 +- package.json | 4 +- .../EnrollmentPageDefault.component.js | 117 +- .../EnrollmentPageDefault.container.js | 3 - .../EnrollmentPageDefault.types.js | 8 +- ...kedEntityRelationshipsWrapper.component.js | 52 + ...TrackedEntityRelationshipsWrapper.types.js | 8 + .../index.js | 2 + .../common/TEIRelationshipsWidget/index.js | 3 + .../useTEIRelationshipsWidgetMetadata.js | 63 + .../Widget/WidgetCollapsible.component.js | 1 - .../Breadcrumbs/Breadcrumbs.js | 55 - .../NewTrackedEntityRelationship.component.js | 76 - .../NewTrackedEntityRelationship.container.js | 156 -- .../NewTrackedEntityRelationship.types.js | 13 - .../RelationshipSelectorRow.js | 64 - .../RelationshipTypeSelector.js | 44 - .../WidgetTrackedEntityRelationship.const.js | 7 - .../WidgetTrackedEntityRelationship.js | 31 - .../WidgetTrackedEntityRelationship.types.js | 29 - .../WidgetTrackedEntityRelationship/hooks.js | 118 -- .../Breadcrumbs/Breadcrumbs.component.js | 76 + .../Breadcrumbs/breadcrumbs.types.js | 13 + .../Breadcrumbs/index.js | 2 + .../LinkedEntityMetadataSelector.component.js | 28 + .../LinkedEntityMetadataSelector/index.js | 3 + .../linkedEntityMetadataSelector.types.js | 67 + .../useApplicableTypesAndSides.js | 168 +++ .../NewTrackedEntityRelationship.actions.js | 2 + .../NewTrackedEntityRelationship.component.js | 210 +++ .../NewTrackedEntityRelationship.const.js | 0 .../NewTrackedEntityRelationship.epics.js | 3 +- .../NewTrackedEntityRelationship.portal.js | 10 + .../NewTrackedEntityRelationship.types.js | 25 + .../RetrieverModeSelector.component.js | 40 + .../RetrieverModeSelector/index.js | 2 + .../retrieverModeSelector.types.js | 11 + .../SearchOrgUnitSelector.component.js | 0 .../SearchOrgUnitSelector.container.js | 0 ...archOrgUnitSelectorRefHandler.component.js | 0 .../searchOrgUnitSelector.actions.js | 0 .../searchOrgUnitSelector.epics.js | 0 .../SearchProgramSelector.component.js | 0 .../SearchProgramSelector.container.js | 0 .../searchProgramSelector.actions.js | 0 .../searchProgramSelector.selectors.js | 0 .../TeiSearchOLD}/TeiSearch.component.js | 0 .../TeiSearchOLD}/TeiSearch.container.js | 0 .../TeiSearchOLD}/TeiSearch.types.js | 0 .../TeiSearchForm/TeiSearchForm.component.js | 0 .../TeiSearchForm/TeiSearchForm.container.js | 0 .../TeiSearchResults.component.js | 0 .../TeiSearchResults.container.js | 0 .../TeiSearchResults.types.js | 0 .../actions/teiSearch.actions.js | 0 .../TeiSearchOLD}/getSearchGroups.js | 0 .../TeiSearchOLD}/teiSearch.selectors.js | 0 .../ProgramSection.component.js | 39 + .../ProgramSelector.component.js | 42 + .../ProgramSection/index.js | 2 + .../ProgramSection/programSelector.types.js | 9 + .../SearchFormSection.component.js | 31 + .../SearchFormSection/index.js | 4 + .../searchFormSection.types.js | 23 + .../SearchFormSection/useCancelableAsyncFn.js | 68 + .../SearchFormSection/useSearchGroups.js | 42 + .../TrackedEntityFinder.component.js | 45 + .../TrackedEntityFinder.types.js | 16 + .../TrackedEntityFinder/index.js | 4 + .../common/common.types.js | 8 + .../common/index.js | 3 + .../common/targetSides.js | 6 + .../NewTrackedEntityRelationship/index.js | 4 + .../wizardSteps.const.js | 6 + ...dgetTrackedEntityRelationship.component.js | 55 + .../WidgetTrackedEntityRelationship.types.js | 43 + .../common/index.js | 2 + .../common/relationshipEntities.js | 7 + .../WidgetTrackedEntityRelationship/hooks.js | 29 + .../WidgetTrackedEntityRelationship/index.js | 4 + .../LinkedEntityMetadataSelector.component.js | 73 + .../LinkedEntityMetadataSelector/index.js | 3 + .../linkedEntityMetadataSelector.types.js | 36 + .../organisationUnits.reducerDescription.js | 2 +- .../capture-core/storageControllers/index.js | 1 + .../ErrorHandler/ErrorHandler.component.js | 25 + .../ErrorHandler/errorHandler.types.js | 7 + .../capture-ui/ErrorHandler/index.js | 3 + .../LoadingHandler.component.js | 10 + .../capture-ui/LoadingHandler/index.js | 3 + .../LoadingHandler/loadingHandler.types.js | 7 + src/core_modules/capture-ui/index.js | 4 + src/epics/trackerCapture.epics.js | 4 - 94 files changed, 2828 insertions(+), 668 deletions(-) create mode 100644 flow-typed/npm/react-query_v3.x.x.js create mode 100644 src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js create mode 100644 src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js create mode 100644 src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipSelectorRow/RelationshipSelectorRow.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js rename src/core_modules/capture-core/components/{ => WidgetsRelationship}/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js (99%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js rename src/core_modules/capture-core/components/{ => WidgetsRelationship}/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js (100%) rename src/core_modules/capture-core/components/{ => WidgetsRelationship}/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js (99%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchProgramSelector/SearchProgramSelector.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchProgramSelector/SearchProgramSelector.container.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchProgramSelector/searchProgramSelector.actions.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/SearchProgramSelector/searchProgramSelector.selectors.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearch.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearch.container.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearch.types.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearchForm/TeiSearchForm.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearchForm/TeiSearchForm.container.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearchResults/TeiSearchResults.component.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearchResults/TeiSearchResults.container.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/TeiSearchResults/TeiSearchResults.types.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/actions/teiSearch.actions.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/getSearchGroups.js (100%) rename src/core_modules/capture-core/components/{WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch => WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD}/teiSearch.selectors.js (100%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js create mode 100644 src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js create mode 100644 src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js create mode 100644 src/core_modules/capture-ui/ErrorHandler/index.js create mode 100644 src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js create mode 100644 src/core_modules/capture-ui/LoadingHandler/index.js create mode 100644 src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js diff --git a/flow-typed/npm/react-query_v3.x.x.js b/flow-typed/npm/react-query_v3.x.x.js new file mode 100644 index 0000000000..4c6bd13ad0 --- /dev/null +++ b/flow-typed/npm/react-query_v3.x.x.js @@ -0,0 +1,1299 @@ +// flow-typed signature: fa0e8e361269bd4b07d94a58120cf9ad +// flow-typed version: 2d06cbbb04/react-query_v3.x.x/flow_>=v0.104.x + +// @flow + +declare module "react-query" { + declare export class Subscribable { + subscribe(listener?: TListener): () => void; + hasListeners(): boolean; + } + declare export type QueryCacheListener = (query?: Query) => void; + + declare export class QueryCache extends Subscribable { + build( + client: QueryClient, + options: QueryOptions, + state?: QueryState + ): Query; + add(query: Query): void; + remove(query: Query): void; + clear(): void; + get( + queryHash: string + ): ?Query; + getAll(): Query[]; + find( + queryKey: QueryKey, + filters?: QueryFilters + ): ?Query; + findAll( + queryKey?: QueryKey, + filters?: QueryFilters + ): Query[]; + findAll( + filters?: QueryFilters + ): Query[]; + notify( + query?: Query + ): void; + onFocus(): void; + onOnline(): void; + } + + declare export type QueryFilters = {| + /** + * Include or exclude active queries + */ + active?: boolean, + /** + * Match query key exactly + */ + exact?: boolean, + /** + * Include or exclude inactive queries + */ + inactive?: boolean, + /** + * Include queries matching this predicate function + */ + predicate?: ( + query: Query + ) => boolean, + /** + * Include queries matching this query key + */ + queryKey?: QueryKey, + /** + * Include or exclude stale queries + */ + stale?: boolean, + /** + * Include or exclude fetching queries + */ + fetching?: boolean, + |}; + declare export type InvalidateQueryFilters = {| + ...QueryFilters, + refetchActive?: boolean, + refetchInactive?: boolean, + |}; + + declare export type QueryClientConfig = {| + queryCache?: QueryCache, + mutationCache?: MutationCache, + defaultOptions?: DefaultOptions, + |}; + + declare export class QueryClient { + constructor(config?: QueryClientConfig): this; + mount(): void; + unmount(): void; + + isFetching(filters?: QueryFilters): number; + isFetching(queryKey?: QueryKey, filters?: QueryFilters): number; + + getQueryData(queryKey: QueryKey, filters?: QueryFilters): void | TData; + setQueryData( + queryKey: QueryKey, + updater: Updater, + options?: SetDataOptions + ): TData; + getQueryState( + queryKey: QueryKey, + filters?: QueryFilters + ): ?QueryState; + + removeQueries(filters?: QueryFilters): void; + removeQueries(queryKey?: QueryKey, filters?: QueryFilters): void; + + cancelQueries( + filters?: QueryFilters, + options?: CancelOptions + ): Promise; + cancelQueries( + queryKey?: QueryKey, + filters?: QueryFilters, + options?: CancelOptions + ): Promise; + + invalidateQueries( + filters?: InvalidateQueryFilters, + options?: InvalidateOptions + ): Promise; + invalidateQueries( + queryKey: QueryKey, + filters?: InvalidateQueryFilters, + options?: InvalidateOptions + ): Promise; + + refetchQueries( + filters?: QueryFilters, + options?: RefetchOptions + ): Promise; + refetchQueries( + queryKey: QueryKey, + filters?: QueryFilters, + options?: RefetchOptions + ): Promise; + + fetchQuery( + options: FetchQueryOptions + ): Promise; + fetchQuery( + queryKey: QueryKey, + options?: FetchQueryOptions + ): Promise; + fetchQuery( + queryKey: QueryKey, + queryFn: QueryFunction, + options?: FetchQueryOptions + ): Promise; + + prefetchQuery( + options: FetchQueryOptions + ): Promise; + prefetchQuery( + queryKey: QueryKey, + options?: FetchQueryOptions + ): Promise; + prefetchQuery( + queryKey: QueryKey, + queryFn: QueryFunction, + options?: FetchQueryOptions + ): Promise; + + cancelMutations(): Promise; + resumePausedMutations(): Promise; + executeMutation( + options: MutationOptions + ): Promise; + + getQueryCache(): QueryCache; + getMutationCache(): MutationCache; + getDefaultOptions(): DefaultOptions; + setDefaultOptions(options: DefaultOptions): void; + setQueryDefaults( + queryKey: QueryKey, + options: QueryObserverOptions + ): void; + getQueryDefaults( + queryKey?: QueryKey + ): ?QueryObserverOptions; + setMutationDefaults( + mutationKey: MutationKey, + options: MutationObserverOptions + ): void; + getMutationDefaults( + mutationKey?: MutationKey + ): ?MutationObserverOptions; + defaultQueryOptions>(options?: T): T; + defaultQueryObserverOptions>( + options?: T + ): T; + defaultMutationOptions>( + options?: T + ): T; + clear(): void; + } + + declare export type CancelOptions = {| + revert?: boolean, + silent?: boolean, + |}; + declare export type ResultOptions = {| + throwOnError?: boolean, + cancelRefetch?: boolean, + |}; + declare export type RefetchOptions = ResultOptions; + declare export type InvalidateOptions = ResultOptions; + declare export type FetchNextPageOptions = {| + ...ResultOptions, + pageParam?: T, + |}; + declare export type FetchPreviousPageOptions = FetchNextPageOptions; + + // retry related + declare export type RetryValue = + | boolean + | number + | ShouldRetryFunction; + declare export type ShouldRetryFunction = ( + failureCount: number, + error: TError + ) => boolean; + declare export type RetryDelayValue = + | number + | RetryDelayFunction; + declare export type RetryDelayFunction = ( + failureCount: number, + error: TError + ) => number; + + // updater + declare export type DataUpdateFunction = (input: TInput) => TOutput; + declare export type Updater = + | TOutput + | DataUpdateFunction; + + // Action + declare export type FailedAction = {| + type: "failed", + |}; + + declare export type FetchAction = {| + type: "fetch", + meta?: mixed, + |}; + + declare export type SuccessAction = {| + data: void | TData, + type: "success", + dataUpdatedAt?: number, + |}; + + declare export type ErrorAction = {| + type: "error", + error: TError, + |}; + + declare export type InvalidateAction = {| + type: "invalidate", + |}; + + declare export type PauseAction = {| + type: "pause", + |}; + + declare export type ContinueAction = {| + type: "continue", + |}; + + declare export type SetStateAction = {| + type: "setState", + state: MutationState, + |}; + + declare export type SetQueryStateAction = {| + type: "setState", + state: QueryState, + |}; + + declare export type Action = + | ContinueAction + | ErrorAction + | FailedAction + | LoadingAction + | PauseAction + | SetStateAction + | SuccessAction; + + declare export type QueryAction = + | ContinueAction + | ErrorAction + | FailedAction + | FetchAction + | InvalidateAction + | PauseAction + | SetQueryStateAction + | SuccessAction; + + // query types + declare export type FetchOptions = {| + cancelRefresh?: boolean, + meta?: mixed, + |}; + declare export type FetchContext = {| + fetchFn: () => mixed | Promise, + fetchOptions?: FetchOptions, + options: QueryOptions, + queryKey: QueryKey, + state: QueryState, + |}; + declare export type QueryBehavior = {| + onFetch: (context: FetchContext) => void, + |}; + declare export type QueryConfig = {| + cache: QueryCache, + queryKey: QueryKey, + queryHash: string, + options?: QueryOptions, + defaultOptions?: QueryOptions, + state?: QueryState, + |}; + declare export type EnumStatus = "idle" | "loading" | "success" | "error"; + declare export type QueryStatus = EnumStatus; + declare export type QueryState = {| + data: void | TData, + dataUpdateCount: number, + dataUpdatedAt: number, + error: TError | null, + errorUpdateCount: number, + errorUpdatedAt: number, + fetchFailureCount: number, + fetchMeta: mixed, + isFetching: boolean, + isInvalidated: boolean, + isPaused: boolean, + status: QueryStatus, + |}; + + declare export type InitialDataFunction = () => void | TData; + declare export type QueryKey = string | mixed[] | $ReadOnlyArray; + declare export type PageParam = mixed; + declare export type QueryKeyHashFunction = (queryKey: TQueryKey) => string; + declare export type QueryFunctionContext = {| + queryKey: TQueryKey, + pageParam?: TPageParam, + |}; + declare export type QueryFunction = (context: QueryFunctionContext) => Promise | TQueryFnData; + declare export type GetPreviousPageParamFunction = ( + firstPage: TQueryFnData, + allPages: TQueryFnData[] + ) => U; + declare export type GetNextPageParamFunction = ( + lastPage: TQueryFnData, + allPages: TQueryFnData[] + ) => U; + declare export type QueryOptions = {| + retry?: RetryValue, + retryDelay?: RetryDelayValue, + cacheTime?: number, + isDataEqual?: (oldData: void | TData, newData: TData) => boolean, + queryFn?: QueryFunction, + queryHash?: string, + queryKey?: TQueryKey, + queryKeyHashFn?: QueryKeyHashFunction, + initialData?: TData | InitialDataFunction, + behavior?: QueryBehavior, + /** + * Set this to `false` to disable structural sharing between query results. + * Defaults to `true`. + */ + structuralSharing?: boolean, + /** + * This function can be set to automatically get the previous cursor for infinite queries. + * The result will also be used to determine the value of `hasPreviousPage`. + */ + getPreviousPageParam?: GetPreviousPageParamFunction, + /** + * This function can be set to automatically get the next cursor for infinite queries. + * The result will also be used to determine the value of `hasNextPage`. + */ + getNextPageParam?: GetNextPageParamFunction, + |}; + declare export type FetchQueryOptions = {| + ...QueryOptions, + staleTime?: number, + |}; + declare export type PlaceholderDataFunction = () => void | TResult; + + declare export type QueryObserverOptions< + TQueryFnData, + TError = mixed, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey = QueryKey, + > = {| + ...QueryOptions, + /** + * Set this to `false` to disable automatic refetching when the query mounts or changes query keys. + * To refetch the query, use the `refetch` method returned from the `useQuery` instance. + * Defaults to `true`. + */ + enabled?: boolean, + /** + * The time in milliseconds after data is considered stale. + * If set to `Infinity`, the data will never be considered stale. + */ + staleTime?: number, + /** + * If set to a number, the query will continuously refetch at this frequency in milliseconds. + * Defaults to `false`. + */ + refetchInterval?: number | false, + /** + * If set to `true`, the query will continue to refetch while their tab/window is in the background. + * Defaults to `false`. + */ + refetchIntervalInBackground?: boolean, + /** + * If set to `true`, the query will refetch on window focus if the data is stale. + * If set to `false`, the query will not refetch on window focus. + * If set to `'always'`, the query will always refetch on window focus. + * Defaults to `true`. + */ + refetchOnWindowFocus?: boolean | "always", + /** + * If set to `true`, the query will refetch on reconnect if the data is stale. + * If set to `false`, the query will not refetch on reconnect. + * If set to `'always'`, the query will always refetch on reconnect. + * Defaults to `true`. + */ + refetchOnReconnect?: boolean | "always", + /** + * If set to `true`, the query will refetch on mount if the data is stale. + * If set to `false`, will disable additional instances of a query to trigger background refetches. + * If set to `'always'`, the query will always refetch on mount. + * Defaults to `true`. + */ + refetchOnMount?: boolean | "always", + /** + * If set to `false`, the query will not be retried on mount if it contains an error. + * Defaults to `true`. + */ + retryOnMount?: boolean, + /** + * If set, the component will only re-render if any of the listed properties change. + * When set to `['data', 'error']`, the component will only re-render when the `data` or `error` properties change. + */ + notifyOnChangeProps?: string[] | "tracked", + /** + * If set, the component will not re-render if any of the listed properties change. + */ + notifyOnChangePropsExclusions?: string[], + /** + * This callback will fire any time the query successfully fetches new data. + */ + onSuccess?: (data: TData) => void, + /** + * This callback will fire if the query encounters an error and will be passed the error. + */ + onError?: (err: TError) => void, + /** + * This callback will fire any time the query is either successfully fetched or errors and be passed either the data or error. + */ + onSettled?: (data: void | TData, error: TError | null) => void, + /** + * Whether errors should be thrown instead of setting the `error` property. + * Defaults to `false`. + */ + useErrorBoundary?: boolean, + /** + * This option can be used to transform or select a part of the data returned by the query function. + */ + select?: (data: TQueryData) => TData, + /** + * If set to `true`, the query will suspend when `status === 'loading'` + * and throw errors when `status === 'error'`. + * Defaults to `false`. + */ + suspense?: boolean, + /** + * Set this to `true` to keep the previous `data` when fetching based on a new query key. + * Defaults to `false`. + */ + keepPreviousData?: boolean, + /** + * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. + */ + placeholderData?: TData | PlaceholderDataFunction, + |}; + + declare export type InfiniteQueryObserverOptions = + QueryObserverOptions< + TQueryFnData, + TError, + InfiniteData, + InfiniteData, + TQueryKey, + >; + + declare export type QueryObserverBaseResult = {| + data: void | TData, + dataUpdatedAt: number, + error: TError | null, + errorUpdatedAt: number, + failureCount: number, + isError: boolean, + isFetched: boolean, + isFetchedAfterMount: boolean, + isFetching: boolean, + isIdle: boolean, + isLoading: boolean, + isLoadingError: boolean, + isPlaceholderData: boolean, + isPreviousData: boolean, + isRefetchError: boolean, + isStale: boolean, + isSuccess: boolean, + refetch: ( + options?: RefetchOptions + ) => Promise>, + remove: () => void, + status: QueryStatus, + |}; + + declare export type QueryObserverIdleResult = {| + ...QueryObserverBaseResult, + error: null, + isError: false, + isIdle: true, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "idle", + |}; + + declare export type QueryObserverLoadingResult = {| + ...QueryObserverBaseResult, + error: null, + isError: false, + isIdle: false, + isLoading: true, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "loading", + |}; + + declare export type QueryObserverLoadingErrorResult = {| + ...QueryObserverBaseResult, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: true, + isRefetchError: false, + isSuccess: false, + status: "error", + |}; + + declare export type QueryObserverRefetchErrorResult = {| + ...QueryObserverBaseResult, + data: TData, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: true, + isSuccess: false, + status: "error", + |}; + + declare export type QueryObserverSuccessResult = {| + ...QueryObserverBaseResult, + data: TData, + error: null, + isError: false, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: "success", + |}; + + // exported for testing + declare export type QueryObserverResult = + | QueryObserverIdleResult + | QueryObserverLoadingErrorResult + | QueryObserverLoadingResult + | QueryObserverRefetchErrorResult + | QueryObserverSuccessResult; + + declare export type InfiniteData = {| + pages: TData[], + pageParams: mixed[], + |}; + + declare export type InfiniteQueryObserverBaseResult = {| + ...QueryObserverBaseResult, TError>, + fetchNextPage: ( + options?: FetchNextPageOptions + ) => Promise>, + fetchPreviousPage: ( + options?: FetchPreviousPageOptions + ) => Promise>, + hasNextPage?: boolean, + hasPreviousPage?: boolean, + isFetchingNextPage: boolean, + isFetchingPreviousPage: boolean, + |}; + + declare export type InfiniteQueryObserverIdleResult = {| + ...InfiniteQueryObserverBaseResult, + error: null, + isError: false, + isIdle: true, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "idle", + |}; + + declare export type InfiniteQueryObserverLoadingResult = {| + ...InfiniteQueryObserverBaseResult, + error: null, + isError: false, + isIdle: false, + isLoading: true, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: "loading", + |}; + + declare export type InfiniteQueryObserverLoadingErrorResult = {| + ...InfiniteQueryObserverBaseResult, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: true, + isRefetchError: false, + isSuccess: false, + status: "error", + |}; + + declare export type InfiniteQueryObserverRefetchErrorResult = {| + ...InfiniteQueryObserverBaseResult, + data: InfiniteData, + error: TError, + isError: true, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: true, + isSuccess: false, + status: "error", + |}; + + declare export type InfiniteQueryObserverSuccessResult = {| + ...InfiniteQueryObserverBaseResult, + data: InfiniteData, + error: null, + isError: false, + isIdle: false, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: "success", + |}; + + declare export type InfiniteQueryObserverResult = + | InfiniteQueryObserverIdleResult + | InfiniteQueryObserverLoadingErrorResult + | InfiniteQueryObserverLoadingResult + | InfiniteQueryObserverRefetchErrorResult + | InfiniteQueryObserverSuccessResult; + + declare export type QueryObserverListener = ( + result: QueryObserverResult + ) => void; + + declare export type ObserverFetchOptions = {| + ...FetchOptions, + throwOnError?: boolean, + |}; + + declare export class QueryObserver< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + TPageParam = PageParam, + > + extends Subscribable> { + +options: QueryObserverOptions; + + constructor( + client: QueryClient, + options: QueryObserverOptions + ): this; + + willLoadOnMount(): boolean; + willRefetchOnMount(): boolean; + willFetchOnMount(): boolean; + willFetchOnReconnect(): boolean; + willFetchOnWindowFocus(): boolean; + destroy(): void; + setOptions( + options?: QueryObserverOptions + ): void; + getCurrentResult(): QueryObserverResult; + getNextResult( + options?: ResultOptions + ): Promise>; + getCurrentQuery(): Query; + remove(): void; + refetch( + options?: RefetchOptions + ): Promise>; + onQueryUpdate(action: Action): void; + } + + declare export type QueriesObserverListener = ( + result: $ReadOnlyArray> + ) => void; + declare export class QueriesObserver< + TQueryFnData, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > + extends Subscribable> { + constructor( + client: QueryClient, + queries?: QueryObserverOptions[] + ): this; + destroy(): void; + setQueries( + queries: QueryObserverOptions[] + ): void; + getCurrentResult(): QueryObserverResult[]; + } + + declare export type InfiniteQueryObserverListener = ( + result: InfiniteQueryObserverResult + ) => void; + + declare export class InfiniteQueryObserver< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > + extends Subscribable> { + constructor( + client: QueryClient, + options: InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + > + ): this; + + // Type override + subscribe( + listener?: InfiniteQueryObserverListener + ): () => void; + + // Type override + getCurrentResult(): InfiniteQueryObserverResult; + + willLoadOnMount(): boolean; + willRefetchOnMount(): boolean; + willFetchOnMount(): boolean; + willFetchOnReconnect(): boolean; + willFetchOnWindowFocus(): boolean; + destroy(): void; + getCurrentResult(): InfiniteQueryObserverResult; + getNextResult( + options?: ResultOptions + ): Promise>; + getCurrentQuery(): Query; + remove(): void; + refetch( + options?: RefetchOptions + ): Promise>; + onQueryUpdate(action: Action): void; + + //////////////////// + + setOptions( + options?: InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + > + ): void; + fetchNextPage( + options?: FetchNextPageOptions + ): Promise>; + fetchPreviousPage( + options?: FetchPreviousPageOptions + ): Promise>; + } + + declare export type SetDataOptions = {| updatedAt?: number |}; + + declare export class Query { + +queryKey: QueryKey; + +queryHash: string; + +options: QueryOptions; + +state: QueryState; + +cacheTime: number; + + constructor(config: QueryConfig): this; + setDefaultOptions(options: QueryOptions): void; + setData(updater: Updater, options?: SetDataOptions): TData; + setState(state: QueryState): void; + cancel(options?: CancelOptions): Promise; + destroy(): void; + isActive(): boolean; + isFetching(): boolean; + isStale(): boolean; + isStaleByTime(staleTime?: number): boolean; + onFocus(): void; + onOnline(): void; + addObserver(observer: QueryObserver): void; + removeObserver(observer: QueryObserver): void; + invalidate(): void; + fetch( + options?: QueryOptions, + fetchOptions?: FetchOptions + ): Promise; + } + + /** + * + * Mutations + * + */ + + declare export type MutationConfig = {| + mutationId: number, + mutationCache: MutationCache, + options: MutationOptions, + defaultOptions?: MutationOptions, + state?: MutationState, + |}; + + declare export type MutationKey = QueryKey; + declare export type MutationStatus = EnumStatus; + + declare export type MutationFunction = ( + variables: TVariables + ) => Promise; + + declare export type MutationOptions = {| + mutationFn?: MutationFunction, + mutationKey?: MutationKey, + variables?: TVariables, + onMutate?: (variables: TVariables) => Promise | TContext, + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: TContext + ) => Promise | void, + retry?: RetryValue, + retryDelay?: RetryDelayValue, + |}; + + declare export type MutationObserverOptions< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + ...MutationOptions, + useErrorBoundary?: boolean, + |}; + + declare export type MutateOptions = {| + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: void | TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: void | TContext + ) => Promise | void, + |}; + + declare export type MutateFunction = ( + variables: TVariables, + options?: MutateOptions + ) => Promise; + + declare export type MutationObserverResult< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + ...MutationState, + isError: boolean, + isIdle: boolean, + isLoading: boolean, + isSuccess: boolean, + mutate: MutateFunction, + reset: () => void, + |}; + + declare export type DefaultOptions = {| + queries?: QueryObserverOptions, + mutations?: MutationObserverOptions, + |}; + declare export type MutationState = {| + context: void | TContext, + data: void | TData, + error: TError | null, + failureCount: number, + isPaused: boolean, + status: MutationStatus, + variables: void | TVariables, + |}; + + declare export type LoadingAction = {| + type: "loading", + variables?: TVariables, + context?: TContext, + |}; + + declare export type SetMutationStateAction< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + type: "setState", + state: MutationState, + |}; + + declare export type MutationAction = + | ContinueAction + | ErrorAction + | FailedAction + | LoadingAction + | PauseAction + | SetMutationStateAction + | SuccessAction; + + declare export class Mutation { + +state: MutationState; + +options: MutationOptions; + +mutationId: number; + constructor( + config: MutationConfig + ): this; + setState(state: MutationState): void; + addObserver(observer: MutationObserver): void; + removeObserver(observer: MutationObserver): void; + cancel(): Promise; + continue(): Promise; + execute(): Promise; + } + + declare export type MutationObserverListener< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = ( + result: MutationObserverResult + ) => void; + declare export class MutationObserver< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > + extends + Subscribable< + MutationObserverListener + > { + +options: MutationObserverOptions; + constructor( + client: QueryClient, + options: MutationObserverOptions + ): this; + setOptions( + options?: MutationObserverOptions + ): void; + onMutationUpdate(action: Action): void; + getCurrentResult(): MutationObserverResult< + TData, + TError, + TVariables, + TContext + >; + reset(): void; + mutate( + variables?: TVariables, + options?: MutateOptions + ): Promise; + } + + declare export type MutationCacheListener = ( + mutation?: Mutation + ) => void; + declare export class MutationCache + extends Subscribable { + build( + client: QueryClient, + options: MutationOptions, + state?: MutationState + ): Mutation; + add(mutation: Mutation): void; + remove(mutation: Mutation): void; + clear(): void; + getAll(): Mutation[]; + notify(mutation?: Mutation): void; + onFocus(): void; + onOnline(): void; + resumePausedMutations(): Promise; + } + + declare export type UseBaseQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = QueryObserverOptions; + + declare export type UseQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = UseBaseQueryOptions; + + declare export type UseInfiniteQueryOptions< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey, + > = InfiniteQueryObserverOptions; + + declare export type UseBaseQueryResult = QueryObserverResult< + TData, + TError>; + + declare export type UseQueryResult = UseBaseQueryResult< + TData, + TError>; + + declare export type UseInfiniteQueryResult< + TData, + TError + > = InfiniteQueryObserverResult; + + declare export type UseMutationOptions< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + mutationKey?: MutationKey, + onMutate?: (variables: TVariables) => Promise | TContext, + onSuccess?: ( + data: TData, + variables: TVariables, + context: ?TContext + ) => Promise | void, + onError?: ( + error: TError, + variables: TVariables, + context: ?TContext + ) => Promise | void, + onSettled?: ( + data: void | TData, + error: TError | null, + variables: TVariables, + context: ?TContext + ) => Promise | void, + retry?: RetryValue, + retryDelay?: RetryDelayValue, + useErrorBoundary?: boolean, + |}; + + declare export type UseMutateFunction = ( + variables: TVariables, + options?: MutateOptions + ) => void; + + declare export type UseMutateAsyncFunction< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = ( + variables: TVariables, + options?: MutateOptions + ) => Promise; + + declare export type UseMutationResult< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + > = {| + context: ?TContext, + data: void | TData, + error: TError | null, + failureCount: number, + isError: boolean, + isIdle: boolean, + isLoading: boolean, + isPaused: boolean, + isSuccess: boolean, + mutate: UseMutateFunction, + mutateAsync: UseMutateAsyncFunction, + reset: () => void, + status: MutationStatus, + variables: ?TVariables, + |}; + + declare export function useQuery( + options: UseQueryOptions + ): UseQueryResult; + declare export function useQuery( + queryKey: TQueryKey, + options?: UseQueryOptions + ): UseQueryResult; + declare export function useQuery( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseQueryOptions + ): UseQueryResult; + + declare export function useQueries( + queries: UseQueryOptions[] + ): UseQueryResult[]; + + declare export function useInfiniteQuery(options: UseInfiniteQueryOptions): UseInfiniteQueryResult; + declare export function useInfiniteQuery( + queryKey: TQueryKey, + options?: UseInfiniteQueryOptions + ): UseInfiniteQueryResult; + declare export function useInfiniteQuery< + TQueryFnData = mixed, + TError = mixed, + TData = TQueryFnData, + TQueryKey = QueryKey + >( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseInfiniteQueryOptions + ): UseInfiniteQueryResult; + + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + options: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationFn: MutationFunction, + options?: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationKey: MutationKey, + options?: UseMutationOptions + ): UseMutationResult; + declare export function useMutation< + TData = mixed, + TError = mixed, + TVariables = mixed, + TContext = mixed, + >( + mutationKey: MutationKey, + mutationFn?: MutationFunction, + options?: UseMutationOptions + ): UseMutationResult; + + declare export function useIsFetching(filters?: QueryFilters): number; + declare export function useIsFetching( + queryKey?: QueryKey, + filters?: QueryFilters + ): number; + + declare export function useQueryClient(): QueryClient; + declare export class QueryClientProvider + extends + React$Component<{| + client: QueryClient, + children?: React$Node, + |}> {} + + declare export type QueryErrorResetBoundaryValue = {| + clearReset: () => void, + isReset: () => boolean, + reset: () => void, + |}; + declare export function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue; + declare export class QueryErrorResetBoundary + extends + React$Component<{| + children: + | React$Node + | ((value: QueryErrorResetBoundaryValue) => React$Node), + |}> {} + declare export function setLogger(logger: {| + log: (...args: mixed[]) => void, + warn: (...args: mixed[]) => void, + error: (...args: mixed[]) => void, + |}): void; + + declare export type NotifyCallback = () => void; + declare export type NotifyFunction = (callback: () => void) => void; + declare export type BatchNotifyFunction = (callback: () => void) => void; + + declare export class NotifyManager { + batch(callback: () => T): T; + schedule(callback: NotifyCallback): void; + batchCalls(callback: T): T; + flush(): void; + + /** + * Use this method to set a custom notify function. + * This can be used to for example wrap notifications with `React.act` while running tests. + */ + setNotifyFunction(fn: NotifyFunction): void; + + /** + * Use this method to set a custom function to batch notifications together into a single tick. + * By default React Query will use the batch function provided by ReactDOM or React Native. + */ + setBatchNotifyFunction(fn: BatchNotifyFunction): void; + } + declare export var notifyManager: NotifyManager; + declare export var focusManager: {| + setEventListener( + setup: (onFocus: () => void) => (focused?: boolean) => void + ): void, + setFocused(focused?: boolean): void, + isFocused: boolean, + |}; + declare export var onlineManager: {| + setEventListener( + setup: (setOnline: () => void) => (online?: boolean) => void + ): void, + setOnline(online?: boolean): void, + isOnline: boolean, + |}; + declare export function hashQueryKey(queryKey: QueryKey): string; + declare export function isError(value: any): boolean; + declare export function isCancelledError(value: any): boolean; +} diff --git a/i18n/en.pot b/i18n/en.pot index f973854e00..77db698b3e 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2022-02-18T22:32:30.675Z\n" -"PO-Revision-Date: 2022-02-18T22:32:30.675Z\n" +"POT-Creation-Date: 2022-11-29T09:14:06.464Z\n" +"PO-Revision-Date: 2022-11-29T09:14:06.464Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -625,6 +625,9 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" +msgid "Could not retrieve metadata. Please try again later." +msgstr "Could not retrieve metadata. Please try again later." + msgid "Program stage not found" msgstr "Program stage not found" @@ -1169,6 +1172,9 @@ msgstr "Stages and Events" msgid "New TEI Relationship" msgstr "New TEI Relationship" +msgid "Go back without saving relationship" +msgstr "Go back without saving relationship" + msgid "Link to an existing person" msgstr "Link to an existing person" @@ -1178,9 +1184,6 @@ msgstr "Create new" msgid "An error occurred" msgstr "An error occurred" -msgid "Go back without saving relationship" -msgstr "Go back without saving relationship" - msgid "New Relationship" msgstr "New Relationship" diff --git a/package.json b/package.json index d51e4ef769..769e10a5a1 100644 --- a/package.json +++ b/package.json @@ -120,9 +120,7 @@ "wait-on": "^6.0.1" }, "peerDependencies": { - "capture-core": "1", - "capture-core-utils": "1", - "capture-ui": "1" + "react-query": "^3.13.11" }, "resolutions": { "@babel/preset-react": "7.16.7" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 7964be47cf..6f55cda55a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType, useRef } from 'react'; +import React, { type ComponentType, useRef, useEffect, useState, useCallback } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -13,9 +13,7 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; -import { - WidgetTrackedEntityRelationship, -} from '../../../WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship'; +import { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper'; const getStyles = ({ typography }) => ({ container: { @@ -62,62 +60,75 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, - relationshipTypes, }: PlainProps) => { + const [mainContentVisible, setMainContentVisibility] = useState(true); + const [addRelationShipContainerElement, setAddRelationshipContainerElement] = useState(undefined); const renderRelationshipRef = useRef(); + + useEffect(() => { + setAddRelationshipContainerElement(renderRelationshipRef.current); + }, []); + + const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); + return ( -
-
{i18n.t('Enrollment Dashboard')}
-
-
- - -
-
- - - - - {!hideWidgets.indicator && ( - +
+
+
{i18n.t('Enrollment Dashboard')}
+
+
+ + - )} - {!hideWidgets.feedback && ( - +
+ - )} - - {enrollmentId !== 'AUTO' && } + + + + {!hideWidgets.indicator && ( + + )} + {!hideWidgets.feedback && ( + + )} + + {enrollmentId !== 'AUTO' && } +
-
+ ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 3ef5a20682..84d7607c9d 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -18,7 +18,6 @@ import { import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; -import { useRelationshipTypes } from '../../../WidgetTrackedEntityRelationship/hooks'; export const EnrollmentPageDefault = () => { @@ -26,7 +25,6 @@ export const EnrollmentPageDefault = () => { const dispatch = useDispatch(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit } = useOrganisationUnit(orgUnitId); - const { relationshipTypes } = useRelationshipTypes(); const program = useTrackerProgram(programId); const { @@ -89,7 +87,6 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} - relationshipTypes={relationshipTypes} /> ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 89ed15793b..088460205f 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -1,12 +1,12 @@ // @flow -import type { Program } from 'capture-core/metaData'; +import type { TrackerProgram } from 'capture-core/metaData'; import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types'; import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import type { RelationshipType } from '../../../WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types'; +import type { RelationshipType } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type Props = {| - program: Program, + program: TrackerProgram, enrollmentId: string, teiId: string, events: ?Array, @@ -18,7 +18,7 @@ export type Props = {| onViewAll: (stageId: string) => void, onCreateNew: (stageId: string) => void, onEventClick: (eventId: string, stageId: string) => void, - relationshipTypes: ?Array + relationshipTypes?: Array |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js new file mode 100644 index 0000000000..37a09a563c --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -0,0 +1,52 @@ +// @flow +import React, { useCallback, useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { useTEIRelationshipsWidgetMetadata } from '../../../common/TEIRelationshipsWidget'; +import { WidgetTrackedEntityRelationship } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; +import type { Props } from './TrackedEntityRelationshipsWrapper.types'; + +export const TrackedEntityRelationshipsWrapper = ({ + trackedEntityTypeId, + programId, + addRelationshipRenderElement, + onOpenAddRelationship, + onCloseAddRelationship, +}: Props) => { + const [a, setA] = useState(); + debugger; + const { getPrograms, relationshipTypes, failed } = useTEIRelationshipsWidgetMetadata(); + + const getSearchGroups = useCallback((programId: string) => { + + }, []); + + if (failed) { + return ( +
+ {i18n.t('Could not retrieve metadata. Please try again later.')} +
+ ); + } + if (!(relationshipTypes && addRelationshipRenderElement)) { + return null; + } + + return ( + <> + + [{ unique: false, fields: [{ id: 'id1', type: 'TEXT' }] }]} + /> + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js new file mode 100644 index 0000000000..4316c363eb --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js @@ -0,0 +1,8 @@ +// @flow +export type Props = {| + trackedEntityTypeId: string, + programId: string, + addRelationshipRenderElement: HTMLDivElement, + onOpenAddRelationship: () => void, + onCloseAddRelationship: () => void, +|}; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js new file mode 100644 index 0000000000..6ca3eb46ab --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js @@ -0,0 +1,2 @@ +// @flow +export { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper.component'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js new file mode 100644 index 0000000000..355ee4bfa6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js @@ -0,0 +1,3 @@ +// @flow + +export { useTEIRelationshipsWidgetMetadata } from './useTEIRelationshipsWidgetMetadata'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js new file mode 100644 index 0000000000..f1de1cb662 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js @@ -0,0 +1,63 @@ +// @flow +import { useCallback } from 'react'; +import { useMetadataCustomQuery } from 'capture-core-utils/reactQueryHelpers'; +import { getUserStorageController, userStores } from '../../../../storageControllers'; +import { programCollection } from '../../../../metaDataMemoryStores'; +import { TrackerProgram } from '../../../../metaData'; +import type { GetPrograms } from '../../../WidgetTrackedEntityRelationship'; + +export const useTEIRelationshipsWidgetMetadata = (): { getPrograms: GetPrograms } => { + const getPrograms = useCallback((trackedEntityTypeId: string) => + [...programCollection.values()] + .filter(program => + program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.read, + ) + .map(p => ({ id: p.id, name: p.name })), + []); + + const { data: relationshipTypes, failed } = + useMetadataCustomQuery<>( + 'relationshipTypes', + async () => { + const userStorageController = getUserStorageController(); + const relationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); + + const attributeIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = fromConstraint.tracekrDataView.attributes || []; + const toAttributes = toConstraint.tracekrDataView.attributes || []; + return [fromAttributes, toAttributes]; + }); + + const attributes = await Promise.all( + attributeIds.map(attributeId => + userStorageController.get(userStores.TRACKED_ENTITY_ATTRIBUTES, attributeId)), + ); + + const dataElementIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromDataElements = fromConstraint.tracekrDataView.dataElements || []; + const toDataElements = toConstraint.tracekrDataView.dataElements || []; + return [fromDataElements, toDataElements]; + }); + + const dataElements = await Promise.all( + attributeIds.map(attributeId => + userStorageController.get(userStores.DATA, attributeId)), + ); + + + + }, + ); + + return { + relationshipTypes, + getPrograms, + failed, + }; +}; diff --git a/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js b/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js index 3c32a870ae..ad477faa94 100644 --- a/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js +++ b/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js @@ -39,7 +39,6 @@ const styles = { borderColor: colors.grey400, borderWidth: 1, borderTopWidth: 0, - overflow: 'hidden', '&.open': { animation: 'slidein 200ms normal forwards ease-in-out', transformOrigin: '50% 0%', diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js deleted file mode 100644 index 4c68ceea25..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow -import React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { spacers } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core'; -import { LinkButton } from '../../../Buttons/LinkButton.component'; -import { NewTEIRelationshipStatuses } from '../../WidgetTrackedEntityRelationship.const'; - -const styles = { - container: { - padding: `${spacers.dp8} 0`, - }, - slash: { - padding: 5, - }, -}; - -const BreadcrumbsPlain = ({ - selectedRelationshipType, - onResetRelationshipType, - onResetCreationMode, - pageStatus, - classes, -}) => { - const initialText = i18n.t('New TEI Relationship'); - const renderSlash = () => (/); - - return ( -
- {pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE && ( - {initialText} - )} - - {pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE && ( - <> - {initialText} - {renderSlash()} - {selectedRelationshipType.displayName} - - )} - - {pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING && ( - <> - {initialText} - {renderSlash()} - {selectedRelationshipType.displayName} - {renderSlash()} - {'Search'} - - )} -
- ); -}; - -export const Breadcrumbs = withStyles(styles)(BreadcrumbsPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js deleted file mode 100644 index 8a8ea1a37a..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { Button, IconSearch16, IconAdd16, spacers } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core'; -import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; -import { RelationshipTypeSelector } from './RelationshipTypeSelector/RelationshipTypeSelector'; -import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; -import { TeiSearch } from './TeiSearch/TeiSearch.container'; -import { - TeiRelationshipSearchResults, -} from '../../Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; - -const styles = { - container: { - padding: spacers.dp16, - paddingTop: 0, - }, - creationselector: { - display: 'flex', - gap: spacers.dp4, - }, - cancelButton: { - marginTop: spacers.dp8, - }, -}; - -const NewTrackedEntityRelationshipComponentPlain = ({ pageStatus, onSetCreationMode, addRelationship, classes, ...PassOnProps }) => { - if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { - return ( - - ); - } - - if (pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE) { - return ( -
-
- - -
-
- ); - } - - if (pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING) { - return ( -
- ( - - )} - /> -
- ); - } - - return

{i18n.t('An error occurred')}

; -}; - -export const NewTrackedEntityRelationshipComponent = withStyles(styles)(NewTrackedEntityRelationshipComponentPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js deleted file mode 100644 index ee4c36ee79..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ /dev/null @@ -1,156 +0,0 @@ -// @flow -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import * as ReactDOM from 'react-dom'; -import { withStyles } from '@material-ui/core'; -import i18n from '@dhis2/d2-i18n'; -import { Widget } from '../../Widget'; -import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; -import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; -import type { Props } from './NewTrackedEntityRelationship.types'; -import { LinkButton } from '../../Buttons/LinkButton.component'; -import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'; -import { useLocationQuery } from '../../../utils/routing'; -import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; -import { requestSaveRelationshipForTei, startTeiSearchForWidget } from './NewTrackedEntityRelationship.actions'; - -const styles = { - container: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: '#FAFAFA', - }, - bar: { - color: '#494949', - padding: '8px', - display: 'inline-block', - fontSize: '14px', - borderRadius: '4px', - marginBottom: '10px', - backgroundColor: '#E9EEF4', - }, - linkText: { - backgroundColor: 'transparent', - fontSize: 'inherit', - color: 'inherit', - }, -}; - -export const NewTrackedEntityRelationshipPlain = ({ - renderRef, - showDialog, - setShowDialog, - hideDialog, - relationshipTypes, - trackedEntityType, - classes, - ...passOnProps -}: Props) => { - const [selectedRelationshipType, setSelectedRelationshipType] = useState(); - const [creationMode, setCreationMode] = useState(); - const { programId, teiId } = useLocationQuery(); - const dispatch = useDispatch(); - - const handleAddRelationship = useCallback((linkedTei) => { - if (selectedRelationshipType) { - const { - constraintSide, - id, - } = selectedRelationshipType; - const linkedTeiConstraintSide: string = constraintSide !== 'from' ? 'from' : 'to'; - - const serverData = { - relationships: [{ - relationshipType: id, - [constraintSide]: { - trackedEntity: teiId, - }, - [linkedTeiConstraintSide]: { - trackedEntity: linkedTei, - }, - }], - }; - - dispatch(requestSaveRelationshipForTei({ serverData })); - setShowDialog(); - } - }, [dispatch, selectedRelationshipType, setShowDialog, teiId]); - - const pageStatus = useMemo(() => { - if (!selectedRelationshipType) { - return NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE; - } - if (!creationMode) { - return NewTEIRelationshipStatuses.MISSING_CREATION_MODE; - } - if (creationMode === creationModeStatuses.SEARCH) { - return NewTEIRelationshipStatuses.LINK_TO_EXISTING; - } - return NewTEIRelationshipStatuses.DEFAULT; - }, [creationMode, selectedRelationshipType]); - - const onSelectRelationshipType = useCallback( - relationshipType => setSelectedRelationshipType(relationshipType), - []); - - const onCancel = useCallback(() => { - hideDialog(); - setSelectedRelationshipType(); - setCreationMode(); - }, [hideDialog]); - - const onResetRelationshipType = useCallback(() => { - setSelectedRelationshipType(); - setCreationMode(); - }, []); - - const onResetCreationMode = useCallback(() => { - setCreationMode(); - }, []); - - const onSetCreationMode = useCallback((value) => { - setCreationMode(value); - dispatch(startTeiSearchForWidget({ selectedRelationshipType })); - }, [dispatch, selectedRelationshipType]); - - if (!showDialog || !renderRef.current) { - return null; - } - - return ReactDOM.createPortal(( - <> -
-
- - {i18n.t('Go back without saving relationship')} - -
- } - > - - -
- - ), renderRef.current); -}; - -export const NewTrackedEntityRelationship = withStyles(styles)(NewTrackedEntityRelationshipPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js deleted file mode 100644 index df39ba36ed..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow - -import type { RelationshipType } from '../WidgetTrackedEntityRelationship.types'; - -export type Props = {| - renderRef: Object, - relationshipTypes: Array, - setShowDialog: () => void, - trackedEntityType: string, - showDialog: boolean, - hideDialog: () => void, - ...CssClasses, -|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipSelectorRow/RelationshipSelectorRow.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipSelectorRow/RelationshipSelectorRow.js deleted file mode 100644 index 1543c5494e..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipSelectorRow/RelationshipSelectorRow.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow -import React from 'react'; -import { withStyles } from '@material-ui/core'; -import { Button, spacers } from '@dhis2/ui'; - -const styles = { - selectorButton: { - display: 'flex', - flexDirection: 'column', - gap: spacers.dp4, - marginBottom: spacers.dp8, - }, - title: { - fontWeight: 500, - marginBottom: spacers.dp4, - }, - buttonContainer: { - display: 'flex', - gap: spacers.dp8, - }, -}; - -const RelationshipSelectorRowPlain = ({ relationship, onSelectType, classes }) => ( -
-
- {relationship.displayName} -
-
-
- {relationship.fromConstraint && ( - - )} - {relationship.toConstraint && ( - - )} -
-
-
-); - -export const RelationshipSelectorRow = withStyles(styles)(RelationshipSelectorRowPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js deleted file mode 100644 index ba89b0a619..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RelationshipTypeSelector/RelationshipTypeSelector.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow -import React from 'react'; -import { spacers } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core'; -import { useFilteredRelationshipTypes, useRelationshipsForCurrentTEI } from '../../hooks'; -import { RelationshipSelectorRow } from '../RelationshipSelectorRow/RelationshipSelectorRow'; - -const styles = { - container: { - padding: spacers.dp16, - paddingTop: 0, - }, - typeSelector: { - display: 'flex', - flexDirection: 'column', - gap: spacers.dp8, - marginBottom: spacers.dp16, - }, -}; - -const RelationshipTypeSelectorPlain = ({ relationshipTypes, trackedEntityType, programId, onSelectType, classes }) => { - const filteredRelationshipTypes = useFilteredRelationshipTypes(relationshipTypes, trackedEntityType, programId); - const relationshipsForCurrentTEI = useRelationshipsForCurrentTEI({ - relationshipTypes: filteredRelationshipTypes, - programId, - trackedEntityType, - }); - - return ( -
-
- {relationshipsForCurrentTEI.map(relationship => ( - - ))} -
-
- ); -}; - -export const RelationshipTypeSelector = withStyles(styles)(RelationshipTypeSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js deleted file mode 100644 index 6cca69cf45..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.const.js +++ /dev/null @@ -1,7 +0,0 @@ -export const NewTEIRelationshipStatuses = Object.freeze({ - DEFAULT: 'Default', - DATA_ENTRY_NEW_RECORD: 'DataEntryNewRecord', - LINK_TO_EXISTING: 'LinkToExisting', - MISSING_RELATIONSHIP_TYPE: 'MissingRelationshipType', - MISSING_CREATION_MODE: 'MissingCreationMode', -}); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js deleted file mode 100644 index 11f8683826..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow -import React, { useCallback, useState } from 'react'; -import { Button } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; -import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship/NewTrackedEntityRelationship.container'; -import type { Props } from './WidgetTrackedEntityRelationship.types'; - -export const WidgetTrackedEntityRelationship = ({ ...PassOnProps }: Props) => { - const [showDialog, setShowDialog] = useState(false); - - const changeDialogView = useCallback(() => { - setShowDialog(prevState => !prevState); - }, []); - - return ( - <> - - - - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js deleted file mode 100644 index 3abade5297..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow - -type Constraint = {| - program?: { id: string }, - trackedEntityType?: { id: string }, -|} - -export type RelationshipType = {| - id: string, - displayName: string, - bidirectional: boolean, - fromToName: string, - toFromName?: string, - fromConstraint: Constraint, - toConstraint: Constraint, -|} - -export type Props = {| - relationshipTypes: ?Array, - renderRef: Object, - trackedEntityType: string, - programId: string, -|} - -export type RelationshipsForCurrentTEI = {| - relationshipTypes: Array, - programId: string, - trackedEntityType: string, -|} diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js deleted file mode 100644 index 3b9faee424..0000000000 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/hooks.js +++ /dev/null @@ -1,118 +0,0 @@ -// @flow -import { useMemo, useRef, useState } from 'react'; -import { - getCachedResourceAsync, -} from '../../MetaDataStoreUtils/MetaDataStoreUtils'; -import { userStores } from '../../storageControllers/stores'; -import type { RelationshipsForCurrentTEI, RelationshipType } from './WidgetTrackedEntityRelationship.types'; - -export const useRelationshipTypes = () => { - const [cachedTypes, setCachedTypes] = useState(); - const [loading, setLoading] = useState(true); - const error = useRef(); - useMemo(() => { - getCachedResourceAsync(userStores.RELATIONSHIP_TYPES) - .then((relationshipTypes) => { - setCachedTypes(relationshipTypes?.response); - setLoading(false); - }) - .catch((e) => { - console.error(e); - }); - }, []); - - return { - loading, - error: error.current, - relationshipTypes: cachedTypes, - }; -}; - -export const useFilteredRelationshipTypes = (relationshipTypes: Array, trackedEntityType: string, programId: string) => - // $FlowFixMe - useMemo(() => relationshipTypes?.filter((type) => { - const { toConstraint, fromConstraint } = type; - if (type.bidirectional) { - if (toConstraint.trackedEntityType?.id === trackedEntityType || fromConstraint.trackedEntityType?.id === trackedEntityType) { - let fromConstraintValid = true; - let toConstraintValid = true; - if (fromConstraint.program) { - if ((fromConstraint.program?.id !== programId) || (fromConstraint.trackedEntityType?.id !== trackedEntityType)) { - fromConstraintValid = false; - } - } - if (toConstraint.program) { - if ((toConstraint.program?.id !== programId) || (toConstraint.trackedEntityType?.id !== trackedEntityType)) { - toConstraintValid = false; - } - } - - if (fromConstraintValid || toConstraintValid) { - return true; - } - return false; - } - return false; - } - if (fromConstraint.trackedEntityType?.id === trackedEntityType) { - if (fromConstraint.program) { - if (fromConstraint.program.id !== programId) { - return false; - } - } - return true; - } - return false; - }), [relationshipTypes, programId, trackedEntityType]); - -export const useRelationshipsForCurrentTEI = ({ relationshipTypes, programId, trackedEntityType }: RelationshipsForCurrentTEI) => - useMemo(() => relationshipTypes.reduce((acc, relationship) => { - const newRelationship: any = { - id: relationship.id, - displayName: relationship.displayName, - }; - - if (relationship.bidirectional) { - if (relationship.fromConstraint.trackedEntityType?.id === trackedEntityType) { - if (relationship.fromConstraint?.program) { - if (relationship.fromConstraint?.program?.id === programId) { - newRelationship.fromConstraint = { - ...relationship.fromConstraint, - program: relationship.fromConstraint.program, - displayName: relationship.fromToName, - }; - } - } else { - newRelationship.fromConstraint = { - ...relationship.fromConstraint, - displayName: relationship.fromToName, - }; - } - } - - if (relationship.toConstraint?.trackedEntityType?.id === trackedEntityType) { - if (relationship.toConstraint?.program) { - if (relationship.toConstraint?.program?.id === programId) { - newRelationship.toConstraint = { - ...relationship.toConstraint, - program: relationship.toConstraint.program, - displayName: relationship.toFromName, - }; - } - } else { - newRelationship.toConstraint = { - ...relationship.toConstraint, - displayName: relationship.toFromName, - }; - } - } - } else { - newRelationship.fromConstraint = { - ...relationship.fromConstraint, - displayName: relationship.fromToName, - }; - } - - acc.push(newRelationship); - return acc; - }, []), [programId, relationshipTypes, trackedEntityType]); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js new file mode 100644 index 0000000000..678e19a88e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js @@ -0,0 +1,76 @@ +// @flow +import React, { type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { LinkButton } from '../../../../Buttons/LinkButton.component'; +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; +import type { PlainProps, Props } from './breadcrumbs.types'; + +const styles = { + container: { + padding: `${spacers.dp8} 0`, + }, +}; + +const Slash = withStyles({ slash: { padding: 5 } })(({ classes }) => /); + +const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate }) => { + const initialText = i18n.t('New TEI Relationship'); + return (currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.value ? + {initialText} : + {initialText}); +}; + +const RetrieverModeStep = ({ currentStep, onNavigate, linkedEntityMetadataName }) => { + if (currentStep.value < NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.value) { + return null; + } + + return ( + <> + + {currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.value ? + {linkedEntityMetadataName} : + {linkedEntityMetadataName}} + + ); +}; + +const FindExistingStep = ({ currentStep }) => { + if (currentStep.value < NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.value) { + return null; + } + + return ( + <> + + {'Search'} + + ); +}; + +const BreadcrumbsPlain = ({ + currentStep, + onNavigate, + linkedEntityMetadataName, + classes, +}: PlainProps) => ( +
+ + onNavigate(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA)} + /> + onNavigate(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE)} + /> + +
+); + +export const Breadcrumbs: ComponentType = withStyles(styles)(BreadcrumbsPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js new file mode 100644 index 0000000000..5f59de427b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js @@ -0,0 +1,13 @@ +// @flow +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; + +export type Props = {| + currentStep: $Values, + onNavigate: ($Values) => void, + linkedEntityMetadataName?: string, +|}; + +export type PlainProps = {| + ...Props, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js new file mode 100644 index 0000000000..fb78354934 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js @@ -0,0 +1,2 @@ +// @flow +export { Breadcrumbs } from './Breadcrumbs.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js new file mode 100644 index 0000000000..286f7ea49e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js @@ -0,0 +1,28 @@ +// @flow +import React from 'react'; + +import { useApplicableTypesAndSides } from './useApplicableTypesAndSides'; +import { + LinkedEntityMetadataSelector, + type LinkedEntityMetadataSelectorType, +} from '../../../common/LinkedEntityMetadataSelector'; +import type { Props, Side, LinkedEntityMetadata } from './linkedEntityMetadataSelector.types'; + +export const LinkedEntityMetadataSelectorFromTrackedEntity = ({ + relationshipTypes, + trackedEntityTypeId, + programId, + onSelectLinkedEntityMetadata, +}: Props) => { + const applicableTypesInfo = useApplicableTypesAndSides(relationshipTypes, trackedEntityTypeId, [programId]); + + const LinkedEntityMetadataSelectorCommon: LinkedEntityMetadataSelectorType = + LinkedEntityMetadataSelector; + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js new file mode 100644 index 0000000000..0f70e74218 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js @@ -0,0 +1,3 @@ +// @flow +export { LinkedEntityMetadataSelectorFromTrackedEntity } from './LinkedEntityMetadataSelector.component'; +export type { LinkedEntityMetadata, RelationshipTypes, Side } from './linkedEntityMetadataSelector.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js new file mode 100644 index 0000000000..0014fc3933 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -0,0 +1,67 @@ +// @flow +import type { TargetSides } from '../MOVE_WidgetsCommon'; + +type TeiConstraint = $ReadOnly<{ + relationshipEntity: 'TRACKED_ENTITY_INSTANCE', + program?: { id: string }, + trackedEntityType: { id: string }, +}>; + +type OtherConstraint = $ReadOnly<{ + relationshipEntity: 'PROGRAM_STAGE_INSTANCE' | 'PROGRAM_INSTANCE', +}>; + +type RelationshipTypeUnidirectional = $ReadOnly<{ + id: string, + displayName: string, + bidirectional: false, + fromToName: string, + toFromName: void, + fromConstraint: TeiConstraint | OtherConstraint, + toConstraint: TeiConstraint | OtherConstraint, +}>; + +type RelationshipTypeBidirectional = $ReadOnly<{ + id: string, + displayName: string, + bidirectional: true, + fromToName: string, + toFromName: string, + fromConstraint: TeiConstraint | OtherConstraint, + toConstraint: TeiConstraint | OtherConstraint, +}>; + +export type RelationshipType = RelationshipTypeUnidirectional | RelationshipTypeBidirectional; + +export type RelationshipTypes = $ReadOnlyArray; + +export type Side = $ReadOnly<{| + trackedEntityTypeId: string, + programId?: string, + name: string, + targetSide: TargetSides, +|}>; + +export type ApplicableTypeInfo = $ReadOnly<{| + id: string, + name: string, + sides: $ReadOnlyArray, +|}>; + +export type ApplicableTypesInfo = $ReadOnlyArray; + +export type LinkedEntityMetadata = $ReadOnly<{| + trackedEntityTypeId: string, + programId: string, + name: string, + targetSide: TargetSides, + relationshipId: string, +|}>; + +export type Props = $ReadOnly<{| + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: LinkedEntityMetadata) => void, + +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js new file mode 100644 index 0000000000..559b894d81 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -0,0 +1,168 @@ +// @flow +import { useMemo } from 'react'; +import { RELATIONSHIP_ENTITIES } from '../../common'; +import { TARGET_SIDES } from '../common'; +import type { TargetSides } from '../MOVE_WidgetsCommon'; +import type { RelationshipType, ApplicableTypesInfo } from './linkedEntityMetadataSelector.types'; + +const isApplicableProgram = (programId, sourceProgramIds) => + (!sourceProgramIds || !programId || sourceProgramIds.includes(programId)); + +const isApplicableUnidirectionalRelationshipType = ( + { trackedEntityType, program }, + sourceTrackedEntityTypeId, + sourceProgramIds, +) => { + const trackedEntityTypeId = trackedEntityType.id; + const programId = program?.id; + + return Boolean( + sourceTrackedEntityTypeId === trackedEntityTypeId && + isApplicableProgram(programId, sourceProgramIds), + ); +}; + +const computeTargetSidesDualMatchingTET = (() => { + const getProgramMatchInfo = (sourceProgramIds, programId) => { + if (!programId) { + return { noProgram: true, programMatch: false }; + } + + if (sourceProgramIds.includes(programId)) { + return { programMatch: true, noProgram: false }; + } + + return { programMatch: false, noProgram: false }; + }; + + return (sourceProgramIds, fromProgramId, toProgramId): Array => { + if (!sourceProgramIds) { + return [TARGET_SIDES.FROM, TARGET_SIDES.TO]; + } + const { programMatch: fromProgramMatch, noProgram: fromNoProgram } = + getProgramMatchInfo(sourceProgramIds, fromProgramId); + const { programMatch: toProgramMatch, noProgram: toNoProgram } = + getProgramMatchInfo(sourceProgramIds, toProgramId); + + if (fromProgramMatch) { + return [TARGET_SIDES.TO]; + } else if (toProgramMatch) { + return [TARGET_SIDES.FROM]; + } else if (fromNoProgram && toNoProgram) { + return [TARGET_SIDES.FROM, TARGET_SIDES.TO]; + } else if (fromNoProgram) { + return [TARGET_SIDES.TO]; + } else if (toNoProgram) { + return [TARGET_SIDES.FROM]; + } + + return []; + }; +})(); + +const getApplicableTargetSidesForBidirectionalRelationshipType = ({ + fromConstraint, + toConstraint, +}, sourceTrackedEntityTypeId, sourceProgramIds): Array => { + const { trackedEntityType: fromTrackedEntityType } = fromConstraint; + const { trackedEntityType: toTrackedEntityType } = toConstraint; + + if (fromTrackedEntityType.id === sourceTrackedEntityTypeId && + toTrackedEntityType.id === sourceTrackedEntityTypeId) { + return computeTargetSidesDualMatchingTET( + sourceProgramIds, + fromConstraint.program?.id, + toConstraint.program?.id, + ); + } + + if (fromTrackedEntityType.id === sourceTrackedEntityTypeId || + toTrackedEntityType.id === sourceTrackedEntityTypeId) { + const { programId, targetSide } = fromTrackedEntityType.id === sourceTrackedEntityTypeId ? + { programId: fromConstraint.program?.id, targetSide: TARGET_SIDES.TO } : + { programId: toConstraint.program?.id, targetSide: TARGET_SIDES.FROM }; + return isApplicableProgram(programId, sourceProgramIds) ? [targetSide] : []; + } + + return []; +}; + +export const useApplicableTypesAndSides = ( + relationshipTypes: $ReadOnlyArray, + sourceTrackedEntityTypeId: string, + sourceProgramIds: $ReadOnlyArray): ApplicableTypesInfo => + useMemo(() => + relationshipTypes.map(({ + fromConstraint, + toConstraint, + bidirectional, + id, + displayName, + fromToName, + toFromName, + }) => { + if (fromConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE && + toConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + if (!bidirectional) { + const applicable = isApplicableUnidirectionalRelationshipType( + fromConstraint, + sourceTrackedEntityTypeId, + sourceProgramIds, + ); + + if (!applicable) { + // $FlowFixMe filter + return null; + } + const { trackedEntityType, program } = toConstraint; + return { + id, + name: displayName, + sides: [{ + programId: program?.id, + trackedEntityTypeId: trackedEntityType.id, + targetSide: TARGET_SIDES.TO, + name: fromToName, + }], + }; + } + + const targetSides = getApplicableTargetSidesForBidirectionalRelationshipType( + { fromConstraint, toConstraint }, + sourceTrackedEntityTypeId, + sourceProgramIds, + ); + + if (!targetSides.length) { + // $FlowFixMe filter + return null; + } + + return { + id, + name: displayName, + sides: targetSides.map((targetSide) => { + const { trackedEntityTypeId, programId, name } = targetSide === TARGET_SIDES.TO ? { + trackedEntityTypeId: toConstraint.trackedEntityType.id, + programId: toConstraint.program?.id, + name: fromToName, + } : { + trackedEntityTypeId: fromConstraint.trackedEntityType.id, + programId: fromConstraint.program?.id, + name: toFromName, + }; + + return { + trackedEntityTypeId, + programId, + targetSide, + // $FlowFixMe + name, + }; + }), + }; + } + // $FlowFixMe filter + return null; + }).filter(applicableType => applicableType), + [relationshipTypes, sourceTrackedEntityTypeId, sourceProgramIds]); diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js similarity index 99% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js index 516210c91b..17f08c03c8 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js @@ -1,3 +1,4 @@ +/* import { actionCreator } from '../../../actions/actions.utils'; import { effectMethods } from '../../../trackerOffline'; @@ -20,3 +21,4 @@ export const requestSaveRelationshipForTei = ({ serverData }) => }, }, }); +*/ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js new file mode 100644 index 0000000000..9af8b91f57 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -0,0 +1,210 @@ +// @flow +import React, { useCallback, useState, type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../../Widget'; +import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { Breadcrumbs } from './Breadcrumbs'; +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from './wizardSteps.const'; +import { + LinkedEntityMetadataSelectorFromTrackedEntity, + type LinkedEntityMetadata, +} from './LinkedEntityMetadataSelector'; +import { RetrieverModeSelector } from './RetrieverModeSelector'; +import type { Props, PlainProps } from './NewTrackedEntityRelationship.types'; +import { TrackedEntityFinder } from './TrackedEntityFinder'; + +/* +import { Button, IconSearch16, IconAdd16, spacers } from '@dhis2/ui'; +import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; +import { RelationshipTypeSelector } from './RelationshipTypeSelector/RelationshipTypeSelector'; +import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; +import { TeiSearch } from './TeiSearch/TeiSearch.container'; +import { + TeiRelationshipSearchResults, +} from '../../Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; +*/ + +const styles = { + container: { + backgroundColor: '#FAFAFA', + maxWidth: 900, + }, + bar: { + color: '#494949', + padding: '8px', + display: 'inline-block', + fontSize: '14px', + borderRadius: '4px', + marginBottom: '10px', + backgroundColor: '#E9EEF4', + }, + linkText: { + backgroundColor: 'transparent', + fontSize: 'inherit', + color: 'inherit', + }, +}; + +const NewTrackedEntityRelationshipPlain = ({ + relationshipTypes, + trackedEntityTypeId, + programId, + // onSave, + onCancel, + getPrograms, + getSearchGroups, + getSearchGroupsAsync, + classes, +}: PlainProps) => { + const [currentStep, setCurrentStep] = + useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); + const [selectedLinkedEntityMetadata: LinkedEntityMetadata, setSelectedLinkedEntityMetadata] = useState(undefined); + + const handleNavigation = useCallback( + (destination: $Values) => { + setCurrentStep(destination); + }, []); + + const handleLinkedEntityMetadataSelection = useCallback((linkedEntityMetadata: LinkedEntityMetadata) => { + setSelectedLinkedEntityMetadata(linkedEntityMetadata); + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE); + }, []); + + const handleSearchRetrieverModeSelected = useCallback(() => + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY), []); + const handleNewRetrieverModeSelected = useCallback(() => + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY), []); + + const stepContents = { + [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.id]: () => ( + + ), + [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id]: () => ( + + ), + [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id]: () => { + const { + trackedEntityTypeId: linkedEntityTrackedEntityTypeId, + programId: linkedEntityProgramId, + // $FlowFixMe business logic dictates that we will have the linkedEntityMetadata at this step + }: LinkedEntityMetadata = selectedLinkedEntityMetadata; + + return ( + + ); + }, + }; + + return ( +
+
+ + {i18n.t('Go back without saving relationship')} + +
+ } + > + {stepContents[currentStep.id]()} + +
+ ); +}; + +export const NewTrackedEntityRelationshipComponent: ComponentType = + withStyles(styles)(NewTrackedEntityRelationshipPlain); + +/* + +const NewTrackedEntityRelationshipComponentPlain = (props) => { + if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { + return ( + + ); + } + + if (pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE) { + return ( +
+
+ + +
+
+ ); + } + + if (pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING) { + return ( +
+ ( + + )} + /> +
+ ); + } + + return

{i18n.t('An error occurred')}

; +}; + +export const NewTrackedEntityRelationshipComponent = withStyles(styles)(NewTrackedEntityRelationshipComponentPlain); + + const handleAddRelationship = useCallback((linkedTei) => { + if (selectedRelationshipType) { + const { + constraintSide, + id, + } = selectedRelationshipType; + const linkedTeiConstraintSide: string = constraintSide !== 'from' ? 'from' : 'to'; + + const serverData = { + relationships: [{ + relationshipType: id, + [constraintSide]: { + trackedEntity: teiId, + }, + [linkedTeiConstraintSide]: { + trackedEntity: linkedTei, + }, + }], + }; + +*/ diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js similarity index 99% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js index b1212dd663..cf067ce6f1 100644 --- a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js @@ -1,5 +1,5 @@ // @flow - +/* import { ofType } from 'redux-observable'; import { batchActions } from 'redux-batched-actions'; import { map } from 'rxjs/operators'; @@ -35,3 +35,4 @@ export const openRelationshipTeiSearchForWidgetEpic = (action$: InputObservable) ], batchActionTypes.BATCH_OPEN_TEI_SEARCH); })); +*/ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js new file mode 100644 index 0000000000..df67f4476e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js @@ -0,0 +1,10 @@ +// @flow +import React from 'react'; +import ReactDOM from 'react-dom'; +import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; +import type { PortalProps } from './NewTrackedEntityRelationship.types'; + +export const NewTrackedEntityRelationship = ({ renderElement, ...passOnProps }: PortalProps) => + ReactDOM.createPortal(( + + ), renderElement); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..5f3b572e32 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -0,0 +1,25 @@ +// @flow +import type { RelationshipTypes } from './LinkedEntityMetadataSelector'; +import type { GetPrograms } from './common'; +import type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; + +export type Props = $ReadOnly<{| + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onSave: () => void, + onCancel: () => void, + getPrograms: GetPrograms, + getSearchGroups?: GetSearchGroups, + getSearchGroupsAsync?: GetSearchGroupsAsync, +|}>; + +export type PlainProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; + +export type PortalProps = $ReadOnly<{| + renderElement: HTMLElement, + ...Props, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js new file mode 100644 index 0000000000..590878829b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js @@ -0,0 +1,40 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { Button, IconSearch16, IconAdd16, spacersNum, spacers } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import type { PlainProps, Props } from './retrieverModeSelector.types'; + +const styles = { + container: { + padding: spacersNum.dp16, + paddingTop: 0, + }, + retrieverSelector: { + display: 'flex', + gap: spacers.dp8, + }, +}; + +const RetrieverModeSelectorPlain = ({ classes, onSearchSelected, onNewSelected }: PlainProps) => ( +
+
+ + +
+
+); + +export const RetrieverModeSelector: ComponentType = withStyles(styles)(RetrieverModeSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js new file mode 100644 index 0000000000..f0d9af5395 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js @@ -0,0 +1,2 @@ +// @flow +export { RetrieverModeSelector } from './RetrieverModeSelector.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js new file mode 100644 index 0000000000..88aeddf037 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js @@ -0,0 +1,11 @@ +// @flow + +export type Props = $ReadOnly<{| + onSearchSelected: () => void, + onNewSelected: () => void, +|}>; + +export type PlainProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/SearchProgramSelector/searchProgramSelector.selectors.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.container.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearch.types.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchForm/TeiSearchForm.container.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.container.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/TeiSearchResults/TeiSearchResults.types.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/actions/teiSearch.actions.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/getSearchGroups.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js diff --git a/src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearch/teiSearch.selectors.js rename to src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js new file mode 100644 index 0000000000..59a17915bf --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js @@ -0,0 +1,39 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { colors, spacersNum } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../../../../Widget'; +import { ProgramSelectorForTrackedEntityFinder } from './ProgramSelector.component'; + +const styles = { + contents: { + marginLeft: spacersNum.dp16, + marginRight: spacersNum.dp16, + borderTop: `1px solid ${colors.grey400}`, + }, +}; + +const ProgramSectionPlain = ({ trackedEntityTypeId, selectedProgramId, onSelectProgram, getPrograms, classes }) => { + const [open, setOpenStatus] = useState(true); + const toggleOpen = useCallback(() => setOpenStatus(currentStatus => !currentStatus), []); + return ( + +
+ +
+
+ ); +}; + +export const ProgramSection = withStyles(styles)(ProgramSectionPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js new file mode 100644 index 0000000000..6fc9d38599 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js @@ -0,0 +1,42 @@ +// @flow +import React, { useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + OptionsSelectVirtualized, +} from '../../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; +import { withDefaultFieldContainer, withLabel } from '../../../../../FormFields/New'; +import type { Props } from './programSelector.types'; + +const ProgramSelector = withDefaultFieldContainer()(withLabel()(OptionsSelectVirtualized)); + +const programFieldStyles = { + labelContainerStyle: { + paddingTop: 12, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +export const ProgramSelectorForTrackedEntityFinder = ({ + selectedProgramId, + onSelectProgram, + getPrograms, + trackedEntityTypeId, +}: Props) => { + const options = useMemo(() => (getPrograms ? + getPrograms(trackedEntityTypeId) + .map(({ id, name }) => ({ label: name, value: id })) : []), + [getPrograms, trackedEntityTypeId]); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js new file mode 100644 index 0000000000..6f046d4253 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js @@ -0,0 +1,2 @@ +// @flow +export { ProgramSection } from './ProgramSection.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js new file mode 100644 index 0000000000..3af375c7a6 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js @@ -0,0 +1,9 @@ +// @flow +import type { GetPrograms } from '../../common'; + +export type Props = $ReadOnly<{| + selectedProgramId: ?string, + onSelectProgram: (programId: ?string) => void, + trackedEntityTypeId: string, + getPrograms: GetPrograms, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js new file mode 100644 index 0000000000..660f6b3c6f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js @@ -0,0 +1,31 @@ +// @flow + +import React from 'react'; +import { ErrorHandler, LoadingHandler } from 'capture-ui'; +import { useSearchGroups } from './useSearchGroups'; +import type { Props } from './searchFormSection.types'; + +const NextComp = ({ programId }) => ( +
+ {programId} +
+); + +export const SearchFormSection = ({ programId, getSearchGroups, getSearchGroupsAsync }: Props) => { + const { searchGroups, loading, failed } = useSearchGroups(getSearchGroups, getSearchGroupsAsync, programId); + + return ( + + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js new file mode 100644 index 0000000000..ab49382867 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js @@ -0,0 +1,4 @@ +// @flow + +export { SearchFormSection } from './SearchFormSection.component'; +export type { GetSearchGroups, GetSearchGroupsAsync } from './searchFormSection.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js new file mode 100644 index 0000000000..bbdb8c9ba2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js @@ -0,0 +1,23 @@ +// @flow + +type Field = $ReadOnly<{| + id: string, + type: string, +|}>; + +type SearchGroup = $ReadOnly<{| + unique: boolean, + fields: Array, +|}>; + +export type SearchGroups = $ReadOnlyArray; + +export type GetSearchGroups = (programId: string | null) => SearchGroups | void; + +export type GetSearchGroupsAsync = (programId: string | null) => SearchGroups; + +export type Props = $ReadOnly<{| + programId: string | null, + getSearchGroups?: GetSearchGroups, + getSearchGroupsAsync?: GetSearchGroupsAsync, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js new file mode 100644 index 0000000000..a1614e0533 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js @@ -0,0 +1,68 @@ +// @flow +import { useState, useCallback, useLayoutEffect, useRef } from 'react'; +import { makeCancelablePromise } from 'capture-core-utils'; + +type AsyncFn = (args: Array) => any; +type Args = Array; + +export const useCancelableAsyncFn = (asyncFn: AsyncFn, hardLoadMode: boolean, ...args: Args) => { + const [result, setResult] = useState({}); + const [error, setError] = useState(); + const [loading, setLoading] = useState(true); + const hardLoadModeStaticRef = useRef(hardLoadMode); + + const prevArgsRef = useRef(); + const currentTransactionIdRef = useRef(0); + + + if (hardLoadModeStaticRef.current) { + const prevArgs = prevArgsRef.current; + if (args.some((arg, index) => arg !== (prevArgs && prevArgs[index]))) { + prevArgsRef.current = args; + currentTransactionIdRef.current += 1; + } + } + + let cancel; + const execute = useCallback(async () => { + const promise = asyncFn(...args); + if (!(promise instanceof Promise)) { + setResult({ data: promise, transactionId: currentTransactionIdRef.current }); + setLoading(false); + } else { + let cancelablePromise; + ({ promise: cancelablePromise, cancel } = makeCancelablePromise(promise)); // eslint-disable-line + try { + const data = await cancelablePromise; + setResult({ data, transactionId: currentTransactionIdRef.current }); + setLoading(false); + } catch (errorArg) { + if (!errorArg || !errorArg.isCanceled) { + setError('An error occured. Please try again later'); + setLoading(false); + } + } + } + }, [...args]); + + useLayoutEffect(() => { + if (!hardLoadModeStaticRef.current) { + setLoading(true); + } + + if (asyncFn) { + execute(); + } else if (hardLoadModeStaticRef.current) { + setResult({ transactionId: currentTransactionIdRef.current, data: undefined }); + } else { + setLoading(false); + } + return () => cancel && cancel(); + }, [asyncFn, execute, cancel]); + + return { + data: result.data, + error, + loading: hardLoadMode ? currentTransactionIdRef.current !== result.transactionId : loading, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js new file mode 100644 index 0000000000..29479306e3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js @@ -0,0 +1,42 @@ +// @flow +import { useCallback, useMemo } from 'react'; +import { useMetadataApiQuery, useMetadataCustomQuery } from 'capture-core-utils/reactQueryHelpers'; +import type { GetSearchGroups, GetSearchGroupsAsync, SearchGroups } from './searchFormSection.types'; + +export const useSearchGroups = ( + getSearchGroups?: GetSearchGroups, + getSearchGroupsAsync?: GetSearchGroupsAsync, + programId: string | null) => { + const searchGroupsExternal = useMemo(() => + getSearchGroups && getSearchGroups(programId), [getSearchGroups, programId]); + + const getSearchGroupsExternalAsync = useCallback((): SearchGroups => + // $FlowFixMe will never be called if the function is undefined + getSearchGroupsAsync(programId), + [programId, getSearchGroupsAsync]); + + const { data: searchGroupsExternalAsync, loading: loadingExternal, failed: failedExternal } = + useMetadataCustomQuery( + ['WidgetSearchGroupsExternal', programId], + getSearchGroupsExternalAsync, { + enabled: !searchGroupsExternal && Boolean(getSearchGroupsAsync), + }, + ); + + // TODO: Incomplete + const { data: searchGroupsInternal, loading: loadingInternal, failed: failedInternal } = + useMetadataApiQuery( + ['WidgetSearchGroups', programId], { + resource: 'program', + params: { id: '1', fields: 'id' }, + }, { + enabled: !searchGroupsExternal && !getSearchGroupsAsync, + }, + ); + + return { + searchGroups: searchGroupsExternal || searchGroupsExternalAsync || searchGroupsInternal, + loading: loadingExternal || loadingInternal, + failed: failedExternal || failedInternal, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js new file mode 100644 index 0000000000..a86b5df571 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js @@ -0,0 +1,45 @@ +// @flow +import React, { useCallback, useState, type ComponentType } from 'react'; +import { spacersNum } from '@dhis2/ui'; +import withStyles from '@material-ui/core/styles/withStyles'; +import { ProgramSection } from './ProgramSection'; +import type { PlainProps, Props } from './TrackedEntityFinder.types'; +import { SearchFormSection } from './SearchFormSection'; + +const styles = { + container: { + marginLeft: spacersNum.dp16, + marginRight: spacersNum.dp16, + marginBottom: spacersNum.dp16, + }, +}; + +const TrackedEntityFinderPlain = ({ + trackedEntityTypeId, + defaultProgramId = null, + getPrograms, + getSearchGroups, + getSearchGroupsAsync, + classes, +}: PlainProps) => { + const [selectedProgramId, setProgramId] = useState(defaultProgramId); + const handleSelectProgram = useCallback(id => setProgramId(id), []); + + return ( +
+ + +
+ ); +}; + +export const TrackedEntityFinder: ComponentType = withStyles(styles)(TrackedEntityFinderPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js new file mode 100644 index 0000000000..cdbc2abadd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js @@ -0,0 +1,16 @@ +// @flow +import type { GetPrograms } from '../common'; +import type { GetSearchGroups, GetSearchGroupsAsync } from './SearchFormSection'; + +export type Props = $ReadOnly<{| + trackedEntityTypeId: string, + defaultProgramId: ?string, + getPrograms: GetPrograms, + getSearchGroups?: GetSearchGroups, + getSearchGroupsAsync?: GetSearchGroupsAsync, +|}>; + +export type PlainProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js new file mode 100644 index 0000000000..aed16dc829 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js @@ -0,0 +1,4 @@ +// @flow + +export { TrackedEntityFinder } from './TrackedEntityFinder.component'; +export type { GetSearchGroups, GetSearchGroupsAsync } from './SearchFormSection'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js new file mode 100644 index 0000000000..4ebbda87a1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js @@ -0,0 +1,8 @@ +// @flow + +type Program = $ReadOnly<{| + id: string, + name: string, +|}>; + +export type GetPrograms = (trackedEntityTypeId: string) => $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js new file mode 100644 index 0000000000..58de3870b0 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js @@ -0,0 +1,3 @@ +// @flow +export { TARGET_SIDES } from './targetSides'; +export * from './common.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js new file mode 100644 index 0000000000..b97e0d72ab --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js @@ -0,0 +1,6 @@ +// @flow + +export const TARGET_SIDES = Object.freeze({ + FROM: 'FROM', + TO: 'TO', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js new file mode 100644 index 0000000000..390a4ec9db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js @@ -0,0 +1,4 @@ +// @flow +export { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship.portal'; +export type { GetPrograms } from './common'; +export type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js new file mode 100644 index 0000000000..55c01e50de --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js @@ -0,0 +1,6 @@ +export const NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS = Object.freeze({ + SELECT_LINKED_ENTITY_METADATA: Object.freeze({ value: 1, id: 'selectLinkedEntityMetadata' }), + SELECT_RETRIEVER_MODE: Object.freeze({ value: 2, id: 'selectRetrieverMode' }), + FIND_EXISTING_LINKED_ENTITY: Object.freeze({ value: 3, id: 'findExistingLinkedEntity' }), + NEW_LINKED_ENTITY: Object.freeze({ value: 4, id: 'newLinkedEntity' }), +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js new file mode 100644 index 0000000000..1e36ba0d2f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js @@ -0,0 +1,55 @@ +// @flow +import React, { useCallback, useState } from 'react'; +import { Button } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; +import type { Props } from './WidgetTrackedEntityRelationship.types'; + +export const WidgetTrackedEntityRelationship = ({ + relationshipTypes = [], + trackedEntityTypeId, + programId, + addRelationshipRenderElement, + onOpenAddRelationship, + onCloseAddRelationship, + getPrograms, + getSearchGroups, + getSearchGroupsAsync, +}: Props) => { + const [addWizardVisible, setAddWizardVisibility] = useState(false); + + const closeAddWizard = useCallback(() => { + setAddWizardVisibility(false); + onCloseAddRelationship && onCloseAddRelationship(); + }, [onCloseAddRelationship]); + + const openAddWizard = useCallback(() => { + setAddWizardVisibility(true); + onOpenAddRelationship && onOpenAddRelationship(); + }, [onOpenAddRelationship]); + + return ( + <> + + { + addWizardVisible && + + } + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..f5c1ab4e0e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -0,0 +1,43 @@ +// @flow +import type { GetPrograms, GetSearchGroups, GetSearchGroupsAsync } from './NewTrackedEntityRelationship'; + +type Constraint = {| + program?: { id: string }, + trackedEntityType?: { id: string }, +|} + +export type RelationshipType = $ReadOnly<{| + id: string, + displayName: string, + bidirectional: boolean, + fromToName: string, + toFromName?: string, + fromConstraint: Constraint, + toConstraint: Constraint, +|}>; + +export type RelationshipTypes = Array; + +export type Props = {| + relationshipTypes?: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + addRelationshipRenderElement: HTMLElement, + onOpenAddRelationship?: () => void, + onCloseAddRelationship?: () => void, + /* + Advanced props for metadata, at some point the Widget should work without these + These are callbacks so we avoid compution before the data is actually needed, the disadvantage is that these will not automatially re-render inner components if there are changes (not needed in our app) + Obvious workarounds if needed is to add a trigger prop + We might also want to implement async versions of these (async callbacks should be called from useEffects) + */ + getPrograms: GetPrograms, + getSearchGroups?: GetSearchGroups, + getSearchGroupsAsync?: GetSearchGroupsAsync, +|} + +export type RelationshipsForCurrentTEI = {| + relationshipTypes: Array, + programId: string, + trackedEntityType: string, +|} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js new file mode 100644 index 0000000000..ee04405717 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js @@ -0,0 +1,2 @@ +// @flow +export { RELATIONSHIP_ENTITIES } from './relationshipEntities'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js new file mode 100644 index 0000000000..fc528b72e5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js @@ -0,0 +1,7 @@ +// @flow + +export const RELATIONSHIP_ENTITIES = Object.freeze({ + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js new file mode 100644 index 0000000000..0a932a6f5d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js @@ -0,0 +1,29 @@ +// @flow +// TODO: Move / get rid +import { useMemo, useRef, useState } from 'react'; +import { + getCachedResourceAsync, +} from '../../../MetaDataStoreUtils/MetaDataStoreUtils'; +import { userStores } from '../../../storageControllers/stores'; + +export const useRelationshipTypes = () => { + const [cachedTypes, setCachedTypes] = useState(); + const [loading, setLoading] = useState(true); + const error = useRef(); + useMemo(() => { + getCachedResourceAsync(userStores.RELATIONSHIP_TYPES) + .then((relationshipTypes) => { + setCachedTypes(relationshipTypes?.response); + setLoading(false); + }) + .catch((e) => { + console.error(e); + }); + }, []); + + return { + loading, + error: error.current, + relationshipTypes: cachedTypes, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js new file mode 100644 index 0000000000..f1c0a93392 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js @@ -0,0 +1,4 @@ +// @flow +export { WidgetTrackedEntityRelationship } from './WidgetTrackedEntityRelationship.component'; +export type { GetPrograms } from './NewTrackedEntityRelationship'; +export type { RelationshipType, RelationshipTypes } from './WidgetTrackedEntityRelationship.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js new file mode 100644 index 0000000000..98c34a8cb6 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js @@ -0,0 +1,73 @@ +// @flow +import React from 'react'; +import { Button, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import type { PlainProps, LinkedEntityMetadata, Side } from './linkedEntityMetadataSelector.types'; + +const styles = { + container: { + padding: spacers.dp16, + paddingTop: 0, + }, + typeSelector: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp8, + marginBottom: spacers.dp16, + }, + selectorButton: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp4, + marginBottom: spacers.dp8, + }, + title: { + fontWeight: 500, + marginBottom: spacers.dp4, + }, + buttonContainer: { + display: 'flex', + gap: spacers.dp8, + }, +}; + +export const LinkedEntityMetadataSelectorPlain = ({ + applicableTypesInfo, + onSelectLinkedEntityMetadata, + classes }: PlainProps) => ( +
+
+ {applicableTypesInfo.map(({ id, name, sides }) => ( +
+
+ {name} +
+
+
+ {sides.map((side: TSide) => ( + + ))} +
+
+
+ ))} +
+
+ ); + + +export const LinkedEntityMetadataSelector = + withStyles(styles)(LinkedEntityMetadataSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js new file mode 100644 index 0000000000..3468a522b9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js @@ -0,0 +1,3 @@ +// @flow +export { LinkedEntityMetadataSelector } from './LinkedEntityMetadataSelector.component'; +export type { TargetSides, LinkedEntityMetadataSelectorType } from './linkedEntityMetadataSelector.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js new file mode 100644 index 0000000000..d23c3d34ec --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -0,0 +1,36 @@ +// @flow +import type { ComponentType } from 'react'; + +export type TargetSides = 'FROM' | 'TO'; + +export type Side = $ReadOnly<{ + name: string, + targetSide: TargetSides, +}>; + +type ApplicableTypeInfo = $ReadOnly<{ + id: string, + name: string, + sides: $ReadOnlyArray, +}>; + +type ApplicableTypesInfo = $ReadOnlyArray>; + +export type LinkedEntityMetadata = $ReadOnly<{ + ...Side, + relationshipId: string, +}>; + +export type Props = $ReadOnly<{| + applicableTypesInfo: ApplicableTypesInfo, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: TLinkedEntityMetadata) => void, +|}>; + +export type PlainProps = $ReadOnly<{| + applicableTypesInfo: ApplicableTypesInfo, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: TLinkedEntityMetadata) => void, + ...CssClasses, +|}>; + +export type LinkedEntityMetadataSelectorType = + ComponentType>; diff --git a/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js index 9662edbb1d..460078ab90 100644 --- a/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js @@ -51,7 +51,7 @@ export const organisationUnitRootsDesc = createReducerDescription({ roots: action.payload.roots, }, }), -}, 'organisationUnitRoots'); +}, 'organisationUnitRoots2222'); const removeSearchDataOnResetRegUnit = (state) => { setStoreRoots('regUnit', null); diff --git a/src/core_modules/capture-core/storageControllers/index.js b/src/core_modules/capture-core/storageControllers/index.js index 3ae15d0fed..232ea4f37a 100644 --- a/src/core_modules/capture-core/storageControllers/index.js +++ b/src/core_modules/capture-core/storageControllers/index.js @@ -3,3 +3,4 @@ export { initAsync as initControllersAsync } from './storageControllers'; export { closeAsync as closeControllersAsync } from './storageControllers'; export { getMainController as getMainStorageController } from './storageControllers'; export { getUserController as getUserStorageController } from './storageControllers'; +export { userStores } from './stores'; diff --git a/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js b/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js new file mode 100644 index 0000000000..1236db23eb --- /dev/null +++ b/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js @@ -0,0 +1,25 @@ +// @flow +import React from 'react'; +import css from 'styled-jsx/css'; +import { colors, spacers } from '@dhis2/ui'; +import type { Props } from './errorHandler.types'; + +const style = css` +div { + color: ${colors.red500}; + margin: ${spacers.dp16}; +} +`; + +export const ErrorHandler = ({ error, children }: Props) => { + if (error) { + return ( +
+ {error} + +
+ ); + } + + return children; +}; diff --git a/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js b/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js new file mode 100644 index 0000000000..6a951b8fdc --- /dev/null +++ b/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js @@ -0,0 +1,7 @@ +// @flow +import type { Node } from 'react'; + +export type Props = $ReadOnly<{| + error?: string, + children: Node, +|}>; diff --git a/src/core_modules/capture-ui/ErrorHandler/index.js b/src/core_modules/capture-ui/ErrorHandler/index.js new file mode 100644 index 0000000000..2a4e83e8ca --- /dev/null +++ b/src/core_modules/capture-ui/ErrorHandler/index.js @@ -0,0 +1,3 @@ +// @flow + +export { ErrorHandler } from './ErrorHandler.component'; diff --git a/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js b/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js new file mode 100644 index 0000000000..1d07febb94 --- /dev/null +++ b/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js @@ -0,0 +1,10 @@ +// @flow +import type { Props } from './loadingHandler.types'; + +export const LoadingHandler = ({ loading, children }: Props) => { + if (loading) { + return null; + } + + return children; +}; diff --git a/src/core_modules/capture-ui/LoadingHandler/index.js b/src/core_modules/capture-ui/LoadingHandler/index.js new file mode 100644 index 0000000000..a6954365ec --- /dev/null +++ b/src/core_modules/capture-ui/LoadingHandler/index.js @@ -0,0 +1,3 @@ +// @flow + +export { LoadingHandler } from './LoadingHandler.component'; diff --git a/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js b/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js new file mode 100644 index 0000000000..0028b4280b --- /dev/null +++ b/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js @@ -0,0 +1,7 @@ +// @flow +import type { Node } from 'react'; + +export type Props = $ReadOnly<{| + loading: boolean, + children: Node, +|}>; diff --git a/src/core_modules/capture-ui/index.js b/src/core_modules/capture-ui/index.js index d2a91dde4c..67828f6c2a 100644 --- a/src/core_modules/capture-ui/index.js +++ b/src/core_modules/capture-ui/index.js @@ -45,3 +45,7 @@ export { Button } from './Buttons/Button.component'; export { IconButton } from './IconButton'; export { NonBundledIcon } from './NonBundledIcon'; export { FlatList } from './FlatList'; + +// Generic Handlers +export { ErrorHandler } from './ErrorHandler'; +export { LoadingHandler } from './LoadingHandler'; diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index ebdc606e6d..f692861928 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -223,9 +223,6 @@ import { import { scheduleNewEnrollmentEventEpic, } from '../core_modules/capture-core/components/WidgetEventSchedule'; -import { - openRelationshipTeiSearchForWidgetEpic, -} from '../core_modules/capture-core/components/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics'; export const epics = combineEpics( resetProgramAfterSettingOrgUnitIfApplicableEpic, @@ -276,7 +273,6 @@ export const epics = combineEpics( showRegisteringUnitListIndicatorEpic, openRelationshipTeiSearchEpic, requestRelationshipTeiSearchEpic, - openRelationshipTeiSearchForWidgetEpic, TeiRelationshipNewOrEditSearchEpic, teiSearchEpic, teiSearchChangePageEpic, From 67b827aa402bc392e46a06b8ccfe00babb461f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Thu, 5 Jan 2023 11:24:55 +0100 Subject: [PATCH 70/95] feat: continue relationships --- ...kedEntityRelationshipsWrapper.component.js | 11 +- .../useTEIRelationshipsWidgetMetadata.js | 172 ++++++++++++++---- .../SearchFormSection/useSearchGroups.js | 2 +- .../common/Types/RelationshipTypes.types.js | 25 +++ .../WidgetsRelationship/common/Types/index.js | 3 + .../components/WidgetsRelationship/index.js | 3 + .../storageControllers/cache.types.js | 2 + .../capture-core/storageControllers/index.js | 1 + .../utils/reactQueryHelpers/index.js | 2 +- .../utils/reactQueryHelpers/query/index.js | 2 +- .../query/useMetadataQuery.js | 46 ++++- .../query/useMetadataQuery.types.js | 5 +- 12 files changed, 215 insertions(+), 59 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/index.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index 37a09a563c..0af0ca6c28 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -12,15 +12,9 @@ export const TrackedEntityRelationshipsWrapper = ({ onOpenAddRelationship, onCloseAddRelationship, }: Props) => { - const [a, setA] = useState(); - debugger; - const { getPrograms, relationshipTypes, failed } = useTEIRelationshipsWidgetMetadata(); + const { getPrograms, relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); - const getSearchGroups = useCallback((programId: string) => { - - }, []); - - if (failed) { + if (isError) { return (
{i18n.t('Could not retrieve metadata. Please try again later.')} @@ -33,7 +27,6 @@ export const TrackedEntityRelationshipsWrapper = ({ return ( <> - { +const elementTypes = { + ATTRIBUTE: 'attribute', + DATA_ELEMENT: 'dataElement', +}; + +const mapElementIdsToObject = (elementIds, elements, { relationshipType, elementType }) => { + (elementIds || []) + .map((elementId) => { + const element = elements[elementId]; + + if (!element) { + log.error( + errorCreator(`${elementType} from relationshipType not found in cache`)( + { elementId, relationshipType }, + ), + ); + return null; + } + + if (!element.valueType) { + log.error( + errorCreator(`cached ${elementType} is missing value type`)( + { elementId, element }), + ); + return null; + } + + return { + id: elementId, + type: element.valueType, + }; + }) + .filter(element => element); +}; + +const getRelationshipTypes = async (): Promise => { + const userStorageController = getUserStorageController(); + const cachedRelationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); + const attributeIds = cachedRelationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = fromConstraint.trackerDataView?.attributes || []; + const toAttributes = toConstraint.trackerDataView?.attributes || []; + return [...fromAttributes, ...toAttributes]; + }).reduce((acc, attributeId) => { + acc[attributeId] = true; + return acc; + }, {}); + + const attributes = (await userStorageController.getAll(userStores.TRACKED_ENTITY_ATTRIBUTES, { + predicate: ({ id }) => attributeIds[id], + project: ({ id, valueType }) => ({ id, valueType }), + })).reduce((acc, { id, valueType }) => { + acc[id] = { valueType }; + return acc; + }, {}); + + const dataElementIds = cachedRelationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromDataElements = fromConstraint.trackerDataView?.dataElements || []; + const toDataElements = toConstraint.trackerDataView?.dataElements || []; + return [...fromDataElements, ...toDataElements]; + }).reduce((acc, dataElementId) => { + acc[dataElementId] = true; + return acc; + }, {}); + + const dataElements = (await userStorageController.getAll(userStores.DATA_ELEMENTS, { + predicate: ({ id }) => dataElementIds[id], + project: ({ id, valueType }) => ({ id, valueType }), + })).reduce((acc, { id, valueType }) => { + acc[id] = { valueType }; + return acc; + }, {}); + + return cachedRelationshipTypes + .map((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = mapElementIdsToObject( + fromConstraint.trackerDataView?.attributes, + attributes, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const toAttributes = mapElementIdsToObject( + fromConstraint.trackerDataView?.attributes, + attributes, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const fromDataElements = mapElementIdsToObject( + fromConstraint.trackerDataView?.dataElements, + dataElements, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + const toDataElements = mapElementIdsToObject( + fromConstraint.trackerDataView?.dataElements, + dataElements, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + + return { + ...relationshipType, + fromConstraint: { + ...fromConstraint, + trackerDataView: { + attributes: fromAttributes, + dataElements: fromDataElements, + }, + }, + toConstraint: { + ...toConstraint, + trackerDataView: { + attributes: toAttributes, + dataElements: toDataElements, + }, + }, + }; + }); +}; + +export const useTEIRelationshipsWidgetMetadata = (): { + getPrograms: GetPrograms, + relationshipTypes: ?RelationshipTypes, + isError: boolean, +} => { const getPrograms = useCallback((trackedEntityTypeId: string) => [...programCollection.values()] .filter(program => @@ -17,47 +143,15 @@ export const useTEIRelationshipsWidgetMetadata = (): { getPrograms: GetPrograms .map(p => ({ id: p.id, name: p.name })), []); - const { data: relationshipTypes, failed } = - useMetadataCustomQuery<>( + const { data: relationshipTypes, isError } = + useIndexedDBQuery( 'relationshipTypes', - async () => { - const userStorageController = getUserStorageController(); - const relationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); - - const attributeIds = relationshipTypes - .flatMap((relationshipType) => { - const { fromConstraint, toConstraint } = relationshipType; - const fromAttributes = fromConstraint.tracekrDataView.attributes || []; - const toAttributes = toConstraint.tracekrDataView.attributes || []; - return [fromAttributes, toAttributes]; - }); - - const attributes = await Promise.all( - attributeIds.map(attributeId => - userStorageController.get(userStores.TRACKED_ENTITY_ATTRIBUTES, attributeId)), - ); - - const dataElementIds = relationshipTypes - .flatMap((relationshipType) => { - const { fromConstraint, toConstraint } = relationshipType; - const fromDataElements = fromConstraint.tracekrDataView.dataElements || []; - const toDataElements = toConstraint.tracekrDataView.dataElements || []; - return [fromDataElements, toDataElements]; - }); - - const dataElements = await Promise.all( - attributeIds.map(attributeId => - userStorageController.get(userStores.DATA, attributeId)), - ); - - - - }, + getRelationshipTypes, ); return { relationshipTypes, getPrograms, - failed, + isError, }; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js index 29479306e3..2a5edb619a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js @@ -1,6 +1,6 @@ // @flow import { useCallback, useMemo } from 'react'; -import { useMetadataApiQuery, useMetadataCustomQuery } from 'capture-core-utils/reactQueryHelpers'; +import { useMetadataApiQuery, useMetadataCustomQuery } from '../../../../../../utils/reactQueryHelpers'; import type { GetSearchGroups, GetSearchGroupsAsync, SearchGroups } from './searchFormSection.types'; export const useSearchGroups = ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js new file mode 100644 index 0000000000..beeba6a251 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js @@ -0,0 +1,25 @@ +// @flow + +export type TrackerDataView = { + atttributes: Array, + dataElements: Array, +}; + +// Should probably differentiate between the different relationshipEntities here +export type RelationshipConstraint = { + relationshipEntity: string, + trackedEntityType?: ?{ id: string }, + program?: ?{ id: string }, + programStage?: ?{ id: string }, + trackerDataView?: ?TrackerDataView, +}; + +export type RelationshipType = { + id: string, + displayName: string, + access: Object, + fromConstraint: RelationshipConstraint, + toConstraint: RelationshipConstraint, +}; + +export type RelationshipTypes = Array; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js new file mode 100644 index 0000000000..74fefb6074 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js @@ -0,0 +1,3 @@ +// @flow + +export * from './RelationshipTypes.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/index.js new file mode 100644 index 0000000000..ede1679339 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/index.js @@ -0,0 +1,3 @@ +// @flow + +export { RelationshipTypes, RelationshipType, TrackerDataView } from './common/Types'; diff --git a/src/core_modules/capture-core/storageControllers/cache.types.js b/src/core_modules/capture-core/storageControllers/cache.types.js index 9c6d6c8006..711bdc7dc0 100644 --- a/src/core_modules/capture-core/storageControllers/cache.types.js +++ b/src/core_modules/capture-core/storageControllers/cache.types.js @@ -240,3 +240,5 @@ export type CachedRelationshipType = { fromConstraint: CachedRelationshipConstraint, toConstraint: CachedRelationshipConstraint, } + +export type CachedRelationshipTypes = Array; diff --git a/src/core_modules/capture-core/storageControllers/index.js b/src/core_modules/capture-core/storageControllers/index.js index 232ea4f37a..7e5c672c59 100644 --- a/src/core_modules/capture-core/storageControllers/index.js +++ b/src/core_modules/capture-core/storageControllers/index.js @@ -4,3 +4,4 @@ export { closeAsync as closeControllersAsync } from './storageControllers'; export { getMainController as getMainStorageController } from './storageControllers'; export { getUserController as getUserStorageController } from './storageControllers'; export { userStores } from './stores'; +export * from './cache.types'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js index 9d5121c8e5..d3e9e39481 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js @@ -1,2 +1,2 @@ // @flow -export { useIndexedDBQuery } from './query'; +export { useIndexedDBQuery, useMetadataCustomQuery, useMetadataApiQuery } from './query'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js index 951b7145ea..3040abc124 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js @@ -1,2 +1,2 @@ // @flow -export { useIndexedDBQuery } from './useMetadataQuery'; +export { useIndexedDBQuery, useMetadataCustomQuery, useMetadataApiQuery } from './useMetadataQuery'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index fe88e433a6..854a460aff 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -1,9 +1,9 @@ // @flow import { useQuery } from 'react-query'; import log from 'loglevel'; -import type { QueryFunction, QueryKey, UseQueryOptions } from 'react-query'; -import type { Result } from './useMetadataQuery.types'; +import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; import { IndexedDBError } from '../../../../capture-core-utils/storage/StorageController'; +import type { Result, Options, QueryFunction, QueryKey } from './useMetadataQuery.types'; const throwErrorForIndexedDB = (error) => { if (error instanceof IndexedDBError) { @@ -19,22 +19,54 @@ const throwErrorForIndexedDB = (error) => { const useAsyncMetadata = ( queryKey: QueryKey, queryFn: QueryFunction, - queryOptions: UseQueryOptions, + queryOptions: Options, ): Result => useQuery(queryKey, queryFn, { staleTime: Infinity, ...queryOptions, }); +export const useMetadataCustomQuery = ( + queryKey: QueryKey, + queryFn: QueryFunction, { + cacheTime = 5, + ...queryOptions + }: Options = {}, +): Result => + useAsyncMetadata(queryKey, queryFn, { + cacheTime, + ...queryOptions, + }); + + export const useIndexedDBQuery = ( queryKey: QueryKey, - queryFn: QueryFunction, - queryOptions: UseQueryOptions, + queryFn: QueryFunction, { + cacheTime = 0, + onError, + ...queryOptions + }: Options = {}, ): Result => useAsyncMetadata(queryKey, queryFn, { - cacheTime: 0, ...queryOptions, + cacheTime, onError: (error) => { - queryOptions?.onError && queryOptions.onError(error); + onError?.(error); throwErrorForIndexedDB(error); }, }); + +export const useMetadataApiQuery = ( + queryKey: QueryKey, + queryObject: ResourceQuery, { + cacheTime = 5, + ...queryOptions + }: Options = {}, +): Result => { + const dataEngine = useDataEngine(); + const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) + .then(response => response.theQuerykey); + return useAsyncMetadata(queryKey, queryFn, { + cacheTime, + ...queryOptions, + }); +}; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js index 3aac3d847f..79ab47440a 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -1,4 +1,7 @@ // @flow -import type { UseQueryResult } from 'react-query'; +import type { UseQueryResult, UseQueryOptions } from 'react-query'; + +export type { QueryFunction, QueryKey } from 'react-query'; export type Result = UseQueryResult; +export type Options = UseQueryOptions; From 1e76f2f68d9181bc2e5749897c9f6125c7d6d011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Tue, 18 Apr 2023 12:07:32 +0200 Subject: [PATCH 71/95] fix: postmerge --- .../capture-core-utils/storage/StorageController.js | 7 ------- .../useTEIRelationshipsWidgetMetadata.js | 3 +-- .../components/WidgetsRelationship/common/Types/index.js | 2 +- .../utils/reactQueryHelpers/query/useMetadataQuery.js | 8 ++++---- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/core_modules/capture-core-utils/storage/StorageController.js b/src/core_modules/capture-core-utils/storage/StorageController.js index 3a55d6720f..3bd315ec03 100644 --- a/src/core_modules/capture-core-utils/storage/StorageController.js +++ b/src/core_modules/capture-core-utils/storage/StorageController.js @@ -5,13 +5,6 @@ import log from 'loglevel'; import { errorCreator } from '../errorCreator'; import { IndexedDBError } from './IndexedDBError/IndexedDBError'; -export class IndexedDBError extends Error { - constructor(error) { - super(error.message); - this.error = error; - } -} - export class StorageController { static errorMessages = { INVALID_NAME: 'A valid database name must be provided', diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js index 4962251102..c608445c7c 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js @@ -13,7 +13,7 @@ const elementTypes = { DATA_ELEMENT: 'dataElement', }; -const mapElementIdsToObject = (elementIds, elements, { relationshipType, elementType }) => { +const mapElementIdsToObject = (elementIds, elements, { relationshipType, elementType }) => (elementIds || []) .map((elementId) => { const element = elements[elementId]; @@ -41,7 +41,6 @@ const mapElementIdsToObject = (elementIds, elements, { relationshipType, element }; }) .filter(element => element); -}; const getRelationshipTypes = async (): Promise => { const userStorageController = getUserStorageController(); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js index 74fefb6074..5214eaaaa4 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js @@ -1,3 +1,3 @@ // @flow -export * from './RelationshipTypes.types'; +export type * from './RelationshipTypes.types'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index ca95a6aeb7..b416b24f9e 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -3,7 +3,7 @@ import { useQuery } from 'react-query'; import log from 'loglevel'; import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; import type { QueryFunction, QueryKey, UseQueryOptions } from 'react-query'; -import { IndexedDBError } from '../../../../capture-core-utils/storage/StorageController'; +import { IndexedDBError } from '../../../../capture-core-utils/storage/IndexedDBError/IndexedDBError'; import type { Result } from './useMetadataQuery.types'; const throwErrorForIndexedDB = (error) => { @@ -29,7 +29,7 @@ const useAsyncMetadata = ( export const useMetadataCustomQuery = ( queryKey: QueryKey, queryFn: QueryFunction, - queryOptions: UseQueryOptions, + queryOptions?: UseQueryOptions, ): Result => useAsyncMetadata(queryKey, queryFn, { cacheTime: 5, @@ -40,7 +40,7 @@ export const useMetadataCustomQuery = ( export const useIndexedDBQuery = ( queryKey: QueryKey, queryFn: QueryFunction, - queryOptions: UseQueryOptions, + queryOptions?: UseQueryOptions, ): Result => useAsyncMetadata(queryKey, queryFn, { cacheTime: 0, @@ -54,7 +54,7 @@ export const useIndexedDBQuery = ( export const useMetadataApiQuery = ( queryKey: QueryKey, queryObject: ResourceQuery, - queryOptions: UseQueryOptions, + queryOptions?: UseQueryOptions, ): Result => { const dataEngine = useDataEngine(); const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) From dc5b88d9fedb92a716638d9682194af1e382e636 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Thu, 27 Apr 2023 17:49:42 +0200 Subject: [PATCH 72/95] chore: temp --- i18n/en.pot | 40 +++--- .../EnrollmentPageDefault.component.js | 22 +-- .../EnrollmentPageDefault.types.js | 1 - ...kedEntityRelationshipsWrapper.component.js | 35 ++--- ...TrackedEntityRelationshipsWrapper.types.js | 2 + .../EnrollmentEditEventPage.component.js | 12 +- .../useRelationshipTypesMetadata.js | 2 +- .../useTEIRelationshipsWidgetMetadata.js | 125 +++--------------- .../RelationshipsWidget.component.js | 59 --------- .../WidgetEventsRelationships.component.js | 18 --- .../WidgetEventsRelationships/index.js | 1 - .../WidgetEventsRelationships/types.js | 10 -- .../WidgetTeisRelationships.component.js | 18 --- .../WidgetTeisRelationships/index.js | 1 - .../WidgetTeisRelationships/types.js | 10 -- .../WidgetRelationships/common.types.js | 68 ---------- .../components/WidgetRelationships/index.js | 2 - .../Stage/StageDetail/hooks/useEventList.js | 4 +- .../useApplicableTypesAndSides.js | 2 +- .../NewTrackedEntityRelationship.component.js | 12 +- .../NewTrackedEntityRelationship.container.js | 69 ++++++++++ .../NewTrackedEntityRelationship.portal.js | 2 +- .../NewTrackedEntityRelationship.types.js | 3 + .../NewTrackedEntityRelationship/index.js | 2 +- ...dgetTrackedEntityRelationship.component.js | 69 ++++------ .../WidgetTrackedEntityRelationship.types.js | 22 +-- .../common/index.js | 2 - .../common/relationshipEntities.js | 7 - .../WidgetTrackedEntityRelationship/hooks.js | 29 ---- .../WidgetTrackedEntityRelationship/index.js | 1 - .../RelationshipsTable.component.js | 32 ++--- .../RelationshipsTables.component.js} | 28 ++-- .../RelationshipsWidget.component.js | 78 +++++++++++ .../common}/RelationshipsComponent/index.js | 0 .../common/Types/RelationshipTypes.types.js | 4 +- .../WidgetsRelationship/common/hooks/index.js | 4 + .../common}/hooks/useLinkedEntityGroups.js | 18 +-- .../common/hooks/useRelationshipTypes.js | 92 +++++++++++++ .../common/hooks/useRelationships.js | 69 ++++++++++ .../extractElementIdsFromRelationshipTypes.js | 32 +++++ .../common/utils/formatRelationshipTypes.js | 81 ++++++++++++ .../WidgetsRelationship/common/utils/index.js | 4 + .../utils/mapRelationshipElementToId.js | 56 ++++++++ .../constants.js | 8 +- .../components/WidgetsRelationship/index.js | 3 +- .../query/useApiDataQuery.js | 26 ++++ .../query/useMetadataQuery.types.js | 9 +- 47 files changed, 685 insertions(+), 509 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/common.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetRelationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js rename src/core_modules/capture-core/components/{WidgetRelationships => WidgetsRelationship/common}/RelationshipsComponent/RelationshipsTable.component.js (80%) rename src/core_modules/capture-core/components/{WidgetRelationships/RelationshipsComponent/Relationships.component.js => WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js} (55%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js rename src/core_modules/capture-core/components/{WidgetRelationships => WidgetsRelationship/common}/RelationshipsComponent/index.js (100%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js rename src/core_modules/capture-core/components/{WidgetRelationships => WidgetsRelationship/common}/hooks/useLinkedEntityGroups.js (94%) create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js rename src/core_modules/capture-core/components/{WidgetRelationships => WidgetsRelationship}/constants.js (84%) create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js diff --git a/i18n/en.pot b/i18n/en.pot index 511866c929..fb2aedd811 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-04-24T12:30:29.719Z\n" -"PO-Revision-Date: 2023-04-24T12:30:29.719Z\n" +"POT-Creation-Date: 2023-04-26T13:23:00.268Z\n" +"PO-Revision-Date: 2023-04-26T13:23:00.268Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1275,30 +1275,15 @@ msgstr "{{TETName}} profile" msgid "Edit" msgstr "Edit" -msgid "Show {{ rest }} more" -msgstr "Show {{ rest }} more" - -msgid "Event's Relationships" -msgstr "Event's Relationships" - -msgid "TEI's Relationships" -msgstr "TEI's Relationships" - -msgid "TET name" -msgstr "TET name" - -msgid "Created date" -msgstr "Created date" - -msgid "Program stage name" -msgstr "Program stage name" - msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" msgid "This event is not yet preserved and cannot be edited" msgstr "This event is not yet preserved and cannot be edited" +msgid "Show {{ rest }} more" +msgstr "Show {{ rest }} more" + msgid "Reset list" msgstr "Reset list" @@ -1338,6 +1323,21 @@ msgstr "An error occurred" msgid "New Relationship" msgstr "New Relationship" +msgid "Something went wrong while loading relationships. Please try again later." +msgstr "Something went wrong while loading relationships. Please try again later." + +msgid "TEI's Relationships" +msgstr "TEI's Relationships" + +msgid "TET name" +msgstr "TET name" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "Working list could not be loaded" msgstr "Working list could not be loaded" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 4b54b04c14..b006300975 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -13,7 +13,6 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; -import { WidgetTeisRelationships } from '../../../WidgetRelationships'; import { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper'; const getStyles = () => ({ @@ -56,7 +55,6 @@ export const EnrollmentPageDefaultPlain = ({ events, enrollmentId, relationships, - relationshipTypes, stages, onDelete, onAddNew, @@ -78,11 +76,6 @@ export const EnrollmentPageDefaultPlain = ({ setAddRelationshipContainerElement(renderRelationshipRef.current); }, []); - const a = (c, d, e) => { - debugger; - onEventClick(c, d, e); - }; - const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); return ( @@ -102,11 +95,12 @@ export const EnrollmentPageDefaultPlain = ({ events={events} />
@@ -117,18 +111,16 @@ export const EnrollmentPageDefaultPlain = ({ addRelationshipRenderElement={addRelationShipContainerElement} onOpenAddRelationship={toggleVisibility} onCloseAddRelationship={toggleVisibility} + relationships={relationships} + // relationshipTypes={relationshipTypes} + teiId={teiId} + onAddRelationship={() => {}} + onLinkedRecordClick={onLinkedRecordClick} /> } - {}} - onLinkedRecordClick={onLinkedRecordClick} - /> {!hideWidgets.indicator && ( void, onCreateNew: (stageId: string) => void, onEventClick: (eventId: string, stageId: string) => void, - relationshipTypes?: Array, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, onLinkedRecordClick: (parameters: Url) => void, onEnrollmentError: (message: string) => void, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index 0af0ca6c28..0fae62b1bb 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -1,5 +1,5 @@ // @flow -import React, { useCallback, useState } from 'react'; +import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useTEIRelationshipsWidgetMetadata } from '../../../common/TEIRelationshipsWidget'; import { WidgetTrackedEntityRelationship } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; @@ -7,10 +7,12 @@ import type { Props } from './TrackedEntityRelationshipsWrapper.types'; export const TrackedEntityRelationshipsWrapper = ({ trackedEntityTypeId, + teiId, programId, addRelationshipRenderElement, onOpenAddRelationship, onCloseAddRelationship, + onLinkedRecordClick, }: Props) => { const { getPrograms, relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); @@ -21,25 +23,28 @@ export const TrackedEntityRelationshipsWrapper = ({
); } - if (!(relationshipTypes && addRelationshipRenderElement)) { + + if (!relationshipTypes || !addRelationshipRenderElement) { return null; } return ( <> - [{ unique: false, fields: [{ id: 'id1', type: 'TEXT' }] }]} - /> + [{ unique: false, fields: [{ id: 'id1', type: 'TEXT' }] }]} + /> ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js index 4316c363eb..0466288853 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js @@ -1,8 +1,10 @@ // @flow export type Props = {| trackedEntityTypeId: string, + teiId: string, programId: string, addRelationshipRenderElement: HTMLDivElement, onOpenAddRelationship: () => void, onCloseAddRelationship: () => void, + onLinkedRecordClick: () => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 5bb4d0ffb6..0bbf5a1aaa 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -16,7 +16,6 @@ import { WidgetProfile } from '../../WidgetProfile'; import { WidgetEnrollment } from '../../WidgetEnrollment'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; -import { WidgetTeisRelationships, WidgetEventsRelationships } from '../../WidgetRelationships'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; @@ -127,20 +126,13 @@ const EnrollmentEditEventPagePain = ({ - {}} onLinkedRecordClick={onLinkedRecordClick} - /> - {}} - onLinkedRecordClick={onLinkedRecordClick} - /> + /> */} {!hideWidgets.feedback && ( Promise.all(results.map(res => fetchDataView(res)))) .then((res) => { setRelationshipTypesMetadata(res); - storageController.setAll(userStores.RELATIONSHIP_TYPES, res); + // storageController.setAll(userStores.RELATIONSHIP_TYPES, res); }); } }, [relationships, fetchDataView]); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js index c608445c7c..2d43501e2e 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js @@ -1,130 +1,41 @@ // @flow import { useCallback } from 'react'; -import log from 'loglevel'; -import { errorCreator } from 'capture-core-utils'; import { useIndexedDBQuery } from '../../../../utils/reactQueryHelpers'; import { getUserStorageController, userStores } from '../../../../storageControllers'; import { programCollection } from '../../../../metaDataMemoryStores'; import { TrackerProgram } from '../../../../metaData'; -import type { GetPrograms, RelationshipTypes } from '../../../WidgetsRelationship'; - -const elementTypes = { - ATTRIBUTE: 'attribute', - DATA_ELEMENT: 'dataElement', -}; - -const mapElementIdsToObject = (elementIds, elements, { relationshipType, elementType }) => - (elementIds || []) - .map((elementId) => { - const element = elements[elementId]; - - if (!element) { - log.error( - errorCreator(`${elementType} from relationshipType not found in cache`)( - { elementId, relationshipType }, - ), - ); - return null; - } - - if (!element.valueType) { - log.error( - errorCreator(`cached ${elementType} is missing value type`)( - { elementId, element }), - ); - return null; - } - - return { - id: elementId, - type: element.valueType, - }; - }) - .filter(element => element); +import type { RelationshipTypes } from '../../../WidgetsRelationship'; +import { + extractElementIdsFromRelationshipTypes, + formatRelationshipTypes, +} from '../../../WidgetsRelationship'; const getRelationshipTypes = async (): Promise => { const userStorageController = getUserStorageController(); const cachedRelationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); - const attributeIds = cachedRelationshipTypes - .flatMap((relationshipType) => { - const { fromConstraint, toConstraint } = relationshipType; - const fromAttributes = fromConstraint.trackerDataView?.attributes || []; - const toAttributes = toConstraint.trackerDataView?.attributes || []; - return [...fromAttributes, ...toAttributes]; - }).reduce((acc, attributeId) => { - acc[attributeId] = true; - return acc; - }, {}); + const { dataElementIds, attributeIds } = extractElementIdsFromRelationshipTypes(cachedRelationshipTypes); const attributes = (await userStorageController.getAll(userStores.TRACKED_ENTITY_ATTRIBUTES, { predicate: ({ id }) => attributeIds[id], - project: ({ id, valueType }) => ({ id, valueType }), - })).reduce((acc, { id, valueType }) => { - acc[id] = { valueType }; + project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), + })).reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; return acc; }, {}); - const dataElementIds = cachedRelationshipTypes - .flatMap((relationshipType) => { - const { fromConstraint, toConstraint } = relationshipType; - const fromDataElements = fromConstraint.trackerDataView?.dataElements || []; - const toDataElements = toConstraint.trackerDataView?.dataElements || []; - return [...fromDataElements, ...toDataElements]; - }).reduce((acc, dataElementId) => { - acc[dataElementId] = true; - return acc; - }, {}); - const dataElements = (await userStorageController.getAll(userStores.DATA_ELEMENTS, { predicate: ({ id }) => dataElementIds[id], - project: ({ id, valueType }) => ({ id, valueType }), - })).reduce((acc, { id, valueType }) => { - acc[id] = { valueType }; + project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), + })).reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; return acc; }, {}); - return cachedRelationshipTypes - .map((relationshipType) => { - const { fromConstraint, toConstraint } = relationshipType; - const fromAttributes = mapElementIdsToObject( - fromConstraint.trackerDataView?.attributes, - attributes, - { relationshipType, elementType: elementTypes.ATTRIBUTE }, - ); - const toAttributes = mapElementIdsToObject( - fromConstraint.trackerDataView?.attributes, - attributes, - { relationshipType, elementType: elementTypes.ATTRIBUTE }, - ); - const fromDataElements = mapElementIdsToObject( - fromConstraint.trackerDataView?.dataElements, - dataElements, - { relationshipType, elementType: elementTypes.DATA_ELEMENT }, - ); - const toDataElements = mapElementIdsToObject( - fromConstraint.trackerDataView?.dataElements, - dataElements, - { relationshipType, elementType: elementTypes.DATA_ELEMENT }, - ); - - return { - ...relationshipType, - fromConstraint: { - ...fromConstraint, - trackerDataView: { - attributes: fromAttributes, - dataElements: fromDataElements, - }, - }, - toConstraint: { - ...toConstraint, - trackerDataView: { - attributes: toAttributes, - dataElements: toDataElements, - }, - }, - }; - }); + return formatRelationshipTypes({ + relationshipTypes: cachedRelationshipTypes, + attributes, + dataElements, + }); }; export const useTEIRelationshipsWidgetMetadata = (): { @@ -144,7 +55,7 @@ export const useTEIRelationshipsWidgetMetadata = (): { const { data: relationshipTypes, isError } = useIndexedDBQuery( - 'relationshipTypes', + ['cachedRelationshipTypes'], getRelationshipTypes, ); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js deleted file mode 100644 index 2c2e2bddb5..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsWidget.component.js +++ /dev/null @@ -1,59 +0,0 @@ -// @flow -import React, { type ComponentType, useState, useCallback } from 'react'; -import { Chip, IconLink24, spacers } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core'; -import { Widget } from '../../Widget'; -import { Relationships } from './Relationships.component'; -import type { OutputRelationship } from '../common.types'; -import type { Url } from '../../../utils/url'; - -type Props = {| - relationships: Array, - title: string, - onAddRelationship: () => void, - onLinkedRecordClick: (parameters: Url) => void, - ...CssClasses, -|} - -const styles = { - header: { - display: 'flex', - alignItems: 'center', - }, - icon: { - paddingRight: spacers.dp8, - }, -}; - -const RelationshipsWidgetPlain = ({ relationships, title, classes, ...passOnProps }: Props) => { - const [open, setOpenStatus] = useState(true); - const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); - return ( -
- - - {title} - {relationships && - {count} - - } -
- } - onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} - onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} - open={open} - > - - -
- ); -}; - -export const RelationshipsWidget: ComponentType = withStyles(styles)(RelationshipsWidgetPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js deleted file mode 100644 index c520e3182e..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/WidgetEventsRelationships.component.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; -import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { Props } from './types'; - -export const WidgetEventsRelationships = ({ eventId, relationships, relationshipTypes, ...passOnProps }: Props) => { - const { relationships: eventsRelationships } = useLinkedEntityGroups(eventId, relationshipTypes, relationships); - - return ( - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js deleted file mode 100644 index 2c77ef58b8..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/index.js +++ /dev/null @@ -1 +0,0 @@ -export { WidgetEventsRelationships } from './WidgetEventsRelationships.component'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js deleted file mode 100644 index 8c03663e88..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetEventsRelationships/types.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow - -import type { InputRelationship, RelationshipType } from '../common.types'; - -export type Props = {| - eventId: string, - relationships: Array, - relationshipTypes: Array, - onAddRelationship: () => void -|} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js deleted file mode 100644 index 305a07140f..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/WidgetTeisRelationships.component.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { useLinkedEntityGroups } from '../hooks/useLinkedEntityGroups'; -import { RelationshipsWidget } from '../RelationshipsComponent'; -import type { Props } from './types'; - -export const WidgetTeisRelationships = ({ relationships, relationshipTypes, teiId, ...passOnProps }: Props) => { - const { relationships: teiRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); - - return ( - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js deleted file mode 100644 index 5e2c5a06f1..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/index.js +++ /dev/null @@ -1 +0,0 @@ -export { WidgetTeisRelationships } from './WidgetTeisRelationships.component'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js b/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js deleted file mode 100644 index 65df23b52f..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/WidgetTeisRelationships/types.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import type { InputRelationship, RelationshipType } from '../common.types'; - -export type Props = {| - relationships: Array, - relationshipTypes: Array, - onAddRelationship: () => void, - teiId: string, - ...CssClasses, -|}; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/common.types.js b/src/core_modules/capture-core/components/WidgetRelationships/common.types.js deleted file mode 100644 index 66cd80b50d..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/common.types.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow -import type { EnrollmentData, Event } - from '../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types'; - -export type TEIAttribute = {| - attribute: string, - displayName: string, - value: string, - valueType: string, -|} - -export type TEIRelationshipData = {| - trackedEntity: { - trackedEntityType: string, - trackedEntity: string, - attributes: Array, - orgUnit: string - } -|} - -export type EventRelationshipData = {| - event: Event -|} - -export type EnrollmentRelationshipData = {| - enrollment: EnrollmentData -|} - -export type RelationshipData = TEIRelationshipData | EventRelationshipData - -export type InputRelationship = {| - relationshipType: string, - relationshipName: string, - createdAt: string, - relationship: string, - bidirectional: boolean, - from: RelationshipData, - to: RelationshipData -|} - -export type OutputRelationship = { - id: string, - relationshipName: string, - linkedEntityData: Array<{ id: string, values: Array}> -} - -export type RelationshipConstraintDataView = string | {id: string, displayName: string, valueType: string} - -export type RelationshipConstraint = { - relationshipEntity: string, - trackedEntityType?: ?{ id: string }, - program?: ?{ id: string }, - programStage?: ?{ id: string }, - trackerDataView: { - attributes: Array, - dataElements: Array, - } -} - -export type RelationshipType = { - id: string, - bidirectional: boolean, - displayName: string, - fromConstraint: RelationshipConstraint, - fromToName: string, - toConstraint: RelationshipConstraint, - toFromName: string, -} diff --git a/src/core_modules/capture-core/components/WidgetRelationships/index.js b/src/core_modules/capture-core/components/WidgetRelationships/index.js deleted file mode 100644 index 78171f3413..0000000000 --- a/src/core_modules/capture-core/components/WidgetRelationships/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { WidgetTeisRelationships } from './WidgetTeisRelationships/'; -export { WidgetEventsRelationships } from './WidgetEventsRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js index 23188bd257..9fbe29d4d5 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js @@ -27,9 +27,9 @@ const basedFieldTypes = [ ]; const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.formFoundation?.getLabel('occurredAt') ?? 'test', sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.formFoundation?.getLabel('scheduledAt') ?? 'Test', sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, ]; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js index 559b894d81..7be52a5817 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -1,9 +1,9 @@ // @flow import { useMemo } from 'react'; -import { RELATIONSHIP_ENTITIES } from '../../common'; import { TARGET_SIDES } from '../common'; import type { TargetSides } from '../MOVE_WidgetsCommon'; import type { RelationshipType, ApplicableTypesInfo } from './linkedEntityMetadataSelector.types'; +import { RELATIONSHIP_ENTITIES } from '../../../constants'; const isApplicableProgram = (programId, sourceProgramIds) => (!sourceProgramIds || !programId || sourceProgramIds.includes(programId)); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 9af8b91f57..ab24ecf4ba 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -119,11 +119,13 @@ const NewTrackedEntityRelationshipPlain = ({
} + header={( + + )} > {stepContents[currentStep.id]()} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js new file mode 100644 index 0000000000..86845780db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -0,0 +1,69 @@ +// @flow +import React, { useCallback, useState } from 'react'; +import { Button, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import i18n from '@dhis2/d2-i18n'; +import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; +import type { PlainProps } from './NewTrackedEntityRelationship.types'; + +const styles = { + container: { + padding: `0 ${spacers.dp16} ${spacers.dp24} ${spacers.dp16}`, + }, +}; + +export const NewTrackedEntityRelationshipPlain = ({ + addRelationshipRenderElement, + programId, + relationshipTypes, + trackedEntityTypeId, + onCloseAddRelationship, + onOpenAddRelationship, + getPrograms, + getSearchGroups = () => {}, + getSearchGroupsAsync = () => {}, + classes, +}: PlainProps) => { + const [addWizardVisible, setAddWizardVisibility] = useState(false); + + const closeAddWizard = useCallback(() => { + setAddWizardVisibility(false); + onCloseAddRelationship && onCloseAddRelationship(); + }, [onCloseAddRelationship]); + + const openAddWizard = useCallback(() => { + setAddWizardVisibility(true); + onOpenAddRelationship && onOpenAddRelationship(); + }, [onOpenAddRelationship]); + + return ( +
+ + + { + addWizardVisible && ( + + ) + } +
+ ); +}; + +export const NewTrackedEntityRelationship = withStyles(styles)(NewTrackedEntityRelationshipPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js index df67f4476e..eec749787f 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'; import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; import type { PortalProps } from './NewTrackedEntityRelationship.types'; -export const NewTrackedEntityRelationship = ({ renderElement, ...passOnProps }: PortalProps) => +export const NewTrackedEntityRelationshipPortal = ({ renderElement, ...passOnProps }: PortalProps) => ReactDOM.createPortal(( ), renderElement); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index 5f3b572e32..456f2ceeff 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -4,11 +4,14 @@ import type { GetPrograms } from './common'; import type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; export type Props = $ReadOnly<{| + addRelationshipRenderElement: HTMLElement, relationshipTypes: RelationshipTypes, trackedEntityTypeId: string, programId: string, onSave: () => void, onCancel: () => void, + onCloseAddRelationship: () => void, + onOpenAddRelationship: () => void, getPrograms: GetPrograms, getSearchGroups?: GetSearchGroups, getSearchGroupsAsync?: GetSearchGroupsAsync, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js index 390a4ec9db..60442f90dc 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js @@ -1,4 +1,4 @@ // @flow -export { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship.portal'; +export { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship.container'; export type { GetPrograms } from './common'; export type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js index 1e36ba0d2f..708c95fdcf 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js @@ -1,55 +1,40 @@ // @flow -import React, { useCallback, useState } from 'react'; -import { Button } from '@dhis2/ui'; +import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; import type { Props } from './WidgetTrackedEntityRelationship.types'; +import { useLinkedEntityGroups } from '../common/hooks'; +import { RelationshipsWidget } from '../common/RelationshipsComponent'; +import { RelationshipSearchEntities, useRelationships } from '../common/hooks/useRelationships'; +import { useRelationshipTypes } from '../common/hooks/useRelationshipTypes'; export const WidgetTrackedEntityRelationship = ({ - relationshipTypes = [], + cachedRelationshipTypes, trackedEntityTypeId, - programId, - addRelationshipRenderElement, - onOpenAddRelationship, - onCloseAddRelationship, - getPrograms, - getSearchGroups, - getSearchGroupsAsync, + teiId, + ...passOnProps }: Props) => { - const [addWizardVisible, setAddWizardVisibility] = useState(false); + const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); + const { data: relationships, isError } = useRelationships(teiId, RelationshipSearchEntities.TRACKED_ENTITY); - const closeAddWizard = useCallback(() => { - setAddWizardVisibility(false); - onCloseAddRelationship && onCloseAddRelationship(); - }, [onCloseAddRelationship]); + // TODO: Refactor this to be self contained + const { relationships: linkedEntityRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); - const openAddWizard = useCallback(() => { - setAddWizardVisibility(true); - onOpenAddRelationship && onOpenAddRelationship(); - }, [onOpenAddRelationship]); + if (isError) { + return ( +
+ {i18n.t('Something went wrong while loading relationships. Please try again later.')} +
+ ); + } return ( - <> - - { - addWizardVisible && - - } - + ); }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js index f5c1ab4e0e..db5e5bc23a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -1,28 +1,14 @@ // @flow import type { GetPrograms, GetSearchGroups, GetSearchGroupsAsync } from './NewTrackedEntityRelationship'; - -type Constraint = {| - program?: { id: string }, - trackedEntityType?: { id: string }, -|} - -export type RelationshipType = $ReadOnly<{| - id: string, - displayName: string, - bidirectional: boolean, - fromToName: string, - toFromName?: string, - fromConstraint: Constraint, - toConstraint: Constraint, -|}>; - -export type RelationshipTypes = Array; +import type { RelationshipType, RelationshipTypes } from '../common/Types'; export type Props = {| - relationshipTypes?: RelationshipTypes, + cachedRelationshipTypes: RelationshipTypes, trackedEntityTypeId: string, + teiId: string, programId: string, addRelationshipRenderElement: HTMLElement, + onLinkedRecordClick: () => void, onOpenAddRelationship?: () => void, onCloseAddRelationship?: () => void, /* diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js deleted file mode 100644 index ee04405717..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { RELATIONSHIP_ENTITIES } from './relationshipEntities'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js deleted file mode 100644 index fc528b72e5..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/common/relationshipEntities.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow - -export const RELATIONSHIP_ENTITIES = Object.freeze({ - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', -}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js deleted file mode 100644 index 0a932a6f5d..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/hooks.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -// TODO: Move / get rid -import { useMemo, useRef, useState } from 'react'; -import { - getCachedResourceAsync, -} from '../../../MetaDataStoreUtils/MetaDataStoreUtils'; -import { userStores } from '../../../storageControllers/stores'; - -export const useRelationshipTypes = () => { - const [cachedTypes, setCachedTypes] = useState(); - const [loading, setLoading] = useState(true); - const error = useRef(); - useMemo(() => { - getCachedResourceAsync(userStores.RELATIONSHIP_TYPES) - .then((relationshipTypes) => { - setCachedTypes(relationshipTypes?.response); - setLoading(false); - }) - .catch((e) => { - console.error(e); - }); - }, []); - - return { - loading, - error: error.current, - relationshipTypes: cachedTypes, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js index f1c0a93392..25167d6943 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js @@ -1,4 +1,3 @@ // @flow export { WidgetTrackedEntityRelationship } from './WidgetTrackedEntityRelationship.component'; export type { GetPrograms } from './NewTrackedEntityRelationship'; -export type { RelationshipType, RelationshipTypes } from './WidgetTrackedEntityRelationship.types'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js similarity index 80% rename from src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js index cee2fc09f6..1e00ffd4cb 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js @@ -12,7 +12,7 @@ import { spacers, } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -import type { Url } from '../../../utils/url'; +import type { Url } from '../../../../utils/url'; type Props = { @@ -79,20 +79,22 @@ const RelationshipsTablePlain = (props: Props) => { const renderShowMoreButton = () => { const shouldShowMore = linkedEntityData.length > DEFAULT_NUMBER_OF_ROW && displayedRowNumber < linkedEntityData.length; - return shouldShowMore ? : null; + return shouldShowMore ? ( + + ) : null; }; return ( diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js similarity index 55% rename from src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js index 1a1e2542ab..3fd8d19383 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/Relationships.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js @@ -1,10 +1,9 @@ // @flow import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; -import i18n from '@dhis2/d2-i18n'; -import { spacersNum, spacers, colors, Button } from '@dhis2/ui'; +import { spacersNum, spacers, colors } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; -import type { Url } from '../../../utils/url'; +import type { Url } from '../../../../utils/url'; type Props = { relationships: Object, @@ -15,7 +14,7 @@ type Props = { const styles = { container: { - padding: `0 ${spacers.dp16} ${spacers.dp24} ${spacers.dp16}`, + padding: `0 ${spacers.dp16} ${spacers.dp12} ${spacers.dp16}`, }, title: { fontWeight: 500, @@ -31,23 +30,20 @@ const styles = { overflow: 'scroll', }, }; -const RelationshipsPlain = ({ relationships, classes, onAddRelationship, onLinkedRecordClick }: Props) => ( +const RelationshipsTablesPlain = ({ relationships, classes, onLinkedRecordClick }: Props) => (
- { - relationships ? relationships.map((relationship) => { - const { relationshipName, id, ...passOnProps } = relationship; - return (
+ {relationships && relationships.map((relationship) => { + const { relationshipName, id, ...passOnProps } = relationship; + return ( +
{relationshipName}
-
); - }) : null - } - +
+ ); + })}
); -export const Relationships: ComponentType = withStyles(styles)(RelationshipsPlain); +export const RelationshipTables: ComponentType = withStyles(styles)(RelationshipsTablesPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js new file mode 100644 index 0000000000..3b4ae74c70 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js @@ -0,0 +1,78 @@ +// @flow +import React, { type ComponentType, useState } from 'react'; +import { Chip, IconLink24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../../Widget'; +import { RelationshipTables } from './RelationshipsTables.component'; +import type { OutputRelationship } from '../../../WidgetRelationships/common.types'; +import type { Url } from '../../../../utils/url'; +import { NewTrackedEntityRelationship } from '../../WidgetTrackedEntityRelationship/NewTrackedEntityRelationship'; + +type Props = {| + relationships: Array, + title: string, + onAddRelationship: () => void, + onLinkedRecordClick: (parameters: Url) => void, + teiId?: string, + eventId?: string, + ...CssClasses, +|} + +const styles = { + header: { + display: 'flex', + alignItems: 'center', + }, + icon: { + paddingRight: spacers.dp8, + }, +}; + +const RelationshipsWidgetPlain = ({ relationships, title, teiId, eventId, classes, ...passOnProps }: Props) => { + const [open, setOpenStatus] = useState(true); + const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); + return ( +
+ + + {title} + {relationships && ( + + {count} + + )} +
+ )} + onOpen={() => setOpenStatus(true)} + onClose={() => setOpenStatus(false)} + open={open} + > + + + {teiId && ( + + )} + + {eventId && ( +
+

Add event relationships

+

Not implemented yet

+
+ )} + +
+ ); +}; + +export const RelationshipsWidget: ComponentType = withStyles(styles)(RelationshipsWidgetPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/index.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetRelationships/RelationshipsComponent/index.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/index.js diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js index beeba6a251..692b3ff86a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js @@ -1,7 +1,7 @@ // @flow export type TrackerDataView = { - atttributes: Array, + attributes: Array, dataElements: Array, }; @@ -18,6 +18,8 @@ export type RelationshipType = { id: string, displayName: string, access: Object, + toFromName: string, + fromToName: string, fromConstraint: RelationshipConstraint, toConstraint: RelationshipConstraint, }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js new file mode 100644 index 0000000000..87338a8a5e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js @@ -0,0 +1,4 @@ +// @flow + +export { useLinkedEntityGroups } from './useLinkedEntityGroups'; +export { useRelationships } from './useRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js similarity index 94% rename from src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js index 4e5916dde5..5b07347a20 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js @@ -1,19 +1,19 @@ // @flow import { useCallback, useEffect, useState } from 'react'; import log from 'loglevel'; -import { errorCreator } from 'capture-core-utils'; import moment from 'moment'; +import { errorCreator } from 'capture-core-utils'; import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } - from '../../../metaData'; + from '../../../../metaData'; import type { InputRelationship, - RelationshipType, RelationshipData, TEIAttribute, -} from '../common.types'; -import type { DataValue } from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import { getBaseConfigHeaders, relationshipEntities } from '../constants'; -import { convertServerToClient, convertClientToList } from '../../../converters'; +} from '../../../WidgetRelationships/common.types'; +import type { DataValue } from '../../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import { getBaseConfigHeaders, relationshipEntities } from '../../constants'; +import { convertServerToClient, convertClientToList } from '../../../../converters'; +import type { RelationshipTypes } from '../Types'; const convertAttributes = ( attributes: Array | Array, @@ -160,12 +160,12 @@ const getLinkedEntityInfo = ( export const useLinkedEntityGroups = ( targetId: string, - relationshipTypes: Array, + relationshipTypes: ?RelationshipTypes, relationships?: Array, ) => { const [relationshipsByType, setRelationshipByType] = useState([]); - const computeData = useCallback(async () => { + const computeData = useCallback(() => { if (relationships?.length && relationshipTypes?.length) { const linkedEntityGroups = relationships .sort((a, b) => moment.utc(b.createdAt).diff(moment.utc(a.createdAt))) diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js new file mode 100644 index 0000000000..075bab8590 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js @@ -0,0 +1,92 @@ +// @flow +import { useMemo } from 'react'; +import { useMetadataApiQuery } from '../../../../utils/reactQueryHelpers'; +import type { RelationshipTypes } from '../Types'; +import { extractElementIdsFromRelationshipTypes, formatRelationshipTypes } from '../utils'; + +type Element = {| + id: string, + displayName: string, + valueType: string, +|}; + +const relationshipTypesQuery = { + resource: 'relationshipTypes', + params: { + fields: 'id,displayName,fromConstraint,toConstraint', + }, +}; + +export const useRelationshipTypes = (cachedRelationshipTypes: RelationshipTypes) => { + const { data: apiRelationshipTypes, isError, isLoading } = useMetadataApiQuery( + ['relationshipTypesForRelationshipWidget'], + relationshipTypesQuery, + { + enabled: !cachedRelationshipTypes?.length, + select: ({ relationshipTypes }: any) => relationshipTypes, + }, + ); + + const { attributeQuery, dataElementQuery } = useMemo(() => { + if (!apiRelationshipTypes) return {}; + const { dataElementIds, attributeIds } = extractElementIdsFromRelationshipTypes(apiRelationshipTypes); + + const filteredAttributeQuery = { + resource: 'trackedEntityAttributes', + params: { + fields: 'id,displayName,valueType', + filter: `id:in:[${Object.keys(attributeIds).join(',')}]`, + paging: false, + }, + }; + + const filteredDataElementQuery = { + resource: 'dataElements', + params: { + fields: 'id,displayName,valueType', + filter: `id:in:[${Object.keys(dataElementIds).join(',')}]`, + paging: false, + }, + }; + + return { + attributeQuery: filteredAttributeQuery, + dataElementQuery: filteredDataElementQuery, + }; + }, [apiRelationshipTypes]); + + const { data: apiAttributes } = useMetadataApiQuery>( + ['attributesForRelationshipWidget'], + attributeQuery, + { + enabled: !cachedRelationshipTypes?.length && !!attributeQuery, + select: ({ trackedEntityAttributes }: any) => trackedEntityAttributes, + }, + ); + + const { data: apiDataElements } = useMetadataApiQuery>( + ['dataElementsForRelationshipWidget'], + dataElementQuery, + { + enabled: !cachedRelationshipTypes?.length && !!dataElementQuery, + select: ({ dataElements }: any) => dataElements, + }, + ); + + + const relationshipTypes = useMemo(() => { + if (!apiRelationshipTypes || !apiAttributes || !apiDataElements) return null; + + return formatRelationshipTypes({ + relationshipTypes: apiRelationshipTypes, + attributes: apiAttributes, + dataElements: apiDataElements, + }); + }, [apiAttributes, apiDataElements, apiRelationshipTypes]); + + return { + data: relationshipTypes ?? cachedRelationshipTypes, + isError, + isLoading, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js new file mode 100644 index 0000000000..1161ec1a69 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js @@ -0,0 +1,69 @@ +// @flow +import { useMemo } from 'react'; +import { useApiDataQuery } from '../../../../utils/reactQueryHelpers/query/useApiDataQuery'; +import type { Relationship } from '../../../Relationships/relationships.types'; + +export const RelationshipSearchEntities = Object.freeze({ + TRACKED_ENTITY: 'trackedEntity', + ENROLLMENT: 'enrollment', + EVENT: 'event', +}); + +type ReturnData = Array; + +export const useRelationships = (entityId: string, searchMode: string) => { + const query = useMemo(() => ({ + resource: 'tracker/relationships', + params: { + [searchMode]: entityId, + fields: ` + relationshipType, + createdAt, + from[ + trackedEntity[ + trackedEntity, + attributes, + program, + orgUnit, + trackedEntityType + ], + event[ + event, + dataValues, + program, + orgUnit, + orgUnitName, + status, + createdAt + ] + ], + to[ + trackedEntity[ + trackedEntity, + attributes, + program, + orgUnit, + trackedEntityType + ], + event[ + event, + dataValues, + program, + orgUnit, + orgUnitName, + status, + createdAt + ] + ]`, + }, + }), [entityId, searchMode]); + + return useApiDataQuery( + ['widgetRelationship', 'relationships', entityId], + query, + { + enabled: !!entityId, + select: ({ instances }: any) => instances, + }, + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js new file mode 100644 index 0000000000..3563b98c5e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js @@ -0,0 +1,32 @@ +// @flow + +import type { RelationshipTypes } from '../Types'; + +export const extractElementIdsFromRelationshipTypes = (relationshipTypes: RelationshipTypes) => { + const attributeIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = fromConstraint.trackerDataView?.attributes || []; + const toAttributes = toConstraint.trackerDataView?.attributes || []; + return [...fromAttributes, ...toAttributes]; + }).reduce((acc, attributeId) => { + acc[attributeId] = true; + return acc; + }, {}); + + const dataElementIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromDataElements = fromConstraint.trackerDataView?.dataElements || []; + const toDataElements = toConstraint.trackerDataView?.dataElements || []; + return [...fromDataElements, ...toDataElements]; + }).reduce((acc, dataElementId) => { + acc[dataElementId] = true; + return acc; + }, {}); + + return { + attributeIds, + dataElementIds, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js new file mode 100644 index 0000000000..ba021ba68f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js @@ -0,0 +1,81 @@ +// @flow +import type { RelationshipTypes } from '../Types'; +import { mapRelationshipElementToId } from './mapRelationshipElementToId'; + +const elementTypes = { + ATTRIBUTE: 'ATTRIBUTE', + DATA_ELEMENT: 'DATA_ELEMENT', +}; + +type Element = {| + id: string, + valueType: string, + displayName: string, +|} + +type Props = {| + relationshipTypes: RelationshipTypes, + attributes: Array, + dataElements: Array, +|} + +export const formatRelationshipTypes = ({ + relationshipTypes = [], + attributes, + dataElements, +}: Props) => { + const attributesById = attributes.reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; + return acc; + }, {}); + + const dataElementsById = dataElements.reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; + return acc; + }, {}); + + // $FlowFixMe + return relationshipTypes + .map((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = mapRelationshipElementToId( + fromConstraint.trackerDataView?.attributes, + attributesById, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const toAttributes = mapRelationshipElementToId( + toConstraint.trackerDataView?.attributes, + attributesById, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const fromDataElements = mapRelationshipElementToId( + fromConstraint.trackerDataView?.dataElements, + dataElementsById, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + const toDataElements = mapRelationshipElementToId( + toConstraint.trackerDataView?.dataElements, + dataElementsById, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + + return { + ...relationshipType, + fromConstraint: { + ...fromConstraint, + trackerDataView: { + attributes: fromAttributes, + dataElements: fromDataElements, + }, + }, + toConstraint: { + ...toConstraint, + trackerDataView: { + attributes: toAttributes, + dataElements: toDataElements, + }, + }, + }; + }) + .filter(relationshipType => relationshipType); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js new file mode 100644 index 0000000000..1752ef1214 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js @@ -0,0 +1,4 @@ +// @flow +export { extractElementIdsFromRelationshipTypes } from './extractElementIdsFromRelationshipTypes'; +export { mapRelationshipElementToId } from './mapRelationshipElementToId'; +export { formatRelationshipTypes } from './formatRelationshipTypes'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js new file mode 100644 index 0000000000..62a72ce6a9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js @@ -0,0 +1,56 @@ +// @flow +import log from 'loglevel'; +import { errorCreator } from '../../../../../capture-core-utils'; +import type { RelationshipType } from '../Types'; + +const elementTypes = { + ATTRIBUTE: 'attribute', + DATA_ELEMENT: 'dataElement', +}; + +type Elements = {| + [key: string]: { + id: string, + displayName: string, + valueType?: string, + }, +|} + +type Context = {| + relationshipType: RelationshipType, + elementType: $Values +|} + +export const mapRelationshipElementToId = ( + elementIds: ?Array, + elements: Elements, + { relationshipType, elementType }: Context) => + // $FlowFixMe + (elementIds || []) + .map((elementId) => { + const element = elements[elementId]; + + if (!element) { + log.error( + errorCreator(`${elementType} from relationshipType not found in cache`)( + { elementId, relationshipType }, + ), + ); + return null; + } + + if (!element.valueType) { + log.error( + errorCreator(`cached ${elementType} is missing value type`)( + { elementId, element }), + ); + return null; + } + + return { + id: elementId, + type: element.valueType, + displayName: element.displayName, + }; + }) + .filter(element => element); diff --git a/src/core_modules/capture-core/components/WidgetRelationships/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js similarity index 84% rename from src/core_modules/capture-core/components/WidgetRelationships/constants.js rename to src/core_modules/capture-core/components/WidgetsRelationship/constants.js index e76b14b6db..56831f9150 100644 --- a/src/core_modules/capture-core/components/WidgetRelationships/constants.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js @@ -1,6 +1,6 @@ import i18n from '@dhis2/d2-i18n'; import { dataElementTypes } from '../../metaData'; -import { convertServerToClient, convertClientToList } from '../../converters'; +import { convertClientToList, convertServerToClient } from '../../converters'; export const relationshipEntities = Object.freeze({ TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', @@ -40,3 +40,9 @@ export const getBaseConfigHeaders = { ), }], }; + +export const RELATIONSHIP_ENTITIES = Object.freeze({ + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/index.js index ede1679339..8341b80d97 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/index.js @@ -1,3 +1,4 @@ // @flow - export { RelationshipTypes, RelationshipType, TrackerDataView } from './common/Types'; +export { formatRelationshipTypes, extractElementIdsFromRelationshipTypes } from './common/utils'; +export { RELATIONSHIP_ENTITIES } from './constants'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js new file mode 100644 index 0000000000..63d64e0e29 --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js @@ -0,0 +1,26 @@ +// @flow +import { useQuery } from 'react-query'; +import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; +import type { QueryFunction, QueryKey, UseQueryOptions } from 'react-query'; +import type { Result } from './useMetadataQuery.types'; + +export const useApiDataQuery = ( + queryKey: QueryKey, + queryObject: ResourceQuery, + queryOptions: UseQueryOptions, +): Result => { + const dataEngine = useDataEngine(); + const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) + .then(response => response.theQuerykey); + return useQuery( + queryKey, + queryFn, + { + ...queryOptions, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + staleTime: 3, + cacheTime: 5, + }); +}; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js index 3aac3d847f..04779fc25b 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -1,4 +1,11 @@ // @flow -import type { UseQueryResult } from 'react-query'; +import type { UseQueryOptions, UseQueryResult } from 'react-query'; +import type { ResourceQuery } from '@dhis2/app-runtime'; + +export type ApiMetadataProps = {| + queryKey: string | Array, + QueryObject: ResourceQuery, + queryOptions?: UseQueryOptions, +|} export type Result = UseQueryResult; From 43a9c5793b4785b8e152f265e5bbf70d091fc573 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Tue, 2 May 2023 15:40:47 +0200 Subject: [PATCH 73/95] feat: first iteration of Relationships Widget --- i18n/en.pot | 16 +- src/components/App/AppPages.component.js | 26 ++- .../EnrollmentPageDefault.component.js | 5 +- .../EnrollmentPageDefault.container.js | 7 +- .../EnrollmentPageDefault.types.js | 5 +- ...kedEntityRelationshipsWrapper.component.js | 13 +- ...TrackedEntityRelationshipsWrapper.types.js | 6 +- .../AddRelationshipRefWrapper.component.js | 21 ++ .../AddRelationshipRefWrapper/index.js | 3 + .../EnrollmentEditEventPage.component.js | 188 ++++++++++-------- .../EnrollmentEditEventPage.container.js | 12 +- .../EnrollmentEditEventPage.types.js | 6 - .../useEventsRelationships.js | 63 ------ .../enrollment.actions.js | 4 +- .../useCommonEnrollmentDomainData.js | 32 +-- .../useCommonEnrollmentDomainData.types.js | 3 - .../useRelationshipTypesMetadata.js | 97 --------- .../useTEIRelationshipsWidgetMetadata.js | 26 +-- .../stagesAndEvents.types.js | 2 +- .../LinkedEntityMetadataSelector/index.js | 2 +- .../linkedEntityMetadataSelector.types.js | 38 +--- .../useApplicableTypesAndSides.js | 130 ++++++------ .../NewTrackedEntityRelationship.component.js | 153 ++++---------- .../NewTrackedEntityRelationship.container.js | 7 - .../NewTrackedEntityRelationship.types.js | 14 +- .../SearchOrgUnitSelector.component.js | 152 -------------- .../SearchOrgUnitSelector.container.js | 46 ----- ...archOrgUnitSelectorRefHandler.component.js | 18 -- .../searchOrgUnitSelector.actions.js | 30 --- .../searchOrgUnitSelector.epics.js | 82 -------- .../SearchProgramSelector.component.js | 43 ---- .../SearchProgramSelector.container.js | 26 --- .../searchProgramSelector.actions.js | 15 -- .../searchProgramSelector.selectors.js | 19 -- .../TeiSearchOLD/TeiSearch.component.js | 162 --------------- .../TeiSearchOLD/TeiSearch.container.js | 57 ------ .../TeiSearchOLD/TeiSearch.types.js | 27 --- .../TeiSearchForm/TeiSearchForm.component.js | 179 ----------------- .../TeiSearchForm/TeiSearchForm.container.js | 23 --- .../TeiSearchResults.component.js | 11 - .../TeiSearchResults.container.js | 31 --- .../TeiSearchResults.types.js | 23 --- .../TeiSearchOLD/actions/teiSearch.actions.js | 70 ------- .../TeiSearchOLD/getSearchGroups.js | 19 -- .../TeiSearchOLD/teiSearch.selectors.js | 23 --- .../ProgramSection.component.js | 39 ---- .../ProgramSelector.component.js | 42 ---- .../ProgramSection/index.js | 2 - .../ProgramSection/programSelector.types.js | 9 - .../SearchFormSection.component.js | 31 --- .../SearchFormSection/index.js | 4 - .../searchFormSection.types.js | 23 --- .../SearchFormSection/useCancelableAsyncFn.js | 68 ------- .../SearchFormSection/useSearchGroups.js | 42 ---- .../TrackedEntityFinder.component.js | 45 ----- .../TrackedEntityFinder.types.js | 16 -- .../TrackedEntityFinder/index.js | 4 - .../common/common.types.js | 8 - .../common/index.js | 1 - .../common/targetSides.js | 5 +- .../NewTrackedEntityRelationship/index.js | 2 - ...dgetTrackedEntityRelationship.component.js | 3 - .../WidgetTrackedEntityRelationship.types.js | 22 +- .../WidgetTrackedEntityRelationship/index.js | 1 - .../AddNewRelationship.component.js | 26 +++ .../AddNewRelationship.types.js | 8 + .../common/AddNewRelationship/index.js | 3 + .../RelationshipsTable.component.js | 26 +-- .../RelationshipsTables.component.js | 18 +- .../RelationshipsWidget.component.js | 47 +++-- .../common/Types/RelationshipData.types.js | 52 +++++ .../common/Types/RelationshipTypes.types.js | 34 +++- .../WidgetsRelationship/common/Types/index.js | 1 + .../common/hooks/useLinkedEntityGroups.js | 85 ++++---- .../common/hooks/useRelationshipTypes.js | 10 +- .../common/hooks/useRelationships.js | 44 +--- .../WidgetsRelationship/constants.js | 17 +- .../storeRelationshipTypes.js | 40 +++- .../enrollmentDomain.reducerDescription.js | 3 +- .../query/useApiDataQuery.js | 5 +- .../query/useMetadataQuery.js | 4 +- 81 files changed, 568 insertions(+), 2157 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js delete mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js delete mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js diff --git a/i18n/en.pot b/i18n/en.pot index fb2aedd811..fcba2b2f53 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-04-26T13:23:00.268Z\n" -"PO-Revision-Date: 2023-04-26T13:23:00.268Z\n" +"POT-Creation-Date: 2023-05-02T13:40:48.661Z\n" +"PO-Revision-Date: 2023-05-02T13:40:48.661Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1311,18 +1311,18 @@ msgstr "Stages and Events" msgid "New TEI Relationship" msgstr "New TEI Relationship" +msgid "Missing implementation step" +msgstr "Missing implementation step" + msgid "Go back without saving relationship" msgstr "Go back without saving relationship" -msgid "Link to an existing person" -msgstr "Link to an existing person" - -msgid "An error occurred" -msgstr "An error occurred" - msgid "New Relationship" msgstr "New Relationship" +msgid "Link to an existing person" +msgstr "Link to an existing person" + msgid "Something went wrong while loading relationships. Please try again later." msgstr "Something went wrong while loading relationships. Please try again later." diff --git a/src/components/App/AppPages.component.js b/src/components/App/AppPages.component.js index 2203baf361..0af49adb99 100644 --- a/src/components/App/AppPages.component.js +++ b/src/components/App/AppPages.component.js @@ -9,17 +9,21 @@ import { EnrollmentPage } from 'capture-core/components/Pages/Enrollment'; import { StageEventListPage } from 'capture-core/components/Pages/StageEvent'; import { EnrollmentEditEventPage } from 'capture-core/components/Pages/EnrollmentEditEvent'; import { EnrollmentAddEventPage } from 'capture-core/components/Pages/EnrollmentAddEvent'; +import { ReactQueryDevtools } from 'react-query/devtools'; export const AppPages = () => ( - - - - - - - - - - - + <> + + + + + + + + + + + + + ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index b006300975..f5555c07ce 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -54,7 +54,6 @@ export const EnrollmentPageDefaultPlain = ({ orgUnitId, events, enrollmentId, - relationships, stages, onDelete, onAddNew, @@ -109,12 +108,10 @@ export const EnrollmentPageDefaultPlain = ({ trackedEntityTypeId={program.trackedEntityType.id} programId={program.id} addRelationshipRenderElement={addRelationShipContainerElement} + onAddRelationship={() => {}} onOpenAddRelationship={toggleVisibility} onCloseAddRelationship={toggleVisibility} - relationships={relationships} - // relationshipTypes={relationshipTypes} teiId={teiId} - onAddRelationship={() => {}} onLinkedRecordClick={onLinkedRecordClick} /> } diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index c189554658..740e3ca289 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -22,7 +22,6 @@ import { import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { clickLinkedRecord, deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; -import { useRelationshipTypesMetadata } from '../../common/EnrollmentOverviewDomain/useRelationshipTypesMetadata'; export const EnrollmentPageDefault = () => { const history = useHistory(); @@ -35,7 +34,6 @@ export const EnrollmentPageDefault = () => { error: enrollmentsError, enrollment, attributeValues, - relationships, } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { error: programMetaDataError, programMetadata } = useProgramMetadata(programId); const stages = useProgramStages(program, programMetadata?.programStages); @@ -92,9 +90,8 @@ export const EnrollmentPageDefault = () => { history.push(`/new?${buildUrlQueryString({ orgUnitId, programId, teiId })}`); }; - const relationshipTypes = useRelationshipTypesMetadata(relationships); - const onEnrollmentError = message => dispatch(showEnrollmentError({ message })); + if (error) { return error.errorComponent; } @@ -108,8 +105,6 @@ export const EnrollmentPageDefault = () => { stages={stages} events={enrollment?.events} enrollmentId={enrollmentId} - relationships={relationships} - relationshipTypes={relationshipTypes} onAddNew={onAddNew} onDelete={onDelete} onViewAll={onViewAll} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index fad5251854..a49612cd07 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -4,7 +4,6 @@ import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/ import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; import type { Url } from '../../../../utils/url'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; -import type { InputRelationship, RelationshipType } from '../../../WidgetRelationships/common.types'; export type Props = {| program: TrackerProgram, @@ -12,8 +11,6 @@ export type Props = {| teiId: string, events: ?Array, stages?: Array, - relationships?: Array, - relationshipTypes?: Array, widgetEffects: ?WidgetEffects, hideWidgets: HideWidgets, orgUnitId: string, @@ -21,7 +18,7 @@ export type Props = {| onAddNew: () =>void, onViewAll: (stageId: string) => void, onCreateNew: (stageId: string) => void, - onEventClick: (eventId: string, stageId: string) => void, + onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, onLinkedRecordClick: (parameters: Url) => void, onEnrollmentError: (message: string) => void, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index 0fae62b1bb..6fd7a5be1c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -14,7 +14,7 @@ export const TrackedEntityRelationshipsWrapper = ({ onCloseAddRelationship, onLinkedRecordClick, }: Props) => { - const { getPrograms, relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); + const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); if (isError) { return ( @@ -31,19 +31,16 @@ export const TrackedEntityRelationshipsWrapper = ({ return ( <> [{ unique: false, fields: [{ id: 'id1', type: 'TEXT' }] }]} + // optional props + cachedRelationshipTypes={relationshipTypes} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js index 0466288853..7d2a1904f6 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js @@ -1,10 +1,14 @@ // @flow + +import type { Url } from '../../../../../utils/url'; + export type Props = {| trackedEntityTypeId: string, teiId: string, programId: string, + onAddRelationship: () => void, addRelationshipRenderElement: HTMLDivElement, onOpenAddRelationship: () => void, onCloseAddRelationship: () => void, - onLinkedRecordClick: () => void, + onLinkedRecordClick: (parameters: Url) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js new file mode 100644 index 0000000000..7235bb1a14 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js @@ -0,0 +1,21 @@ +// @flow +import React, { useEffect, useRef } from 'react'; + +type Props = { + setRelationshipRef: (HTMLDivElement) => void, +} + +export const AddRelationshipRefWrapper = ({ setRelationshipRef }: Props) => { + const renderRelationshipRef = useRef(undefined); + + // Extracting the logic to separate component because of the OrgUnitFetcher + useEffect(() => { + if (renderRelationshipRef.current) { + setRelationshipRef(renderRelationshipRef.current); + } + }, [setRelationshipRef]); + + return ( +
+ ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js new file mode 100644 index 0000000000..7400ed7ff3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js @@ -0,0 +1,3 @@ +// @flow + +export { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper.component'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 0bbf5a1aaa..0f89b45fa3 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useCallback, useState } from 'react'; import type { ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; @@ -18,11 +18,18 @@ import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; +import { + TrackedEntityRelationshipsWrapper, +} from '../Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; const styles = ({ typography }) => ({ page: { margin: spacersNum.dp16, }, + addRelationshipContainer: { + margin: spacersNum.dp16, + }, columns: { display: 'flex', }, @@ -50,13 +57,8 @@ const EnrollmentEditEventPagePain = ({ mode, programStage, teiId, - eventId, enrollmentId, programId, - relationships, - eventRelationships, - teiRelationshipTypes, - eventRelationshipTypes, enrollmentsAsOptions, trackedEntityName, teiDisplayName, @@ -76,90 +78,108 @@ const EnrollmentEditEventPagePain = ({ onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, -}: PlainProps) => ( - - -
-
- {mode === dataEntryKeys.VIEW - ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) - : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +}: PlainProps) => { + const [mainContentVisible, setMainContentVisible] = useState(true); + const [addRelationShipContainerElement, setAddRelationShipContainerElement] = useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisible(current => !current), []); + + return ( + + +
+
-
-
- {pageStatus === pageStatuses.DEFAULT && programStage && ( - +
+ {mode === dataEntryKeys.VIEW + ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) + : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +
+
+
+ {pageStatus === pageStatuses.DEFAULT && programStage && ( + + )} + {pageStatus === pageStatuses.MISSING_DATA && ( + {i18n.t('The enrollment event data could not be found')} + )} + {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( + + {i18n.t('Choose a registering unit to start reporting')} + + )} +
+
+ + + + {!hideWidgets.feedback && ( + + )} + {!hideWidgets.indicator && ( + + )} + {addRelationShipContainerElement && + {}} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + - )} - {pageStatus === pageStatuses.MISSING_DATA && ( - {i18n.t('The enrollment event data could not be found')} - )} - {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( - - {i18n.t('Choose a registering unit to start reporting')} - - )} -
-
- - - - {/* {}} - onLinkedRecordClick={onLinkedRecordClick} - /> */} - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - - +
-
-
-); + + ); +}; export const EnrollmentEditEventPageComponent: ComponentType<$Diff> = withStyles(styles)(EnrollmentEditEventPagePain); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index c86f6557db..1af2d22052 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -8,7 +8,6 @@ import { useEnrollmentEditEventPageMode } from 'capture-core/hooks'; import { useCommonEnrollmentDomainData, showEnrollmentError, updateEnrollmentEvents } from '../common/EnrollmentOverviewDomain'; import { useTeiDisplayName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; import { useProgramInfo } from '../../../hooks/useProgramInfo'; -import { useEventsRelationships } from './useEventsRelationships'; import { pageStatuses } from './EnrollmentEditEventPage.constants'; import { EnrollmentEditEventPageComponent } from './EnrollmentEditEventPage.component'; import { useWidgetDataFromStore } from '../EnrollmentAddEvent/hooks'; @@ -18,7 +17,6 @@ import { clickLinkedRecord, deleteEnrollment, fetchEnrollments } from '../Enroll import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; import { convertDateWithTimeForView, convertValue } from '../../../converters/clientToView'; import { dataElementTypes } from '../../../metaData/DataElement'; -import { useRelationshipTypesMetadata } from '../common/EnrollmentOverviewDomain/useRelationshipTypesMetadata'; import { useEvent } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; @@ -88,7 +86,7 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); }; - const { enrollment: enrollmentSite, relationships } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); + const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -110,9 +108,6 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); - const { relationships: eventRelationships } = useEventsRelationships(eventId); - const teiRelationshipTypes = useRelationshipTypesMetadata(relationships); - const eventRelationshipTypes = useRelationshipTypesMetadata(eventRelationships); const pageStatus = getPageStatus({ orgUnitId, @@ -131,12 +126,7 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm onGoBack={onGoBack} widgetEffects={outputEffects} hideWidgets={hideWidgets} - relationships={relationships} - eventRelationships={eventRelationships} - teiRelationshipTypes={teiRelationshipTypes} - eventRelationshipTypes={eventRelationshipTypes} teiId={teiId} - eventId={eventId} enrollmentId={enrollmentId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 14e93c5432..13283b925c 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,6 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { InputRelationship, RelationshipType } from '../../WidgetRelationships/common.types'; import type { Url } from '../../../utils/url'; export type PlainProps = {| @@ -9,17 +8,12 @@ export type PlainProps = {| widgetEffects: WidgetEffects, hideWidgets: HideWidgets, teiId: string, - eventId: string, enrollmentId: string, programId: string, mode: string, orgUnitId: string, trackedEntityName: string, teiDisplayName: string, - relationships?: Array, - eventRelationships?: Array, - eventRelationshipTypes?: Array, - teiRelationshipTypes?: Array, eventDate?: string, scheduleDate?: string, enrollmentsAsOptions: Array, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js deleted file mode 100644 index 9409d91bbf..0000000000 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/useEventsRelationships.js +++ /dev/null @@ -1,63 +0,0 @@ -// @flow -import { useMemo, useEffect } from 'react'; -// $FlowFixMe -import { useSelector, useDispatch } from 'react-redux'; -import { useDataQuery } from '@dhis2/app-runtime'; -import { setEventRelationshipsData } from '../Enrollment/EnrollmentPage.actions'; - -export const useEventsRelationships = (eventId: string) => { - const dispatch = useDispatch(); - - const { - eventId: storedEventId, - relationships: storedRelationships, - } = useSelector(({ enrollmentPage }) => enrollmentPage); - - const { - data, - error, - refetch, - } = useDataQuery( - useMemo( - () => ({ - eventRelationships: { - resource: 'tracker/relationships', - params: ({ variables: { eventId: updatedEventId } }) => ({ - event: updatedEventId, - fields: ['relationshipType,to,from,createdAt'], - }), - }, - }), - [], - ), - { lazy: true }, - ); - - const fetchedEventData = { - reference: data, - eventId, - relationships: data?.eventRelationships?.instances, - }; - - useEffect(() => { - if (fetchedEventData.reference) { - dispatch(setEventRelationshipsData( - fetchedEventData.eventId, - fetchedEventData.relationships, - )); - } - }, [ - dispatch, - fetchedEventData.reference, - fetchedEventData.relationships, - fetchedEventData.eventId, - ]); - - useEffect(() => { - if (storedEventId !== eventId) { - refetch({ variables: { eventId } }); - } - }, [storedEventId, eventId, refetch]); - - return { relationships: storedRelationships, error }; -}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js index fc8eea3b31..6cdbe894ba 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js @@ -15,8 +15,8 @@ export const enrollmentSiteActionTypes = { ERROR_ENROLLMENT: 'Enrollment.ErrorEnrollment', }; -export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues, relationships: Object) => - actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues, relationships }); +export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues) => + actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues }); export const updateEnrollmentEvents = (eventId: string, eventData: Object) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENTS)({ diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js index 0677654f22..5ce1534b56 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.js @@ -13,7 +13,6 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin enrollmentId: storedEnrollmentId, enrollment: storedEnrollment, attributeValues: storedAttributeValues, - relationships: storedRelationships, } = useSelector(({ enrollmentDomain }) => enrollmentDomain); const { data, error, refetch } = useDataQuery( @@ -33,32 +32,11 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin { lazy: true }, ); - const { - data: relationshipsData, - error: relationshipsError, - refetch: refetchRelationships, - } = useDataQuery( - useMemo( - () => ({ - teiRelationships: { - resource: 'tracker/relationships', - params: ({ variables: { teiId: updatedTeiId } }) => ({ - tei: updatedTeiId, - fields: ['relationshipType,to,from,createdAt'], - }), - }, - }), - [], - ), - { lazy: true }, - ); - const fetchedEnrollmentData = { reference: data, enrollment: data?.trackedEntityInstance?.enrollments ?.find(enrollment => enrollment.enrollment === enrollmentId), attributeValues: data?.trackedEntityInstance?.attributes, - relationships: relationshipsData?.teiRelationships?.instances, }; useEffect(() => { @@ -67,7 +45,6 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues .map(({ attribute, value }) => ({ id: attribute, value })), - fetchedEnrollmentData.relationships, )); } }, [ @@ -75,24 +52,21 @@ export const useCommonEnrollmentDomainData = (teiId: string, enrollmentId: strin fetchedEnrollmentData.reference, fetchedEnrollmentData.enrollment, fetchedEnrollmentData.attributeValues, - fetchedEnrollmentData.relationships, ]); useEffect(() => { if (storedEnrollmentId !== enrollmentId) { refetch({ variables: { teiId, programId } }); - refetchRelationships({ variables: { teiId } }); } - }, [refetch, refetchRelationships, storedEnrollmentId, enrollmentId, teiId, programId]); + }, [refetch, storedEnrollmentId, enrollmentId, teiId, programId]); const inEffectData = enrollmentId === storedEnrollmentId ? { enrollment: storedEnrollment, attributeValues: storedAttributeValues, - relationships: storedRelationships, - } : { enrollment: undefined, attributeValues: undefined, relationships: undefined }; + } : { enrollment: undefined, attributeValues: undefined }; return { - error: error || relationshipsError, + error, ...inEffectData, }; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 5369783615..6ee03b475f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,6 +1,4 @@ // @flow -import type { InputRelationship } from '../../../../WidgetRelationships/common.types'; - export type DataValue = { dataElement: string, value: string, @@ -55,5 +53,4 @@ export type Output = {| error?: any, enrollment?: EnrollmentData, attributeValues?: Array, - relationships?: Array |}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js deleted file mode 100644 index 3e5dffa27e..0000000000 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRelationshipTypesMetadata.js +++ /dev/null @@ -1,97 +0,0 @@ -// @flow -import { useCallback, useEffect, useState } from 'react'; -import { useDataEngine } from '@dhis2/app-runtime'; -import { makeQuerySingleResource } from 'capture-core/utils/api'; -import { userStores } from '../../../../storageControllers/stores'; -import { getUserStorageController } from '../../../../storageControllers'; -import type { InputRelationship } from '../../../WidgetRelationships/common.types'; - -export const useRelationshipTypesMetadata = (relationships?: Array) => { - const [relationshipTypesMetadata, setRelationshipTypesMetadata] = useState([]); - const dataEngine = useDataEngine(); - - const fetchDataView = useCallback(async (relType) => { - if (!relationships?.find(({ relationshipType }) => relationshipType === relType.id)) { return relType; } - const { fromConstraint, toConstraint } = relType; - - const querySingleResource = makeQuerySingleResource(dataEngine.query.bind(dataEngine)); - - const getPromise = (resource, attributes) => { - if (attributes.length && !attributes.every(att => att.valueType)) { - return querySingleResource({ - resource, - params: { - filter: `id:in:[${attributes.join(',')}]`, - fields: ['id,valueType,displayName'], - }, - }); - } - return undefined; - }; - - const fetchValues = async (constraint) => { - let requestPromise; - if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { - requestPromise = getPromise('trackedEntityAttributes', constraint.trackerDataView.attributes); - } else if (constraint.relationshipEntity === 'PROGRAM_STAGE_INSTANCE') { - requestPromise = getPromise('dataElements', constraint.trackerDataView.dataElements); - } - return requestPromise; - }; - - const mergeConstraintDataValues = (constraint, dataValues) => { - if (!dataValues) { return constraint; } - if (constraint.relationshipEntity === 'TRACKED_ENTITY_INSTANCE') { - if (constraint.trackerDataView.attributes.length - && !constraint.trackerDataView.attributes.every(att => att.valueType)) { - return { - ...constraint, - trackerDataView: { - ...constraint.trackerDataView, - attributes: constraint.trackerDataView.attributes - .map(id => dataValues.trackedEntityAttributes.find(item => item.id === id)) - .filter(item => item), - } }; - } - } else if (constraint.relationshipEntity === 'PROGRAM_STAGE_INSTANCE') { - if (constraint.trackerDataView.dataElements.length - && !constraint.trackerDataView.dataElements.every(att => att.valueType)) { - return { - ...constraint, - trackerDataView: { - ...constraint.trackerDataView, - dataElements: constraint.trackerDataView.dataElements - .map(id => dataValues.dataElements.find(item => item.id === id)) - .filter(item => item), - }, - }; - } - } - return constraint; - }; - - const [fromDataValues, toDataValues] = await Promise - .all([fetchValues(fromConstraint), fetchValues(toConstraint)]); - - return { - ...relType, - fromConstraint: mergeConstraintDataValues(fromConstraint, fromDataValues), - toConstraint: mergeConstraintDataValues(toConstraint, toDataValues), - }; - }, [dataEngine, relationships]); - - useEffect(() => { - if (relationships) { - const storageController = getUserStorageController(); - const allRelationshipTypePromises = storageController.getAll(userStores.RELATIONSHIP_TYPES); - allRelationshipTypePromises - .then(results => Promise.all(results.map(res => fetchDataView(res)))) - .then((res) => { - setRelationshipTypesMetadata(res); - // storageController.setAll(userStores.RELATIONSHIP_TYPES, res); - }); - } - }, [relationships, fetchDataView]); - - return relationshipTypesMetadata; -}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js index 2d43501e2e..4cb736b91f 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js @@ -1,9 +1,6 @@ // @flow -import { useCallback } from 'react'; import { useIndexedDBQuery } from '../../../../utils/reactQueryHelpers'; import { getUserStorageController, userStores } from '../../../../storageControllers'; -import { programCollection } from '../../../../metaDataMemoryStores'; -import { TrackerProgram } from '../../../../metaData'; import type { RelationshipTypes } from '../../../WidgetsRelationship'; import { extractElementIdsFromRelationshipTypes, @@ -13,23 +10,18 @@ import { const getRelationshipTypes = async (): Promise => { const userStorageController = getUserStorageController(); const cachedRelationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); + const { dataElementIds, attributeIds } = extractElementIdsFromRelationshipTypes(cachedRelationshipTypes); const attributes = (await userStorageController.getAll(userStores.TRACKED_ENTITY_ATTRIBUTES, { predicate: ({ id }) => attributeIds[id], project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), - })).reduce((acc, { id, valueType, displayName }) => { - acc[id] = { valueType, displayName }; - return acc; - }, {}); + })); const dataElements = (await userStorageController.getAll(userStores.DATA_ELEMENTS, { predicate: ({ id }) => dataElementIds[id], project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), - })).reduce((acc, { id, valueType, displayName }) => { - acc[id] = { valueType, displayName }; - return acc; - }, {}); + })); return formatRelationshipTypes({ relationshipTypes: cachedRelationshipTypes, @@ -39,20 +31,9 @@ const getRelationshipTypes = async (): Promise => { }; export const useTEIRelationshipsWidgetMetadata = (): { - getPrograms: GetPrograms, relationshipTypes: ?RelationshipTypes, isError: boolean, } => { - const getPrograms = useCallback((trackedEntityTypeId: string) => - [...programCollection.values()] - .filter(program => - program instanceof TrackerProgram && - program.trackedEntityType.id === trackedEntityTypeId && - program.access.data.read, - ) - .map(p => ({ id: p.id, name: p.name })), - []); - const { data: relationshipTypes, isError } = useIndexedDBQuery( ['cachedRelationshipTypes'], @@ -61,7 +42,6 @@ export const useTEIRelationshipsWidgetMetadata = (): { return { relationshipTypes, - getPrograms, isError, }; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js index c7e2453f24..9156fc0951 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js @@ -5,7 +5,7 @@ import type { Stage, StageCommonProps, Event } from './types/common.types'; type ExtractedProps = {| stages?: Array, events: ?Array, - onEventClick: (eventId: string) => void, + onEventClick: (eventId: string, stageId: string) => void, className?: string, |}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js index 0f70e74218..f4da8e02ea 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js @@ -1,3 +1,3 @@ // @flow export { LinkedEntityMetadataSelectorFromTrackedEntity } from './LinkedEntityMetadataSelector.component'; -export type { LinkedEntityMetadata, RelationshipTypes, Side } from './linkedEntityMetadataSelector.types'; +export type { LinkedEntityMetadata, Side } from './linkedEntityMetadataSelector.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js index 0014fc3933..81c8a85153 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -1,39 +1,7 @@ // @flow -import type { TargetSides } from '../MOVE_WidgetsCommon'; +import type { RelationshipType } from '../../../common/Types'; +import type { TargetSides } from '../../../common/LinkedEntityMetadataSelector'; -type TeiConstraint = $ReadOnly<{ - relationshipEntity: 'TRACKED_ENTITY_INSTANCE', - program?: { id: string }, - trackedEntityType: { id: string }, -}>; - -type OtherConstraint = $ReadOnly<{ - relationshipEntity: 'PROGRAM_STAGE_INSTANCE' | 'PROGRAM_INSTANCE', -}>; - -type RelationshipTypeUnidirectional = $ReadOnly<{ - id: string, - displayName: string, - bidirectional: false, - fromToName: string, - toFromName: void, - fromConstraint: TeiConstraint | OtherConstraint, - toConstraint: TeiConstraint | OtherConstraint, -}>; - -type RelationshipTypeBidirectional = $ReadOnly<{ - id: string, - displayName: string, - bidirectional: true, - fromToName: string, - toFromName: string, - fromConstraint: TeiConstraint | OtherConstraint, - toConstraint: TeiConstraint | OtherConstraint, -}>; - -export type RelationshipType = RelationshipTypeUnidirectional | RelationshipTypeBidirectional; - -export type RelationshipTypes = $ReadOnlyArray; export type Side = $ReadOnly<{| trackedEntityTypeId: string, @@ -59,7 +27,7 @@ export type LinkedEntityMetadata = $ReadOnly<{| |}>; export type Props = $ReadOnly<{| - relationshipTypes: RelationshipTypes, + relationshipTypes: Array, trackedEntityTypeId: string, programId: string, onSelectLinkedEntityMetadata: (linkedEntityMetadata: LinkedEntityMetadata) => void, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js index 7be52a5817..dcec3b6a79 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -1,9 +1,10 @@ // @flow import { useMemo } from 'react'; import { TARGET_SIDES } from '../common'; -import type { TargetSides } from '../MOVE_WidgetsCommon'; -import type { RelationshipType, ApplicableTypesInfo } from './linkedEntityMetadataSelector.types'; +import type { ApplicableTypesInfo } from './linkedEntityMetadataSelector.types'; +import type { RelationshipType } from '../../../common/Types'; import { RELATIONSHIP_ENTITIES } from '../../../constants'; +import type { TargetSides } from '../../../common/LinkedEntityMetadataSelector'; const isApplicableProgram = (programId, sourceProgramIds) => (!sourceProgramIds || !programId || sourceProgramIds.includes(programId)); @@ -90,79 +91,80 @@ const getApplicableTargetSidesForBidirectionalRelationshipType = ({ export const useApplicableTypesAndSides = ( relationshipTypes: $ReadOnlyArray, sourceTrackedEntityTypeId: string, - sourceProgramIds: $ReadOnlyArray): ApplicableTypesInfo => - useMemo(() => - relationshipTypes.map(({ - fromConstraint, - toConstraint, - bidirectional, - id, - displayName, - fromToName, - toFromName, - }) => { - if (fromConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE && + sourceProgramIds: $ReadOnlyArray, +): ApplicableTypesInfo => useMemo(() => + relationshipTypes.map(({ + fromConstraint, + toConstraint, + bidirectional, + id, + displayName, + fromToName, + toFromName, + }) => { + if (fromConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE && toConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { - if (!bidirectional) { - const applicable = isApplicableUnidirectionalRelationshipType( - fromConstraint, - sourceTrackedEntityTypeId, - sourceProgramIds, - ); - - if (!applicable) { - // $FlowFixMe filter - return null; - } - const { trackedEntityType, program } = toConstraint; - return { - id, - name: displayName, - sides: [{ - programId: program?.id, - trackedEntityTypeId: trackedEntityType.id, - targetSide: TARGET_SIDES.TO, - name: fromToName, - }], - }; - } - - const targetSides = getApplicableTargetSidesForBidirectionalRelationshipType( - { fromConstraint, toConstraint }, + if (!bidirectional) { + const applicable = isApplicableUnidirectionalRelationshipType( + fromConstraint, sourceTrackedEntityTypeId, sourceProgramIds, ); - if (!targetSides.length) { + if (!applicable) { // $FlowFixMe filter return null; } + const { trackedEntityType, program } = toConstraint; return { id, name: displayName, - sides: targetSides.map((targetSide) => { - const { trackedEntityTypeId, programId, name } = targetSide === TARGET_SIDES.TO ? { - trackedEntityTypeId: toConstraint.trackedEntityType.id, - programId: toConstraint.program?.id, - name: fromToName, - } : { - trackedEntityTypeId: fromConstraint.trackedEntityType.id, - programId: fromConstraint.program?.id, - name: toFromName, - }; - - return { - trackedEntityTypeId, - programId, - targetSide, - // $FlowFixMe - name, - }; - }), + sides: [{ + programId: program?.id, + trackedEntityTypeId: trackedEntityType.id, + targetSide: TARGET_SIDES.TO, + name: fromToName, + }], }; } - // $FlowFixMe filter - return null; - }).filter(applicableType => applicableType), - [relationshipTypes, sourceTrackedEntityTypeId, sourceProgramIds]); + + const targetSides = getApplicableTargetSidesForBidirectionalRelationshipType( + { fromConstraint, toConstraint }, + sourceTrackedEntityTypeId, + sourceProgramIds, + ); + + if (!targetSides.length) { + // $FlowFixMe filter + return null; + } + + return { + id, + name: displayName, + sides: targetSides.map((targetSide) => { + const { trackedEntityTypeId, programId, name } = targetSide === TARGET_SIDES.TO ? { + trackedEntityTypeId: toConstraint.trackedEntityType.id, + programId: toConstraint.program?.id, + name: fromToName, + } : { + trackedEntityTypeId: fromConstraint.trackedEntityType.id, + programId: fromConstraint.program?.id, + name: toFromName, + }; + + return { + trackedEntityTypeId, + programId, + targetSide, + // $FlowFixMe + name, + }; + }), + }; + } + // $FlowFixMe filter + return null; + }).filter(applicableType => applicableType), +[relationshipTypes, sourceTrackedEntityTypeId, sourceProgramIds]); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index ab24ecf4ba..6c38954447 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -1,5 +1,5 @@ // @flow -import React, { useCallback, useState, type ComponentType } from 'react'; +import React, { useCallback, useState, type ComponentType, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../../Widget'; @@ -12,18 +12,6 @@ import { } from './LinkedEntityMetadataSelector'; import { RetrieverModeSelector } from './RetrieverModeSelector'; import type { Props, PlainProps } from './NewTrackedEntityRelationship.types'; -import { TrackedEntityFinder } from './TrackedEntityFinder'; - -/* -import { Button, IconSearch16, IconAdd16, spacers } from '@dhis2/ui'; -import { NewTEIRelationshipStatuses } from '../WidgetTrackedEntityRelationship.const'; -import { RelationshipTypeSelector } from './RelationshipTypeSelector/RelationshipTypeSelector'; -import { creationModeStatuses } from './NewTrackedEntityRelationship.const'; -import { TeiSearch } from './TeiSearch/TeiSearch.container'; -import { - TeiRelationshipSearchResults, -} from '../../Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; -*/ const styles = { container: { @@ -50,11 +38,7 @@ const NewTrackedEntityRelationshipPlain = ({ relationshipTypes, trackedEntityTypeId, programId, - // onSave, onCancel, - getPrograms, - getSearchGroups, - getSearchGroupsAsync, classes, }: PlainProps) => { const [currentStep, setCurrentStep] = @@ -76,26 +60,32 @@ const NewTrackedEntityRelationshipPlain = ({ const handleNewRetrieverModeSelected = useCallback(() => setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY), []); - const stepContents = { - [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.id]: () => ( - - ), - [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id]: () => ( - - ), - [NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id]: () => { + const stepContents = useMemo(() => { + if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.id) { + return ( + + ); + } + if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id) { + return ( + + ); + } + + // Steps below will be implemented by new PR + /* if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { const { trackedEntityTypeId: linkedEntityTrackedEntityTypeId, programId: linkedEntityProgramId, - // $FlowFixMe business logic dictates that we will have the linkedEntityMetadata at this step + // $FlowFixMe business logic dictates that we will have the linkedEntityMetadata at this step }: LinkedEntityMetadata = selectedLinkedEntityMetadata; return ( @@ -107,8 +97,22 @@ const NewTrackedEntityRelationshipPlain = ({ getSearchGroupsAsync={getSearchGroupsAsync} /> ); - }, - }; + } */ + + return ( +
+ {i18n.t('Missing implementation step')} +
+ ); + }, [ + currentStep.id, + handleLinkedEntityMetadataSelection, + handleNewRetrieverModeSelected, + handleSearchRetrieverModeSelected, + programId, + relationshipTypes, + trackedEntityTypeId, + ]); return (
@@ -127,7 +131,7 @@ const NewTrackedEntityRelationshipPlain = ({ /> )} > - {stepContents[currentStep.id]()} + {stepContents}
); @@ -135,78 +139,3 @@ const NewTrackedEntityRelationshipPlain = ({ export const NewTrackedEntityRelationshipComponent: ComponentType = withStyles(styles)(NewTrackedEntityRelationshipPlain); - -/* - -const NewTrackedEntityRelationshipComponentPlain = (props) => { - if (pageStatus === NewTEIRelationshipStatuses.MISSING_RELATIONSHIP_TYPE) { - return ( - - ); - } - - if (pageStatus === NewTEIRelationshipStatuses.MISSING_CREATION_MODE) { - return ( -
-
- - -
-
- ); - } - - if (pageStatus === NewTEIRelationshipStatuses.LINK_TO_EXISTING) { - return ( -
- ( - - )} - /> -
- ); - } - - return

{i18n.t('An error occurred')}

; -}; - -export const NewTrackedEntityRelationshipComponent = withStyles(styles)(NewTrackedEntityRelationshipComponentPlain); - - const handleAddRelationship = useCallback((linkedTei) => { - if (selectedRelationshipType) { - const { - constraintSide, - id, - } = selectedRelationshipType; - const linkedTeiConstraintSide: string = constraintSide !== 'from' ? 'from' : 'to'; - - const serverData = { - relationships: [{ - relationshipType: id, - [constraintSide]: { - trackedEntity: teiId, - }, - [linkedTeiConstraintSide]: { - trackedEntity: linkedTei, - }, - }], - }; - -*/ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index 86845780db..3d9a38ea26 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -19,9 +19,6 @@ export const NewTrackedEntityRelationshipPlain = ({ trackedEntityTypeId, onCloseAddRelationship, onOpenAddRelationship, - getPrograms, - getSearchGroups = () => {}, - getSearchGroupsAsync = () => {}, classes, }: PlainProps) => { const [addWizardVisible, setAddWizardVisibility] = useState(false); @@ -49,16 +46,12 @@ export const NewTrackedEntityRelationshipPlain = ({ { addWizardVisible && ( ) } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index 456f2ceeff..e6c0bab063 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -1,23 +1,19 @@ // @flow -import type { RelationshipTypes } from './LinkedEntityMetadataSelector'; -import type { GetPrograms } from './common'; -import type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; + +import type { RelationshipTypes } from '../../common/Types'; export type Props = $ReadOnly<{| - addRelationshipRenderElement: HTMLElement, relationshipTypes: RelationshipTypes, trackedEntityTypeId: string, programId: string, onSave: () => void, onCancel: () => void, - onCloseAddRelationship: () => void, - onOpenAddRelationship: () => void, - getPrograms: GetPrograms, - getSearchGroups?: GetSearchGroups, - getSearchGroupsAsync?: GetSearchGroupsAsync, + onCloseAddRelationship?: () => void, + onOpenAddRelationship?: () => void, |}>; export type PlainProps = $ReadOnly<{| + addRelationshipRenderElement: HTMLElement, ...Props, ...CssClasses, |}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js deleted file mode 100644 index 931c58c08a..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js +++ /dev/null @@ -1,152 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { - SelectionBoxes, - withDefaultFieldContainer, - withLabel, - withFocusSaver, - withCalculateMessages, - withDisplayMessages, - SingleOrgUnitSelectField, -} from '../../../../FormFields/New'; - -const TeiSearchOrgUnitField = withFocusSaver()(withCalculateMessages()(withDefaultFieldContainer()(withLabel()(withDisplayMessages()(SingleOrgUnitSelectField))))); -const TeiSearchSelectionBoxes = withDefaultFieldContainer()(withLabel()(SelectionBoxes)); - -type Props = { - searchId: string, - selectedOrgUnit?: ?any, - selectedOrgUnitScope?: ?string, - treeRoots: ?Array, - treeReady: ?boolean, - treeKey: ?string, - treeSearchText?: ?string, - onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => void, - onSetOrgUnit: (searchId: string, orgUnit: ?Object) => void, - onFilterOrgUnits: (searchId: string, searchText: ?string) => void, - searchAttempted: ?boolean, -} - -const orgUnitFieldStyles = { - labelContainerStyle: { - paddingTop: 12, - flexBasis: 200, - }, - inputContainerStyle: { - flexBasis: 150, - }, -}; - -const selectionBoxesStyles = { - labelContainerStyle: { - paddingTop: 13, - flexBasis: 200, - }, - inputContainerStyle: { - flexBasis: 150, - }, -}; - -const options = [ - { - name: 'All accessible', - value: 'ACCESSIBLE', - }, - { - name: 'Selected', - value: 'SELECTED', - }, -]; - -const errorMessage = 'Please select an organisation unit'; - -export class SearchOrgUnitSelector extends React.Component { - gotoInstance: any; - - onSelectOrgUnitScope = (value: any) => { - if (value) { - this.props.onSelectOrgUnitScope(this.props.searchId, value); - } - } - onSetOrgUnit = (orgUnit: ?Object) => { - this.props.onSetOrgUnit(this.props.searchId, orgUnit); - } - renderOrgUnitScopeSelector = () => { - const { selectedOrgUnitScope } = this.props; - return ( - - ); - } - - isValid = () => this.props.selectedOrgUnitScope === 'ACCESSIBLE' || this.props.selectedOrgUnit - - validateAndScrollToIfFailed() { - const isValid = this.isValid(); - if (!isValid) { - this.goto(); - } - - return isValid; - } - - goto() { - if (this.gotoInstance) { - this.gotoInstance.scrollIntoView(); - - const scrolledY = window.scrollY; - if (scrolledY) { - // TODO: Set the modifier some other way (caused be the fixed header) - window.scroll(0, scrolledY - 48); - } - } - } - - getErrorMessage = () => { - if (!this.isValid() && this.props.searchAttempted) { - return i18n.t(errorMessage); - } - return null; - } - - handleFilterOrgUnits = (searchText: ?string) => { - this.props.onFilterOrgUnits(this.props.searchId, searchText); - } - - renderOrgUnitField = () => { - const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; - return ( - - ); - } - - render() { - const { selectedOrgUnitScope } = this.props; - return ( -
{ this.gotoInstance = gotoInstance; }} - > - {this.renderOrgUnitScopeSelector()} - {selectedOrgUnitScope !== 'ACCESSIBLE' && this.renderOrgUnitField()} -
- ); - } -} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js deleted file mode 100644 index 9842e8fd9b..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow -import { connect } from 'react-redux'; -import { - setOrgUnitScope, - setOrgUnit, - requestFilterOrgUnits, - clearOrgUnitsFilter, -} from './searchOrgUnitSelector.actions'; -import { get as getOrgUnitRoots } from '../../../../FormFields/New/Fields/OrgUnitField/orgUnitRoots.store'; -import { SearchOrgUnitSelectorRefHandler } from './SearchOrgUnitSelectorRefHandler.component'; - -const mapStateToProps = (state: ReduxState, props: Object) => { - const searchId = props.searchId; - - const filteredRoots = getOrgUnitRoots(searchId); - const roots = filteredRoots || getOrgUnitRoots('searchRoots'); - - return { - selectedOrgUnit: state.teiSearch[searchId].selectedOrgUnit, - selectedOrgUnitScope: state.teiSearch[searchId].selectedOrgUnitScope, - treeRoots: roots, - treeSearchText: state.teiSearch[searchId].orgUnitsSearchText, - treeReady: !state.teiSearch[searchId].orgUnitsLoading, - treeKey: state.teiSearch[searchId].orgUnitsSearchText || 'initial', - }; -}; - -const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ - onFilterOrgUnits: (searchId: string, searchText: string) => { - const action = searchText ? - requestFilterOrgUnits(searchId, searchText) : - clearOrgUnitsFilter(searchId); - dispatch(action); - }, - onSetOrgUnit: (searchId: string, orgUnit: ?any) => { - dispatch(setOrgUnit(searchId, orgUnit)); - }, - onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => { - dispatch(setOrgUnitScope(searchId, orgUnitScope)); - }, -}); - -// $FlowFixMe[missing-annot] automated comment -export const SearchOrgUnitSelector = connect(mapStateToProps, mapDispatchToProps)( - SearchOrgUnitSelectorRefHandler, -); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js deleted file mode 100644 index 9374a93993..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import * as React from 'react'; -import { SearchOrgUnitSelector } from './SearchOrgUnitSelector.component'; - -type Props = { - innerRef: Function, -}; - -export const SearchOrgUnitSelectorRefHandler = (props: Props) => { - const { innerRef, ...passOnProps } = props; - return ( - // $FlowFixMe[cannot-spread-inexact] automated comment - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js deleted file mode 100644 index 2208cf9233..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js +++ /dev/null @@ -1,30 +0,0 @@ -// @flow - -import { actionCreator } from '../../../../../actions/actions.utils'; - -export const actionTypes = { - TEI_SEARCH_REQUEST_FILTER_ORG_UNITS: 'TeiSearchRequestFilterOrgUnits', - TEI_SEARCH_CLEAR_ORG_UNITS_FILTER: 'TeiSearchClearOrgUnitsFilter', - TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED: 'TeiSearchFilteredOrgUnitsRetrieved', - TEI_SEARCH_FILTER_ORG_UNITS_FAILED: 'TeiSearchFilterOrgUnitsFailed', - TEI_SEARCH_SET_ORG_UNIT_SCOPE: 'TeiSearchSetOrgUnitScope', - TEI_SEARCH_SET_ORG_UNIT: 'TeiSearchSetOrgUnit', -}; - -export const setOrgUnitScope = (searchId: string, orgUnitScope: string) => - actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT_SCOPE)({ searchId, orgUnitScope }); - -export const setOrgUnit = (searchId: string, orgUnit: ?any) => - actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT)({ searchId, orgUnit }); - -export const requestFilterOrgUnits = (searchId: string, searchText: string) => - actionCreator(actionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS)({ searchId, searchText }); - -export const filteredOrgUnitsRetrieved = (searchId: string, roots: ?Array, searchText: string) => - actionCreator(actionTypes.TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED)({ searchId, roots, searchText }); - -export const filterOrgUnitsFailed = (searchId: string, error: any) => - actionCreator(actionTypes.TEI_SEARCH_FILTER_ORG_UNITS_FAILED)({ searchId, error }); - -export const clearOrgUnitsFilter = (searchId: string) => - actionCreator(actionTypes.TEI_SEARCH_CLEAR_ORG_UNITS_FILTER)({ searchId }); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js deleted file mode 100644 index f2fd91e6f8..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js +++ /dev/null @@ -1,82 +0,0 @@ -// @flow -import log from 'loglevel'; -import isArray from 'd2-utilizr/lib/isArray'; -import { from } from 'rxjs'; -import { getD2 } from 'capture-core/d2/d2Instance'; -import { errorCreator } from 'capture-core-utils'; -import { ofType } from 'redux-observable'; -import { map, concatMap, takeUntil, filter } from 'rxjs/operators'; -import { - actionTypes as teiSearchActionTypes, -} from '../actions/teiSearch.actions'; - -import { - actionTypes as searchOrgUnitActionTypes, - filterOrgUnitsFailed, - filteredOrgUnitsRetrieved, -} from './searchOrgUnitSelector.actions'; - -import { set as setStoreRoots } from '../../../../FormFields/New/Fields/OrgUnitField/orgUnitRoots.store'; - -const RETRIEVE_ERROR = 'Could not retrieve registering unit list'; - - -const isInitializeTeiSearch = (action: Object, searchId: string) => - action.type === teiSearchActionTypes.INITIALIZE_TEI_SEARCH && - action.payload.searchId === searchId; - -const isRequestFilterOrgUnits = (action: Object, searchId: string) => - action.type === searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS && - action.payload.searchId === searchId; - - -const cancelActionFilter = (action: Object, searchId: string) => { - if (isArray(action.payload)) { - return action.payload.some(innerAction => isInitializeTeiSearch(innerAction, searchId)); - } - return isInitializeTeiSearch(action, searchId) || isRequestFilterOrgUnits(action, searchId); -}; - -// get organisation units based on search criteria -export const teiSearchFilterOrgUnitsEpic = (action$: InputObservable) => - action$.pipe( - ofType(searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS), - concatMap((action) => { - const searchText = action.payload.searchText; - const searchId = action.payload.searchId; - - return from(getD2() - .models - .organisationUnits - .list({ - fields: [ - 'id,displayName,path,publicAccess,access,lastUpdated', - 'children[id,displayName,publicAccess,access,path,children::isNotEmpty]', - ].join(','), - paging: true, - withinUserSearchHierarchy: true, - query: searchText, - pageSize: 15, - }) - .then(orgUnitCollection => ({ regUnitArray: orgUnitCollection.toArray(), searchText, searchId })) - .catch(error => ({ error, searchId })), - ).pipe(takeUntil(action$.pipe(filter(a => cancelActionFilter(a, searchId))))); - }), - map((resultContainer) => { - if (resultContainer.error) { - log.error(errorCreator(RETRIEVE_ERROR)( - { error: resultContainer.error, method: 'searchRegisteringUnitListEpic' }), - ); - return filterOrgUnitsFailed(resultContainer.searchId, RETRIEVE_ERROR); - } - - const regUnitArray = resultContainer.regUnitArray; - setStoreRoots(resultContainer.searchId, regUnitArray); - const regUnits = resultContainer.regUnitArray - .map(unit => ({ - id: unit.id, - path: unit.path, - displayName: unit.displayName, - })); - return filteredOrgUnitsRetrieved(resultContainer.searchId, regUnits, resultContainer.searchText); - })); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js deleted file mode 100644 index 83cb72d687..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.component.js +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { OptionsSelectVirtualized } from '../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; -import type { - VirtualizedOptionConfig, -} from '../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; -import { withDefaultFieldContainer, withLabel } from '../../../../FormFields/New'; - -const SearchProgramField = withDefaultFieldContainer()(withLabel()(OptionsSelectVirtualized)); - -const programFieldStyles = { - labelContainerStyle: { - paddingTop: 12, - flexBasis: 200, - }, - inputContainerStyle: { - flexBasis: 150, - }, -}; - -type Props = { - searchId: string, - selectedProgramId: ?string, - onSetProgram: (searchId: string, programId: ?string) => void, - programOptions: Array, -} -export class SearchProgramSelectorComponent extends React.Component { - onSelectProgram = (programId: ?string) => { - this.props.onSetProgram(this.props.searchId, programId); - } - render() { - return ( - - ); - } -} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js deleted file mode 100644 index 16ec57dea5..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/SearchProgramSelector.container.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import { connect } from 'react-redux'; -import { SearchProgramSelectorComponent } from './SearchProgramSelector.component'; -import { startSetProgram } from './searchProgramSelector.actions'; -import { makeProgramOptionsSelector } from './searchProgramSelector.selectors'; - -const makeMapStateToProps = () => { - const getProgramOptions = makeProgramOptionsSelector(); - const mapStateToProps = (state: ReduxState, props: Object) => ({ - selectedProgramId: state.teiSearch[props.searchId]?.selectedProgramId, - programOptions: getProgramOptions(state, props), - }); - // $FlowFixMe[not-an-object] automated comment - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ - onSetProgram: (searchId: string, programId: ?string) => { - dispatch(startSetProgram(searchId, programId)); - }, -}); - -// $FlowFixMe[missing-annot] automated comment -export const SearchProgramSelector = connect(makeMapStateToProps, mapDispatchToProps)( - SearchProgramSelectorComponent, -); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js deleted file mode 100644 index 8fc8ad8358..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.actions.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow - -import { actionCreator } from '../../../../../actions/actions.utils'; - -export const batchActionTypes = { - BATCH_SET_TEI_SEARCH_PROGRAM: 'BatchSetTeiSearchProgram', -}; - -export const actionTypes = { - TEI_SEARCH_SET_PROGRAM: 'TeiSearchSetProgram', - TEI_SEARCH_START_SET_PROGRAM: 'TeiSearchStartSetProgram', -}; - -export const startSetProgram = (searchId: string, programId: ?string) => - actionCreator(actionTypes.TEI_SEARCH_START_SET_PROGRAM)({ searchId, programId }); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js deleted file mode 100644 index a207519d39..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/SearchProgramSelector/searchProgramSelector.selectors.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { createSelector } from 'reselect'; -import { programCollection } from '../../../../../metaDataMemoryStores'; -import { TrackerProgram } from '../../../../../metaData'; - -const trackedEntityTypeIdSelector = (state, props) => state.teiSearch[props.searchId].selectedTrackedEntityTypeId; - -// $FlowFixMe -export const makeProgramOptionsSelector = () => createSelector( - trackedEntityTypeIdSelector, - (trackedEntityTypeId: string) => - Array.from(programCollection.values()) - .filter(program => - program instanceof TrackerProgram && - program.trackedEntityType.id === trackedEntityTypeId && - program.access.data.read, - ) - .map(p => ({ value: p.id, label: p.name })), -); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js deleted file mode 100644 index f918e43229..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.component.js +++ /dev/null @@ -1,162 +0,0 @@ -// @flow -import React, { type ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; -import withStyles from '@material-ui/core/styles/withStyles'; -import type { SearchGroup } from '../../../../metaData'; -import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; -import { TeiSearchResults } from '../../../TeiSearch/TeiSearchResults/TeiSearchResults.container'; -import { SearchProgramSelector } from '../../../TeiSearch/SearchProgramSelector/SearchProgramSelector.container'; -import { Section, SectionHeaderSimple } from '../../../Section'; -import { ResultsPageSizeContext } from '../../../Pages/shared-contexts'; -import type { Props } from './TeiSearch.types'; - -const getStyles = (theme: Theme) => ({ - container: { - margin: theme.typography.pxToRem(10), - }, - programSection: { - backgroundColor: 'white', - maxWidth: theme.typography.pxToRem(900), - marginBottom: theme.typography.pxToRem(20), - }, - formContainerSection: { - maxWidth: theme.typography.pxToRem(900), - marginBottom: theme.typography.pxToRem(20), - }, -}); - -type State = { - programSectionOpen: boolean, -} - -class TeiSearchPlain extends React.Component { - constructor(props) { - super(props); - this.state = { programSectionOpen: true }; - } - - - getFormId = (searchGroupId: string) => { - const contextId = this.props.selectedProgramId || this.props.selectedTrackedEntityTypeId || ''; - return `${this.props.id}-${contextId}-${searchGroupId}`; - } - - handleSearch = (formId: string, searchGroupId: string) => { - const { id } = this.props; - this.props.onSearch(formId, searchGroupId, id); - } - - handleSearchResultsChangePage = (pageNumber: number) => { - this.props.onSearchResultsChangePage(this.props.id, pageNumber); - } - - handleNewSearch = () => { - this.props.onNewSearch(this.props.id); - } - - handleEditSearch = () => { - this.props.onEditSearch(this.props.id); - } - - handleSearchValidationFailed = (...args) => { - const { id } = this.props; - this.props.onSearchValidationFailed(...args, id); - } - - renderSearchForms = (searchGroups: Array) => ( -
- {this.renderProgramSection()} - {this.renderSearchGroups(searchGroups)} -
- ); - - renderProgramSection = () => { - const isCollapsed = !this.state.programSectionOpen; - return ( -
{ this.setState({ programSectionOpen: !!isCollapsed }); }} - title={i18n.t('Program')} - /> - } - > - -
- ); - } - - onChangeSectionCollapseState = (id) => { - if (this.props.openSearchGroupSection === id) { - this.props.onSetOpenSearchGroupSection(this.props.id, null); - return; - } - this.props.onSetOpenSearchGroupSection(this.props.id, id); - } - - renderSearchGroups = (searchGroups: Array) => searchGroups.map((sg, i) => { - const searchGroupId = i.toString(); - const formId = this.getFormId(searchGroupId); - const header = sg.unique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : i18n.t('Search by attributes'); - const collapsed = this.props.openSearchGroupSection !== searchGroupId; - return ( -
{ this.onChangeSectionCollapseState(searchGroupId); }} - isCollapsed={collapsed} - title={header} - />} - > - -
- ); - }) - renderSearchResult = () => { - const { - id, - searchGroups, - getResultsView, - } = this.props; - return ( - - - - ); - } - - render() { - const searchGroups = this.props.searchGroups; - - if (this.props.showResults) { - return this.renderSearchResult(); - } - - return searchGroups ? this.renderSearchForms(searchGroups) : (
); - } -} - -export const TeiSearchComponent: ComponentType<$Diff> = withStyles(getStyles)(TeiSearchPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js deleted file mode 100644 index b4cd8592bd..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.container.js +++ /dev/null @@ -1,57 +0,0 @@ -// @flow -import { type ComponentType } from 'react'; -import { connect } from 'react-redux'; -import { TeiSearchComponent } from './TeiSearch.component'; -import { - requestSearchTei, - searchFormValidationFailed, - teiNewSearch, - teiEditSearch, - teiSearchResultsChangePage, - setOpenSearchGroupSection, -} from './actions/teiSearch.actions'; -import { makeSearchGroupsSelector } from './teiSearch.selectors'; -import type { Props, OwnProps } from './TeiSearch.types'; - -const makeMapStateToProps = () => { - const searchGroupsSelector = makeSearchGroupsSelector(); - - const mapStateToProps = (state: ReduxState, props: OwnProps) => { - const searchGroups = searchGroupsSelector(state, props); - const currentTeiSearch = state.teiSearch[props.id]; - return { - searchGroups, - showResults: !!currentTeiSearch.searchResults, - selectedProgramId: currentTeiSearch.selectedProgramId, - selectedTrackedEntityTypeId: currentTeiSearch.selectedTrackedEntityTypeId, - openSearchGroupSection: currentTeiSearch.openSearchGroupSection, - }; - }; - - // $FlowFixMe[not-an-object] automated comment - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: ReduxDispatch, ownProps: OwnProps) => ({ - onSearch: (formId: string, searchGroupId: string, searchId: string) => { - dispatch(requestSearchTei(formId, searchGroupId, searchId, ownProps.resultsPageSize)); - }, - onSearchResultsChangePage: (searchId: string, pageNumber: number) => { - dispatch(teiSearchResultsChangePage(searchId, pageNumber, ownProps.resultsPageSize)); - }, - onSearchValidationFailed: (formId: string, searchGroupId: string, searchId: string) => { - dispatch(searchFormValidationFailed(formId, searchGroupId, searchId)); - }, - onNewSearch: (searchId: string) => { - dispatch(teiNewSearch(searchId)); - }, - onEditSearch: (searchId: string) => { - dispatch(teiEditSearch(searchId)); - }, - onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => { - dispatch(setOpenSearchGroupSection(searchId, searchGroupId)); - }, -}); - -export const TeiSearch: ComponentType = - connect<$Diff, OwnProps, _, _, _, _>(makeMapStateToProps, mapDispatchToProps)(TeiSearchComponent); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js deleted file mode 100644 index dffb5de485..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearch.types.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow -import { type SearchGroup } from '../../../../metaData'; - -type PropsFromRedux = {| - searchGroups: ?Array, - showResults?: ?boolean, - openSearchGroupSection: ?string, -|} - -type DispatchersFromRedux = {| - onSearch: Function, - onSearchValidationFailed: Function, - onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => void, - onSearchResultsChangePage: (searchId: string, pageNumber: number) => void, - onNewSearch: (searchId: string) => void, - onEditSearch: (searchId: string) => void, -|} - -export type OwnProps = {| - id: string, - getResultsView: Function, - resultsPageSize: number, - selectedProgramId: ?string, - selectedTrackedEntityTypeId: ?string, -|} - -export type Props = {| ...OwnProps, ...DispatchersFromRedux, ...PropsFromRedux, ...CssClasses |} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js deleted file mode 100644 index fedd33b19d..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.component.js +++ /dev/null @@ -1,179 +0,0 @@ -// @flow -import * as React from 'react'; -import log from 'loglevel'; -import { withStyles } from '@material-ui/core/styles'; -import i18n from '@dhis2/d2-i18n'; -import classNames from 'classnames'; -import { errorCreator } from 'capture-core-utils'; -import { Button } from '../../../../Buttons'; -import { D2Form } from '../../../../D2Form'; -import { SearchOrgUnitSelector } from '../../../../TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container'; -import type { SearchGroup } from '../../../../../metaData'; -import { withGotoInterface } from '../../../../FormFields/New'; - -const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); - -const getStyles = (theme: Theme) => ({ - orgUnitSection: { - backgroundColor: 'white', - padding: theme.typography.pxToRem(8), - maxWidth: theme.typography.pxToRem(892), - }, - searchButtonContainer: { - padding: theme.typography.pxToRem(10), - display: 'flex', - alignItems: 'center', - }, - minAttributesRequired: { - flexGrow: 1, - textAlign: 'right', - fontSize: theme.typography.pxToRem(14), - }, - minAttribtuesRequiredInvalid: { - color: theme.palette.error.main, - }, -}); - -type Props = { - id: string, - searchGroupId: string, - onSearch: (formId: string, searchGroupId: string) => void, - onSearchValidationFailed: (formId: string, SearchGroupId: string) => void, - searchAttempted: boolean, - searchId: string, - searchGroup: SearchGroup, - attributesWithValuesCount: number, - classes: { - container: string, - searchButtonContainer: string, - orgUnitSection: string, - minAttributesRequired: string, - minAttribtuesRequiredInvalid: string, - }, -}; - -class SearchFormPlain extends React.Component { - formInstance: any; - orgUnitSelectorInstance: SearchOrgUnitSelector; - - static errorMessages = { - NO_ITEM_SELECTED: 'No item selected', - SEARCH_FORM_MISSING: 'search form is missing. see log for details', - }; - - validNumberOfAttributes = () => { - const attributesWithValuesCount = this.props.attributesWithValuesCount; - const minAttributesRequiredToSearch = this.props.searchGroup.minAttributesRequiredToSearch; - return attributesWithValuesCount >= minAttributesRequiredToSearch; - } - - validateForm() { - if (!this.formInstance) { - log.error( - errorCreator( - SearchFormPlain.errorMessages.SEARCH_FORM_MISSING)({ Search: this }), - ); - return { - error: true, - isValid: false, - }; - } - - let isValid = this.formInstance.validateFormScrollToFirstFailedField({}); - - // $FlowFixMe[prop-missing] automated comment - if (isValid && !this.props.searchGroup.unique) isValid = this.orgUnitSelectorInstance.validateAndScrollToIfFailed(); - - if (isValid && !this.props.searchGroup.unique) isValid = this.validNumberOfAttributes(); - - return { - isValid, - error: false, - }; - } - - handleSearchAttempt = () => { - const { error: validateFormError, isValid: isFormValid } = this.validateForm(); - if (validateFormError || !isFormValid) { - this.props.onSearchValidationFailed(this.props.id, this.props.searchGroupId); - return; - } - this.props.onSearch(this.props.id, this.props.searchGroupId); - } - - getUniqueSearchButtonText = (searchForm) => { - const attributeName = searchForm.getElements()[0].formName; - return `Search ${attributeName}`; - } - - renderOrgUnitSelector = () => ( - { - this.orgUnitSelectorInstance = instance; - }} - searchId={this.props.searchId} - searchAttempted={this.props.searchAttempted} - /> - ); - - renderMinAttributesRequired = () => { - const { classes, searchAttempted, searchGroup } = this.props; - const displayInvalidNumberOfAttributes = searchAttempted && !this.validNumberOfAttributes(); - const minAttributesRequiredClass = classNames( - classes.minAttributesRequired, { - [classes.minAttribtuesRequiredInvalid]: displayInvalidNumberOfAttributes, - }, - ); - - return ( -
- {i18n.t( - 'Fill in at least {{minAttributesRequired}} attributes to search', - { - minAttributesRequired: searchGroup.minAttributesRequiredToSearch, - })} -
- ); - } - - render() { - const { searchGroup, classes, id } = this.props; - - const searchForm = searchGroup && searchGroup.searchForm; - - if (!searchForm) { - return ( -
- {SearchFormPlain.errorMessages.SEARCH_FORM_MISSING} -
- ); - } - const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) : i18n.t('Search by attributes'); - return ( -
- { this.formInstance = formInstance; }} - formFoundation={searchGroup.searchForm} - id={id} - /> - {!searchGroup.unique && this.renderOrgUnitSelector()} -
- - {!searchGroup.unique && this.renderMinAttributesRequired()} -
-
- ); - } -} - -export const TeiSearchFormComponent = withStyles(getStyles)(SearchFormPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js deleted file mode 100644 index 898d2db20c..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchForm/TeiSearchForm.container.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { connect } from 'react-redux'; -import { TeiSearchFormComponent } from './TeiSearchForm.component'; - -const getAttributesWithValuesCount = (state: ReduxState, formId: string) => { - const formValues = state.formsValues[formId] || {}; - return Object.keys(formValues).filter(key => formValues[key]).length; -}; - -const mapStateToProps = (state: ReduxState, props: Object) => { - const searchId = props.searchId; - const formId = props.id; - const formState = state.teiSearch[searchId] && state.teiSearch[searchId][formId] ? state.teiSearch[searchId][formId] : {}; - - return { - searchAttempted: formState.validationFailed, - attributesWithValuesCount: getAttributesWithValuesCount(state, formId), - }; -}; - -// $FlowSuppress -// $FlowFixMe[missing-annot] automated comment -export const TeiSearchForm = connect(mapStateToProps, () => ({}))(TeiSearchFormComponent); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js deleted file mode 100644 index 0ca0ad0668..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.component.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import React from 'react'; -import type { Props } from './TeiSearchResults.types'; - - -export const TeiSearchResultsComponent = ({ getResultsView, ...passOnProps }: Props) => ( -
- { getResultsView && getResultsView(passOnProps)} -
-); - diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js deleted file mode 100644 index 70e5c557dd..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.container.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow -import { type ComponentType } from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { TeiSearchResultsComponent } from './TeiSearchResults.component'; -import { withLoadingIndicator } from '../../../../../HOC'; -import type { OwnProps, Props } from './TeiSearchResults.types'; - -const mapStateToProps = (state: ReduxState, props: OwnProps) => { - const currentTeiSearch = state.teiSearch[props.id] || {}; - const searchResults = currentTeiSearch.searchResults || {}; - const searchValues = state.formsValues[searchResults.formId]; - const searchGroup = props.searchGroups[parseInt(searchResults.searchGroupId, 10)]; - return { - resultsLoading: searchResults.resultsLoading, - teis: searchResults.teis || [], - currentPage: searchResults.currentPage, - searchValues, - selectedProgramId: currentTeiSearch.selectedProgramId, - selectedTrackedEntityTypeId: currentTeiSearch.selectedTrackedEntityTypeId, - searchGroup, - }; -}; - -const mapDispatchToProps = () => ({}); - -export const TeiSearchResults: ComponentType = - compose( - connect(mapStateToProps, mapDispatchToProps), - withLoadingIndicator(() => ({ padding: '100px 0' }), null, props => (!props.resultsLoading)), - )(TeiSearchResultsComponent); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js deleted file mode 100644 index a9fbfae49d..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/TeiSearchResults/TeiSearchResults.types.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { type SearchGroup } from '../../../../../metaData'; - -export type OwnProps = {| - id: string, - searchGroups: any, - onChangePage: Function, - onNewSearch: Function, - onEditSearch: Function, - getResultsView: Function, -|} - -type PropsFromRedux = {| - resultsLoading: boolean, - teis: any, - currentPage: number, - searchValues: any, - selectedProgramId: string, - selectedTrackedEntityTypeId: string, - searchGroup: SearchGroup -|} - -export type Props = {|...OwnProps, ...PropsFromRedux |} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js deleted file mode 100644 index a8c87ce2c7..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/actions/teiSearch.actions.js +++ /dev/null @@ -1,70 +0,0 @@ -// @flow - -import { actionCreator } from '../../../../../actions/actions.utils'; - -export const batchActionTypes = { - BATCH_SET_TEI_SEARCH_PROGRAM_AND_TET: 'BatchSetTeiSearchProgramAndTet', - RESET_SEARCH_FORMS: 'ResetSearchForms', -}; - -export const actionTypes = { - INITIALIZE_TEI_SEARCH: 'InitializeTeiSearch', - REQUEST_SEARCH_TEI: 'RequestSearchTei', - SEARCH_FORM_VALIDATION_FAILED: 'SearchFormValidationFailed', - SEARCH_TEI_FAILED: 'SearchTeiFailed', - SEARCH_TEI_RESULT_RETRIEVED: 'SearchTeiResultRetrieved', - SET_TEI_SEARCH_PROGRAM_AND_TET: 'SetTeiSearchProgramAndTet', - TEI_NEW_SEARCH: 'TeiNewSearch', - TEI_EDIT_SEARCH: 'TeiEditSearch', - TEI_SEARCH_RESULTS_CHANGE_PAGE: 'TeiSearchResultsChangePage', - TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION: 'TeiSearchSetOpenSearchGroupSection', -}; - - -export const initializeTeiSearch = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => - actionCreator(actionTypes.INITIALIZE_TEI_SEARCH)({ searchId, programId, trackedEntityTypeId }); - -export const requestSearchTei = ( - formId: string, - searchGroupId: string, - searchId: string, - resultsPageSize: number, -) => - actionCreator(actionTypes.REQUEST_SEARCH_TEI)({ formId, searchGroupId, searchId, resultsPageSize }); - -export const searchTeiFailed = ( - formId: string, - searchGroupId: string, - searchId: string, -) => - actionCreator(actionTypes.SEARCH_TEI_FAILED)({ formId, searchGroupId, searchId }); - -export const searchTeiResultRetrieved = ( - data: any, - formId: string, - searchGroupId: string, - searchId: string, -) => - actionCreator(actionTypes.SEARCH_TEI_RESULT_RETRIEVED)({ data, formId, searchGroupId, searchId }); - -export const setProgramAndTrackedEntityType = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => - actionCreator(actionTypes.SET_TEI_SEARCH_PROGRAM_AND_TET)({ searchId, programId, trackedEntityTypeId }); - -export const searchFormValidationFailed = ( - formId: string, - searchGroupId: string, - searchId: string, -) => - actionCreator(actionTypes.SEARCH_FORM_VALIDATION_FAILED)({ formId, searchGroupId, searchId }); - -export const teiNewSearch = (searchId: string) => - actionCreator(actionTypes.TEI_NEW_SEARCH)({ searchId }); - -export const teiEditSearch = (searchId: string) => - actionCreator(actionTypes.TEI_EDIT_SEARCH)({ searchId }); - -export const teiSearchResultsChangePage = (searchId: string, pageNumber: number, resultsPageSize: number) => - actionCreator(actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE)({ searchId, pageNumber, resultsPageSize }); - -export const setOpenSearchGroupSection = (searchId: string, searchGroupId: ?string) => - actionCreator(actionTypes.TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION)({ searchId, searchGroupId }); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js deleted file mode 100644 index 79ac1f715e..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/getSearchGroups.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { - getTrackedEntityTypeThrowIfNotFound, - getTrackerProgramThrowIfNotFound, -} from '../../../../metaData'; -import type { - SearchGroup, -} from '../../../../metaData'; - - -export function getSearchGroups(trackedEntityTypeId: string, programId: ?string): Array { - if (programId) { - const program = getTrackerProgramThrowIfNotFound(programId); - return program.searchGroups; - } - const trackedEntityType = getTrackedEntityTypeThrowIfNotFound(trackedEntityTypeId); - return trackedEntityType.searchGroups; -} - diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js deleted file mode 100644 index ffec9a743d..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TeiSearchOLD/teiSearch.selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { createSelector } from 'reselect'; -import { getSearchGroups } from './getSearchGroups'; - -const trackedEntityTypeIdSelector = (state, props) => state.teiSearch[props.id].selectedTrackedEntityTypeId; -const programIdSelector = (state, props) => state.teiSearch[props.id].selectedProgramId; - -// $FlowFixMe[missing-annot] automated comment -export const makeSearchGroupsSelector = () => createSelector( - trackedEntityTypeIdSelector, - programIdSelector, - (trackedEntityTypeId: string, programId: ?string) => - getSearchGroups(trackedEntityTypeId, programId) - .sort((a, b) => { - if (a.unique === b.unique) { - return 0; - } - if (a.unique) { - return -1; - } - return 1; - }), -); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js deleted file mode 100644 index 59a17915bf..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSection.component.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow -import React, { useState, useCallback } from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { colors, spacersNum } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core'; -import { Widget } from '../../../../../Widget'; -import { ProgramSelectorForTrackedEntityFinder } from './ProgramSelector.component'; - -const styles = { - contents: { - marginLeft: spacersNum.dp16, - marginRight: spacersNum.dp16, - borderTop: `1px solid ${colors.grey400}`, - }, -}; - -const ProgramSectionPlain = ({ trackedEntityTypeId, selectedProgramId, onSelectProgram, getPrograms, classes }) => { - const [open, setOpenStatus] = useState(true); - const toggleOpen = useCallback(() => setOpenStatus(currentStatus => !currentStatus), []); - return ( - -
- -
-
- ); -}; - -export const ProgramSection = withStyles(styles)(ProgramSectionPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js deleted file mode 100644 index 6fc9d38599..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/ProgramSelector.component.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow -import React, { useMemo } from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { - OptionsSelectVirtualized, -} from '../../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; -import { withDefaultFieldContainer, withLabel } from '../../../../../FormFields/New'; -import type { Props } from './programSelector.types'; - -const ProgramSelector = withDefaultFieldContainer()(withLabel()(OptionsSelectVirtualized)); - -const programFieldStyles = { - labelContainerStyle: { - paddingTop: 12, - flexBasis: 200, - }, - inputContainerStyle: { - flexBasis: 150, - }, -}; - -export const ProgramSelectorForTrackedEntityFinder = ({ - selectedProgramId, - onSelectProgram, - getPrograms, - trackedEntityTypeId, -}: Props) => { - const options = useMemo(() => (getPrograms ? - getPrograms(trackedEntityTypeId) - .map(({ id, name }) => ({ label: name, value: id })) : []), - [getPrograms, trackedEntityTypeId]); - - return ( - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js deleted file mode 100644 index 6f046d4253..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { ProgramSection } from './ProgramSection.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js deleted file mode 100644 index 3af375c7a6..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/ProgramSection/programSelector.types.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import type { GetPrograms } from '../../common'; - -export type Props = $ReadOnly<{| - selectedProgramId: ?string, - onSelectProgram: (programId: ?string) => void, - trackedEntityTypeId: string, - getPrograms: GetPrograms, -|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js deleted file mode 100644 index 660f6b3c6f..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/SearchFormSection.component.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow - -import React from 'react'; -import { ErrorHandler, LoadingHandler } from 'capture-ui'; -import { useSearchGroups } from './useSearchGroups'; -import type { Props } from './searchFormSection.types'; - -const NextComp = ({ programId }) => ( -
- {programId} -
-); - -export const SearchFormSection = ({ programId, getSearchGroups, getSearchGroupsAsync }: Props) => { - const { searchGroups, loading, failed } = useSearchGroups(getSearchGroups, getSearchGroupsAsync, programId); - - return ( - - - - - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js deleted file mode 100644 index ab49382867..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow - -export { SearchFormSection } from './SearchFormSection.component'; -export type { GetSearchGroups, GetSearchGroupsAsync } from './searchFormSection.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js deleted file mode 100644 index bbdb8c9ba2..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/searchFormSection.types.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow - -type Field = $ReadOnly<{| - id: string, - type: string, -|}>; - -type SearchGroup = $ReadOnly<{| - unique: boolean, - fields: Array, -|}>; - -export type SearchGroups = $ReadOnlyArray; - -export type GetSearchGroups = (programId: string | null) => SearchGroups | void; - -export type GetSearchGroupsAsync = (programId: string | null) => SearchGroups; - -export type Props = $ReadOnly<{| - programId: string | null, - getSearchGroups?: GetSearchGroups, - getSearchGroupsAsync?: GetSearchGroupsAsync, -|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js deleted file mode 100644 index a1614e0533..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useCancelableAsyncFn.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow -import { useState, useCallback, useLayoutEffect, useRef } from 'react'; -import { makeCancelablePromise } from 'capture-core-utils'; - -type AsyncFn = (args: Array) => any; -type Args = Array; - -export const useCancelableAsyncFn = (asyncFn: AsyncFn, hardLoadMode: boolean, ...args: Args) => { - const [result, setResult] = useState({}); - const [error, setError] = useState(); - const [loading, setLoading] = useState(true); - const hardLoadModeStaticRef = useRef(hardLoadMode); - - const prevArgsRef = useRef(); - const currentTransactionIdRef = useRef(0); - - - if (hardLoadModeStaticRef.current) { - const prevArgs = prevArgsRef.current; - if (args.some((arg, index) => arg !== (prevArgs && prevArgs[index]))) { - prevArgsRef.current = args; - currentTransactionIdRef.current += 1; - } - } - - let cancel; - const execute = useCallback(async () => { - const promise = asyncFn(...args); - if (!(promise instanceof Promise)) { - setResult({ data: promise, transactionId: currentTransactionIdRef.current }); - setLoading(false); - } else { - let cancelablePromise; - ({ promise: cancelablePromise, cancel } = makeCancelablePromise(promise)); // eslint-disable-line - try { - const data = await cancelablePromise; - setResult({ data, transactionId: currentTransactionIdRef.current }); - setLoading(false); - } catch (errorArg) { - if (!errorArg || !errorArg.isCanceled) { - setError('An error occured. Please try again later'); - setLoading(false); - } - } - } - }, [...args]); - - useLayoutEffect(() => { - if (!hardLoadModeStaticRef.current) { - setLoading(true); - } - - if (asyncFn) { - execute(); - } else if (hardLoadModeStaticRef.current) { - setResult({ transactionId: currentTransactionIdRef.current, data: undefined }); - } else { - setLoading(false); - } - return () => cancel && cancel(); - }, [asyncFn, execute, cancel]); - - return { - data: result.data, - error, - loading: hardLoadMode ? currentTransactionIdRef.current !== result.transactionId : loading, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js deleted file mode 100644 index 2a5edb619a..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/SearchFormSection/useSearchGroups.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow -import { useCallback, useMemo } from 'react'; -import { useMetadataApiQuery, useMetadataCustomQuery } from '../../../../../../utils/reactQueryHelpers'; -import type { GetSearchGroups, GetSearchGroupsAsync, SearchGroups } from './searchFormSection.types'; - -export const useSearchGroups = ( - getSearchGroups?: GetSearchGroups, - getSearchGroupsAsync?: GetSearchGroupsAsync, - programId: string | null) => { - const searchGroupsExternal = useMemo(() => - getSearchGroups && getSearchGroups(programId), [getSearchGroups, programId]); - - const getSearchGroupsExternalAsync = useCallback((): SearchGroups => - // $FlowFixMe will never be called if the function is undefined - getSearchGroupsAsync(programId), - [programId, getSearchGroupsAsync]); - - const { data: searchGroupsExternalAsync, loading: loadingExternal, failed: failedExternal } = - useMetadataCustomQuery( - ['WidgetSearchGroupsExternal', programId], - getSearchGroupsExternalAsync, { - enabled: !searchGroupsExternal && Boolean(getSearchGroupsAsync), - }, - ); - - // TODO: Incomplete - const { data: searchGroupsInternal, loading: loadingInternal, failed: failedInternal } = - useMetadataApiQuery( - ['WidgetSearchGroups', programId], { - resource: 'program', - params: { id: '1', fields: 'id' }, - }, { - enabled: !searchGroupsExternal && !getSearchGroupsAsync, - }, - ); - - return { - searchGroups: searchGroupsExternal || searchGroupsExternalAsync || searchGroupsInternal, - loading: loadingExternal || loadingInternal, - failed: failedExternal || failedInternal, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js deleted file mode 100644 index a86b5df571..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.component.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow -import React, { useCallback, useState, type ComponentType } from 'react'; -import { spacersNum } from '@dhis2/ui'; -import withStyles from '@material-ui/core/styles/withStyles'; -import { ProgramSection } from './ProgramSection'; -import type { PlainProps, Props } from './TrackedEntityFinder.types'; -import { SearchFormSection } from './SearchFormSection'; - -const styles = { - container: { - marginLeft: spacersNum.dp16, - marginRight: spacersNum.dp16, - marginBottom: spacersNum.dp16, - }, -}; - -const TrackedEntityFinderPlain = ({ - trackedEntityTypeId, - defaultProgramId = null, - getPrograms, - getSearchGroups, - getSearchGroupsAsync, - classes, -}: PlainProps) => { - const [selectedProgramId, setProgramId] = useState(defaultProgramId); - const handleSelectProgram = useCallback(id => setProgramId(id), []); - - return ( -
- - -
- ); -}; - -export const TrackedEntityFinder: ComponentType = withStyles(styles)(TrackedEntityFinderPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js deleted file mode 100644 index cdbc2abadd..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/TrackedEntityFinder.types.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import type { GetPrograms } from '../common'; -import type { GetSearchGroups, GetSearchGroupsAsync } from './SearchFormSection'; - -export type Props = $ReadOnly<{| - trackedEntityTypeId: string, - defaultProgramId: ?string, - getPrograms: GetPrograms, - getSearchGroups?: GetSearchGroups, - getSearchGroupsAsync?: GetSearchGroupsAsync, -|}>; - -export type PlainProps = $ReadOnly<{| - ...Props, - ...CssClasses, -|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js deleted file mode 100644 index aed16dc829..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/TrackedEntityFinder/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow - -export { TrackedEntityFinder } from './TrackedEntityFinder.component'; -export type { GetSearchGroups, GetSearchGroupsAsync } from './SearchFormSection'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js deleted file mode 100644 index 4ebbda87a1..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/common.types.js +++ /dev/null @@ -1,8 +0,0 @@ -// @flow - -type Program = $ReadOnly<{| - id: string, - name: string, -|}>; - -export type GetPrograms = (trackedEntityTypeId: string) => $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js index 58de3870b0..e5ad1f726a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js @@ -1,3 +1,2 @@ // @flow export { TARGET_SIDES } from './targetSides'; -export * from './common.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js index b97e0d72ab..3880543dc2 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js @@ -1,6 +1,9 @@ // @flow -export const TARGET_SIDES = Object.freeze({ +export const TARGET_SIDES: {| + FROM: 'FROM', + TO: 'TO', +|} = Object.freeze({ FROM: 'FROM', TO: 'TO', }); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js index 60442f90dc..18ce242d75 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js @@ -1,4 +1,2 @@ // @flow export { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship.container'; -export type { GetPrograms } from './common'; -export type { GetSearchGroups, GetSearchGroupsAsync } from './TrackedEntityFinder'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js index 708c95fdcf..928cd1af9c 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js @@ -9,14 +9,12 @@ import { useRelationshipTypes } from '../common/hooks/useRelationshipTypes'; export const WidgetTrackedEntityRelationship = ({ cachedRelationshipTypes, - trackedEntityTypeId, teiId, ...passOnProps }: Props) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: relationships, isError } = useRelationships(teiId, RelationshipSearchEntities.TRACKED_ENTITY); - // TODO: Refactor this to be self contained const { relationships: linkedEntityRelationships } = useLinkedEntityGroups(teiId, relationshipTypes, relationships); if (isError) { @@ -32,7 +30,6 @@ export const WidgetTrackedEntityRelationship = ({ title={i18n.t("TEI's Relationships")} relationshipTypes={relationshipTypes} relationships={linkedEntityRelationships} - trackedEntityTypeId={trackedEntityTypeId} teiId={teiId} {...passOnProps} /> diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js index db5e5bc23a..28956cae05 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -1,29 +1,13 @@ // @flow -import type { GetPrograms, GetSearchGroups, GetSearchGroupsAsync } from './NewTrackedEntityRelationship'; -import type { RelationshipType, RelationshipTypes } from '../common/Types'; +import type { RelationshipTypes, UrlParameters } from '../common/Types'; export type Props = {| - cachedRelationshipTypes: RelationshipTypes, trackedEntityTypeId: string, teiId: string, programId: string, addRelationshipRenderElement: HTMLElement, - onLinkedRecordClick: () => void, + onLinkedRecordClick: (parameters: UrlParameters) => void, onOpenAddRelationship?: () => void, onCloseAddRelationship?: () => void, - /* - Advanced props for metadata, at some point the Widget should work without these - These are callbacks so we avoid compution before the data is actually needed, the disadvantage is that these will not automatially re-render inner components if there are changes (not needed in our app) - Obvious workarounds if needed is to add a trigger prop - We might also want to implement async versions of these (async callbacks should be called from useEffects) - */ - getPrograms: GetPrograms, - getSearchGroups?: GetSearchGroups, - getSearchGroupsAsync?: GetSearchGroupsAsync, -|} - -export type RelationshipsForCurrentTEI = {| - relationshipTypes: Array, - programId: string, - trackedEntityType: string, + cachedRelationshipTypes?: RelationshipTypes, |} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js index 25167d6943..f73a0e84d3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js @@ -1,3 +1,2 @@ // @flow export { WidgetTrackedEntityRelationship } from './WidgetTrackedEntityRelationship.component'; -export type { GetPrograms } from './NewTrackedEntityRelationship'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js new file mode 100644 index 0000000000..69fd288d13 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import { NewTrackedEntityRelationship } from '../../WidgetTrackedEntityRelationship/NewTrackedEntityRelationship'; +import type { Props } from './AddNewRelationship.types'; + +export const AddNewRelationship = ({ eventId, teiId, ...passOnProps }: Props) => { + if (teiId) { + return ( + + ); + } + + if (eventId) { + return ( +
+

Event based relationship

+

Event based relationships are not supported yet

+
+ ); + } + + return null; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.types.js new file mode 100644 index 0000000000..38001ed009 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.types.js @@ -0,0 +1,8 @@ +// @flow + +export type Props = {| + eventId?: string, + teiId?: string, + onAddRelationship: () => void, + addRelationshipRenderElement: HTMLElement, +|}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/index.js new file mode 100644 index 0000000000..cbb7f2abf5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/index.js @@ -0,0 +1,3 @@ +// @flow + +export { AddNewRelationship } from './AddNewRelationship.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js index 1e00ffd4cb..8e93da2463 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js @@ -12,13 +12,13 @@ import { spacers, } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -import type { Url } from '../../../../utils/url'; +import type { LinkedEntityData, RelationshipTableHeader, UrlParameters } from '../Types'; type Props = { - headers: Array, - linkedEntityData: Array, - onLinkedRecordClick: (parameters: Url) => void, + headers: Array, + linkedEntityData: Array, + onLinkedRecordClick: (parameters: UrlParameters) => void, ...CssClasses, } const DEFAULT_NUMBER_OF_ROW = 5; @@ -34,8 +34,7 @@ const styles = { }, }; -const RelationshipsTablePlain = (props: Props) => { - const { headers, linkedEntityData, classes, onLinkedRecordClick } = props; +const RelationshipsTablePlain = ({ headers, linkedEntityData, classes, onLinkedRecordClick }: Props) => { const [displayedRowNumber, setDisplayedRowNumber] = useState(DEFAULT_NUMBER_OF_ROW); function renderHeader() { @@ -64,13 +63,14 @@ const RelationshipsTablePlain = (props: Props) => { {headers.map(({ id }) => { const entity = values.find(item => item.id === id); - return ( onLinkedRecordClick(parameters)} - > - {entity?.value} - + return ( + onLinkedRecordClick(parameters)} + > + {entity?.value} + ); })} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js index 3fd8d19383..2839034633 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js @@ -3,12 +3,11 @@ import React, { type ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; -import type { Url } from '../../../../utils/url'; +import type { OutputRelationshipData, UrlParameters } from '../Types'; type Props = { - relationships: Object, - onAddRelationship: () => void, - onLinkedRecordClick: (parameters: Url) =>void, + relationships: Array, + onLinkedRecordClick: (parameters: UrlParameters) => void, ...CssClasses, } @@ -30,7 +29,7 @@ const styles = { overflow: 'scroll', }, }; -const RelationshipsTablesPlain = ({ relationships, classes, onLinkedRecordClick }: Props) => ( +const RelationshipsTablesPlain = ({ relationships, onLinkedRecordClick, classes }: Props) => (
-
{relationshipName}
- +
{relationshipName}
+ {/* TODO: investigate why flow expect classes here */} + {/* $FlowFixMe */} +
); })} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js index 3b4ae74c70..4c118e4d23 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js @@ -4,17 +4,17 @@ import { Chip, IconLink24, spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../../Widget'; import { RelationshipTables } from './RelationshipsTables.component'; -import type { OutputRelationship } from '../../../WidgetRelationships/common.types'; -import type { Url } from '../../../../utils/url'; -import { NewTrackedEntityRelationship } from '../../WidgetTrackedEntityRelationship/NewTrackedEntityRelationship'; +import { AddNewRelationship } from '../AddNewRelationship'; +import type { OutputRelationshipData, UrlParameters } from '../Types'; type Props = {| - relationships: Array, + relationships: Array, title: string, onAddRelationship: () => void, - onLinkedRecordClick: (parameters: Url) => void, + onLinkedRecordClick: (parameters: UrlParameters) => void, teiId?: string, eventId?: string, + addRelationshipRenderElement: HTMLElement, ...CssClasses, |} @@ -28,7 +28,15 @@ const styles = { }, }; -const RelationshipsWidgetPlain = ({ relationships, title, teiId, eventId, classes, ...passOnProps }: Props) => { +const RelationshipsWidgetPlain = ({ + relationships, + title, + teiId, + eventId, + classes, + onLinkedRecordClick, + ...passOnProps +}: Props) => { const [open, setOpenStatus] = useState(true); const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); return ( @@ -38,7 +46,9 @@ const RelationshipsWidgetPlain = ({ relationships, title, teiId, eventId, classe - + + + {title} {relationships && ( @@ -51,25 +61,18 @@ const RelationshipsWidgetPlain = ({ relationships, title, teiId, eventId, classe onClose={() => setOpenStatus(false)} open={open} > + {/* TODO: investigate why flow expect classes here */} + {/* $FlowFixMe */} - {teiId && ( - - )} - - {eventId && ( -
-

Add event relationships

-

Not implemented yet

-
- )} +
); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js new file mode 100644 index 0000000000..81ad4e8371 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js @@ -0,0 +1,52 @@ +// @flow + +export type RelationshipTableHeader = {| + id: string, + displayName: string, + convertValue: (value: any) => any, +|} + +export type UrlParameters = {| + programId?: string, + orgUnitId?: string, + teiId?: string, + enrollmentId?: string, +|} + +export type LinkedEntityData = { + id: string, + values: Array<{ id: string, value: ?string }>, + parameters: UrlParameters, +} + +export type ApiLinkedEntity = {| + event?: { + event: string, + orgUnitName: string, + status: string, + dataValues: Array<{ dataElement: string, value: string }>, + }, + trackedEntity?: { + trackedEntity: string, + orgUnitName: string, + attributes: Array<{ attribute: string, value: string }>, + } +|} + +export type InputRelationshipData = { + id: string, + relationshipName: string, + relationshipType: string, + relationship: string, + createdAt: string, + bidirectional: string, + from: ApiLinkedEntity, + to: ApiLinkedEntity, +} + +export type OutputRelationshipData = { + id: string, + relationshipName: string, + headers: Array, + linkedEntityData: Array, +} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js index 692b3ff86a..c3ccc4184b 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js @@ -5,18 +5,36 @@ export type TrackerDataView = { dataElements: Array, }; -// Should probably differentiate between the different relationshipEntities here -export type RelationshipConstraint = { - relationshipEntity: string, - trackedEntityType?: ?{ id: string }, - program?: ?{ id: string }, - programStage?: ?{ id: string }, - trackerDataView?: ?TrackerDataView, -}; +export type ElementValue = {| + attribute?: string, + dataElement?: string, + displayName: string, + valueType: string, + value: any, +|} +type CommonConstraintTypes = {| + trackerDataView: TrackerDataView, + program?: { id: string, name: string }, +|} + +export type TrackedEntityConstraint = { + ...CommonConstraintTypes, + relationshipEntity: 'TRACKED_ENTITY_INSTANCE', + trackedEntityType: { id: string, name: string }, +} + +export type ProgramStageInstanceConstraint = {| + ...CommonConstraintTypes, + relationshipEntity: 'PROGRAM_STAGE_INSTANCE', + programStage: { id: string, name: string }, +|} + +export type RelationshipConstraint = TrackedEntityConstraint | ProgramStageInstanceConstraint; export type RelationshipType = { id: string, displayName: string, + bidirectional: boolean, access: Object, toFromName: string, fromToName: string, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js index 5214eaaaa4..1182761bee 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js @@ -1,3 +1,4 @@ // @flow export type * from './RelationshipTypes.types'; +export type * from './RelationshipData.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js index 5b07347a20..13cf8a4823 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js @@ -3,20 +3,19 @@ import { useCallback, useEffect, useState } from 'react'; import log from 'loglevel'; import moment from 'moment'; import { errorCreator } from 'capture-core-utils'; -import { getProgramAndStageFromEvent, getTrackedEntityTypeThrowIfNotFound } - from '../../../../metaData'; -import type { - InputRelationship, - RelationshipData, - TEIAttribute, -} from '../../../WidgetRelationships/common.types'; -import type { DataValue } from '../../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import { getBaseConfigHeaders, relationshipEntities } from '../../constants'; import { convertServerToClient, convertClientToList } from '../../../../converters'; -import type { RelationshipTypes } from '../Types'; +import type { + ApiLinkedEntity, + ElementValue, + InputRelationshipData, ProgramStageInstanceConstraint, + RelationshipConstraint, + RelationshipType, + RelationshipTypes, TrackedEntityConstraint, +} from '../Types'; const convertAttributes = ( - attributes: Array | Array, + attributes: Array, displayFields: Array, options: Object, ): Array<{id: string, value: any}> => displayFields.map((field) => { @@ -56,17 +55,17 @@ const getDisplayFields = (linkedEntity) => { const determineLinkedEntity = ( relationshipType: RelationshipType, targetId: string, - from: RelationshipData, - to: RelationshipData, + fromEntity: ApiLinkedEntity, + toEntity: ApiLinkedEntity, ) => { const { id, toConstraint, fromConstraint, toFromName, fromToName } = relationshipType; - if ((to.trackedEntity && to.trackedEntity.trackedEntity === targetId) || (to.event && to.event.event === targetId)) { - return { side: from, constraint: fromConstraint, groupId: `${id}-from`, name: toFromName }; + if ((toEntity.trackedEntity && toEntity.trackedEntity.trackedEntity === targetId) || (toEntity.event && toEntity.event.event === targetId)) { + return { side: fromEntity, constraint: fromConstraint, groupId: `${id}-from`, name: toFromName }; } - if ((from.trackedEntity && from.trackedEntity.trackedEntity === targetId) || (from.event && from.event.event === targetId)) { - return { side: to, constraint: toConstraint, groupId: `${id}-to`, name: fromToName }; + if ((fromEntity.trackedEntity && fromEntity.trackedEntity.trackedEntity === targetId) || (fromEntity.event && fromEntity.event.event === targetId)) { + return { side: toEntity, constraint: toConstraint, groupId: `${id}-to`, name: fromToName }; } log.error(errorCreator('Relationship type is not handled')({ relationshipType })); @@ -74,7 +73,7 @@ const determineLinkedEntity = ( }; -const getLinkedRecordURLParameters = (linkedEntity: RelationshipData, relationshipType: Object) => { +const getLinkedRecordURLParameters = (linkedEntity: Object, entityConstraint: RelationshipConstraint) => { if (linkedEntity.event) { const { event: eventId, @@ -83,48 +82,45 @@ const getLinkedRecordURLParameters = (linkedEntity: RelationshipData, relationsh } = linkedEntity.event; return { eventId, programId, orgUnitId }; } else if (linkedEntity.trackedEntity) { - const programId = relationshipType.program?.id; + const programId = entityConstraint.program?.id; const { trackedEntity: teiId, orgUnit: orgUnitId } = linkedEntity.trackedEntity; return { programId, orgUnitId, teiId }; } return {}; }; -const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, relationshipType: Object, createdAt: string) => { +const getAttributeConstraintsForTEI = ( + linkedEntity: ApiLinkedEntity, + entityConstraint: RelationshipConstraint, + createdAt: string) => { if (linkedEntity.event) { - const { event: eventId, program: programId, programStage, orgUnitName, status } = linkedEntity.event; - /* - * Needs refactoring when moving to Widget Library. - * We will need to either add the program name and program stage name to the relationshipTypes - * or do some kind of callback to get the appropriate information. - */ - // $FlowFixMe - const { stage, program } = getProgramAndStageFromEvent({ - eventId, - programId, - programStageId: programStage, - }); + const { event: eventId, orgUnitName, status } = linkedEntity.event; + const { program, programStage }: ProgramStageInstanceConstraint = (entityConstraint: any); return { id: eventId, values: linkedEntity.event.dataValues, - parameters: getLinkedRecordURLParameters(linkedEntity, relationshipType), + parameters: getLinkedRecordURLParameters(linkedEntity, entityConstraint), options: { orgUnitName, status, programName: program?.name, - programStageName: stage?.stageForm?.name, + programStageName: programStage?.name, created: createdAt, }, }; } else if (linkedEntity.trackedEntity) { - const { trackedEntityType, trackedEntity, attributes } = linkedEntity.trackedEntity; - const tet = getTrackedEntityTypeThrowIfNotFound(trackedEntityType); + const { trackedEntity, attributes } = linkedEntity.trackedEntity; + const { trackedEntityType }: TrackedEntityConstraint = (entityConstraint: any); + return { id: trackedEntity, values: attributes, - parameters: getLinkedRecordURLParameters(linkedEntity, relationshipType), - options: { trackedEntityTypeName: tet.name, created: createdAt }, + parameters: getLinkedRecordURLParameters(linkedEntity, entityConstraint), + options: { + trackedEntityTypeName: trackedEntityType?.name ?? '', + created: createdAt, + }, }; } log.error(errorCreator('Relationship type is not handled')({ linkedEntity })); @@ -134,11 +130,11 @@ const getAttributeConstraintsForTEI = (linkedEntity: RelationshipData, relations const getLinkedEntityInfo = ( relationshipType: RelationshipType, targetId: string, - from: RelationshipData, - to: RelationshipData, + fromEntity: ApiLinkedEntity, + toEntity: ApiLinkedEntity, createdAt: string, ) => { - const linkedEntityData = determineLinkedEntity(relationshipType, targetId, from, to); + const linkedEntityData = determineLinkedEntity(relationshipType, targetId, fromEntity, toEntity); if (!linkedEntityData) { return undefined; } const metadata = getAttributeConstraintsForTEI(linkedEntityData.side, linkedEntityData.constraint, createdAt); @@ -152,6 +148,7 @@ const getLinkedEntityInfo = ( displayFields, parameters, groupId: linkedEntityData.groupId, + // $FlowFixMe - value should have either attribute or dataElement values: convertAttributes(values, displayFields, options), name: linkedEntityData.name, }; @@ -161,7 +158,7 @@ const getLinkedEntityInfo = ( export const useLinkedEntityGroups = ( targetId: string, relationshipTypes: ?RelationshipTypes, - relationships?: Array, + relationships?: Array, ) => { const [relationshipsByType, setRelationshipByType] = useState([]); @@ -169,12 +166,12 @@ export const useLinkedEntityGroups = ( if (relationships?.length && relationshipTypes?.length) { const linkedEntityGroups = relationships .sort((a, b) => moment.utc(b.createdAt).diff(moment.utc(a.createdAt))) - .reduce((acc, rel) => { - const { relationshipType: typeId, from, to, createdAt } = rel; + .reduce((acc, relationship) => { + const { relationshipType: typeId, from: fromEntity, to: toEntity, createdAt } = relationship; const relationshipType = relationshipTypes.find(item => item.id === typeId); if (!relationshipType) { return acc; } - const metadata = getLinkedEntityInfo(relationshipType, targetId, from, to, createdAt); + const metadata = getLinkedEntityInfo(relationshipType, targetId, fromEntity, toEntity, createdAt); if (!metadata) { return acc; } const { displayFields, id, values, parameters, groupId, name } = metadata; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js index 075bab8590..e163621ab8 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js @@ -13,13 +13,13 @@ type Element = {| const relationshipTypesQuery = { resource: 'relationshipTypes', params: { - fields: 'id,displayName,fromConstraint,toConstraint', + fields: 'id,displayName,fromToName,toFromName,fromConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],toConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]]', }, }; -export const useRelationshipTypes = (cachedRelationshipTypes: RelationshipTypes) => { +export const useRelationshipTypes = (cachedRelationshipTypes?: RelationshipTypes) => { const { data: apiRelationshipTypes, isError, isLoading } = useMetadataApiQuery( - ['relationshipTypesForRelationshipWidget'], + ['widgetRelationship', 'relationshipTypes'], relationshipTypesQuery, { enabled: !cachedRelationshipTypes?.length, @@ -56,7 +56,7 @@ export const useRelationshipTypes = (cachedRelationshipTypes: RelationshipTypes) }, [apiRelationshipTypes]); const { data: apiAttributes } = useMetadataApiQuery>( - ['attributesForRelationshipWidget'], + ['widgetRelationship', 'attributes'], attributeQuery, { enabled: !cachedRelationshipTypes?.length && !!attributeQuery, @@ -65,7 +65,7 @@ export const useRelationshipTypes = (cachedRelationshipTypes: RelationshipTypes) ); const { data: apiDataElements } = useMetadataApiQuery>( - ['dataElementsForRelationshipWidget'], + ['widgetRelationship', 'dataElements'], dataElementQuery, { enabled: !cachedRelationshipTypes?.length && !!dataElementQuery, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js index 1161ec1a69..3db57ff253 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js @@ -1,7 +1,7 @@ // @flow import { useMemo } from 'react'; import { useApiDataQuery } from '../../../../utils/reactQueryHelpers/query/useApiDataQuery'; -import type { Relationship } from '../../../Relationships/relationships.types'; +import type { InputRelationshipData } from '../Types'; export const RelationshipSearchEntities = Object.freeze({ TRACKED_ENTITY: 'trackedEntity', @@ -9,52 +9,14 @@ export const RelationshipSearchEntities = Object.freeze({ EVENT: 'event', }); -type ReturnData = Array; +type ReturnData = Array; export const useRelationships = (entityId: string, searchMode: string) => { const query = useMemo(() => ({ resource: 'tracker/relationships', params: { [searchMode]: entityId, - fields: ` - relationshipType, - createdAt, - from[ - trackedEntity[ - trackedEntity, - attributes, - program, - orgUnit, - trackedEntityType - ], - event[ - event, - dataValues, - program, - orgUnit, - orgUnitName, - status, - createdAt - ] - ], - to[ - trackedEntity[ - trackedEntity, - attributes, - program, - orgUnit, - trackedEntityType - ], - event[ - event, - dataValues, - program, - orgUnit, - orgUnitName, - status, - createdAt - ] - ]`, + fields: 'relationshipType,createdAt,from[trackedEntity[trackedEntity,attributes,program,orgUnit,trackedEntityType],event[event,dataValues,program,orgUnit,orgUnitName,status,createdAt]],to[trackedEntity[trackedEntity,attributes,program,orgUnit,trackedEntityType],event[event,dataValues,program,orgUnit,orgUnitName,status,createdAt]]', }, }), [entityId, searchMode]); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js index 56831f9150..a76b4695a2 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js @@ -1,3 +1,4 @@ +// @flow import i18n from '@dhis2/d2-i18n'; import { dataElementTypes } from '../../metaData'; import { convertClientToList, convertServerToClient } from '../../converters'; @@ -12,36 +13,40 @@ export const getBaseConfigHeaders = { [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ id: 'tetName', displayName: i18n.t('TET name'), - convertValue: props => props.trackedEntityTypeName, + convertValue: (props: any) => props.trackedEntityTypeName, }, { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList( + convertValue: (props: any) => convertClientToList( convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, ), }], [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', displayName: i18n.t('Program stage name'), - convertValue: props => props.programStageName, + convertValue: (props: any) => props.programStageName, }, { id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList( + convertValue: (props: any) => convertClientToList( convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, ), }], [relationshipEntities.PROGRAM_INSTANCE]: [{ id: 'createdDate', displayName: i18n.t('Created date'), - convertValue: props => convertClientToList( + convertValue: (props: any) => convertClientToList( convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, ), }], }; -export const RELATIONSHIP_ENTITIES = Object.freeze({ +export const RELATIONSHIP_ENTITIES: {| + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +|} = Object.freeze({ PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js index 83bd29b0e3..158ec902bb 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js @@ -6,7 +6,45 @@ export const storeRelationshipTypes = () => { const query = { resource: 'relationshipTypes', params: { - fields: 'id,displayName,bidirectional,fromToName,toFromName,fromConstraint[*],toConstraint[*],access[*]', + fields: ` + id, + displayName, + bidirectional, + fromToName, + toFromName, + fromConstraint[ + relationshipEntity, + trackerDataView, + trackedEntityType[ + id, + name + ], + program[ + id, + name + ], + programStage[ + id, + name + ] + ], + toConstraint[ + relationshipEntity, + trackerDataView, + trackedEntityType[ + id, + name + ], + program[ + id, + name + ], + programStage[ + id, + name + ] + ], + access[*]`, }, }; diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js index 832a6ea4c6..b3470cbe24 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -19,12 +19,11 @@ const { export const enrollmentDomainDesc = createReducerDescription( { - [COMMON_ENROLLMENT_SITE_DATA_SET]: (state, { payload: { enrollment, attributeValues, relationships } }) => ({ + [COMMON_ENROLLMENT_SITE_DATA_SET]: (state, { payload: { enrollment, attributeValues } }) => ({ ...state, enrollment, attributeValues, enrollmentId: enrollment?.enrollment, - relationships, }), [UPDATE_ENROLLMENT_EVENTS]: ( state, diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js index 63d64e0e29..2be2d8fbb6 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js @@ -20,7 +20,8 @@ export const useApiDataQuery = ( refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false, - staleTime: 3, - cacheTime: 5, + // TODO: Not sure what we want here either + staleTime: 4 * 60 * 1000, + cacheTime: 10 * 60 * 1000, }); }; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index b416b24f9e..86c8dbe41e 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -60,7 +60,9 @@ export const useMetadataApiQuery = ( const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) .then(response => response.theQuerykey); return useAsyncMetadata(queryKey, queryFn, { - cacheTime: 5, + // TODO: What do you think is sensible here? + cacheTime: Infinity, + staleTime: Infinity, ...queryOptions, }); }; From 7685ec3a64e3cc83762363f84f88ddbe34c16e00 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 3 May 2023 10:33:53 +0200 Subject: [PATCH 74/95] fix: code cleanup --- .../MetaDataStoreUtils/MetaDataStoreUtils.js | 8 ---- .../Enrollment/EnrollmentPage.actions.js | 4 -- .../EnrollmentPageDefault.component.js | 15 +++---- .../Relationships/Relationships.component.js | 3 +- .../Stage/StageDetail/hooks/useEventList.js | 4 +- .../stagesAndEvents.types.js | 2 +- .../NewTrackedEntityRelationship.actions.js | 24 ----------- .../NewTrackedEntityRelationship.epics.js | 38 ------------------ .../RelationshipsTable.component.js | 4 +- .../RelationshipsTables.component.js | 6 +-- .../RelationshipsWidget.component.js | 2 - .../common/Types/RelationshipData.types.js | 4 ++ .../common/hooks/useLinkedEntityGroups.js | 8 ++-- .../WidgetsRelationship/constants.js | 22 ++++------ .../storeRelationshipTypes.js | 40 +------------------ .../enrollmentPage.reducerDescription.js | 6 --- .../query/useApiDataQuery.js | 5 +-- .../query/useMetadataQuery.js | 1 - .../ErrorHandler/ErrorHandler.component.js | 25 ------------ .../ErrorHandler/errorHandler.types.js | 7 ---- .../capture-ui/ErrorHandler/index.js | 3 -- .../LoadingHandler.component.js | 10 ----- .../capture-ui/LoadingHandler/index.js | 3 -- .../LoadingHandler/loadingHandler.types.js | 7 ---- src/core_modules/capture-ui/index.js | 4 -- 25 files changed, 33 insertions(+), 222 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js delete mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js delete mode 100644 src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js delete mode 100644 src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js delete mode 100644 src/core_modules/capture-ui/ErrorHandler/index.js delete mode 100644 src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js delete mode 100644 src/core_modules/capture-ui/LoadingHandler/index.js delete mode 100644 src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js diff --git a/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js b/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js index c7162de1e6..5848dd91cf 100644 --- a/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js +++ b/src/core_modules/capture-core/MetaDataStoreUtils/MetaDataStoreUtils.js @@ -11,14 +11,6 @@ export const getCachedSingleResourceFromKeyAsync = ( return storageController.get(store, key).then(response => ({ response, ...propsToPass })); }; -export const getCachedResourceAsync = ( - store: $Values, - propsToPass?: any = {}, -) => { - const storageController = getUserStorageController(); - return storageController.getAll(store).then(response => ({ response, ...propsToPass })); -}; - export const containsKeyInStorageAsync = (store: $Values, key: string, propsToPass?: any = {}) => { const storageController = getUserStorageController(); return storageController.contains(store, key).then(response => ({ response, ...propsToPass })); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index 0edfa13b31..92edfbffe2 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -24,7 +24,6 @@ export const enrollmentPageActionTypes = { DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', - SET_EVENT_RELATIONSHIPS_DATA: 'EnrollmentPage.SetEventRelationshipsData', LINKED_RECORD_CLICK: 'EnrollmentPage.LinkedRecordClick', }; @@ -78,8 +77,5 @@ export const updateTeiDisplayName = (teiDisplayName: string) => teiDisplayName, }); -export const setEventRelationshipsData = (eventId: string, relationships: Array) => - actionCreator(enrollmentPageActionTypes.SET_EVENT_RELATIONSHIPS_DATA)({ eventId, relationships }); - export const clickLinkedRecord = (parameters: Url) => actionCreator(enrollmentPageActionTypes.LINKED_RECORD_CLICK)({ ...parameters }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index f5555c07ce..c0ccfac3c2 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType, useRef, useEffect, useState, useCallback } from 'react'; +import React, { type ComponentType, useState, useCallback } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -14,6 +14,7 @@ import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; import { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from '../../EnrollmentEditEvent/AddRelationshipRefWrapper'; const getStyles = () => ({ container: { @@ -68,20 +69,14 @@ export const EnrollmentPageDefaultPlain = ({ onEnrollmentError, }: PlainProps) => { const [mainContentVisible, setMainContentVisibility] = useState(true); - const [addRelationShipContainerElement, setAddRelationshipContainerElement] = useState(undefined); - const renderRelationshipRef = useRef(); - - useEffect(() => { - setAddRelationshipContainerElement(renderRelationshipRef.current); - }, []); + const [addRelationShipContainerElement, setAddRelationshipContainerElement] = + useState(undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); return ( <> -
+
{ return numberOfChanges > 0; } - renderRelationships = () => this.props.relationships.map(r => r && this.renderRelationship(r)) + renderRelationships = () => this.props.relationships.map(relationship => relationship && + this.renderRelationship(relationship)) renderRelationship = (relationship: Relationship) => { const { classes, onRemoveRelationship } = this.props; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js index 9fbe29d4d5..23188bd257 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js @@ -27,9 +27,9 @@ const basedFieldTypes = [ ]; const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: props.formFoundation?.getLabel('occurredAt') ?? 'test', sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: props.formFoundation?.getLabel('scheduledAt') ?? 'Test', sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, ]; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js index 9156fc0951..c7e2453f24 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.js @@ -5,7 +5,7 @@ import type { Stage, StageCommonProps, Event } from './types/common.types'; type ExtractedProps = {| stages?: Array, events: ?Array, - onEventClick: (eventId: string, stageId: string) => void, + onEventClick: (eventId: string) => void, className?: string, |}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js deleted file mode 100644 index 17f08c03c8..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.actions.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -import { actionCreator } from '../../../actions/actions.utils'; -import { effectMethods } from '../../../trackerOffline'; - -export const NewTrackedEntityRelationshipActionTypes = { - BATCH_OPEN_TEI_SEARCH: 'BatchOpenTeiSearch', - INIT_TEI_SEARCH_FOR_WIDGET: 'InitTeiSearchForWidget', - REQUEST_SAVE_RELATIONSHIP_FOR_TEI: 'RequestSaveRelationshipForTei', -}; - -export const startTeiSearchForWidget = ({ selectedRelationshipType }) => - actionCreator(NewTrackedEntityRelationshipActionTypes.INIT_TEI_SEARCH_FOR_WIDGET)({ selectedRelationshipType }); - -export const requestSaveRelationshipForTei = ({ serverData }) => - actionCreator(NewTrackedEntityRelationshipActionTypes.REQUEST_SAVE_RELATIONSHIP_FOR_TEI)({ serverData }, { - offline: { - effect: { - url: 'tracker?async=false&importStrategy=UPDATE', - method: effectMethods.POST, - data: serverData, - }, - }, - }); -*/ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js deleted file mode 100644 index cf067ce6f1..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.epics.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow -/* -import { ofType } from 'redux-observable'; -import { batchActions } from 'redux-batched-actions'; -import { map } from 'rxjs/operators'; -import { getSearchGroups } from '../../TeiSearch/getSearchGroups'; -import { getSearchFormId } from '../../TeiSearch/getSearchFormId'; -import { addFormData } from '../../D2Form/actions/form.actions'; -import { initializeTeiSearch } from '../../TeiSearch/actions/teiSearch.actions'; -import { batchActionTypes } from '../../Pages/NewRelationship/TeiRelationship/teiRelationship.actions'; -import { NewTrackedEntityRelationshipActionTypes } from './NewTrackedEntityRelationship.actions'; - -const searchId = 'relationshipTeiSearch'; - -export const openRelationshipTeiSearchForWidgetEpic = (action$: InputObservable) => - action$.pipe( - ofType(NewTrackedEntityRelationshipActionTypes.INIT_TEI_SEARCH_FOR_WIDGET), - map((action) => { - const { constraint } = action.payload.selectedRelationshipType; - - const { programId, trackedEntityType } = constraint; - const contextId = programId || trackedEntityType?.id; - - const searchGroups = getSearchGroups(trackedEntityType?.id, programId); - - - const addFormDataActions = searchGroups ? searchGroups.map((sg, i) => { - const key = getSearchFormId(searchId, contextId, i.toString()); - return addFormData(key, {}); - }) : []; - - return batchActions([ - ...addFormDataActions, - initializeTeiSearch(searchId, programId, trackedEntityType?.id), - ], batchActionTypes.BATCH_OPEN_TEI_SEARCH); - })); - -*/ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js index 8e93da2463..c1437bc203 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTable.component.js @@ -1,5 +1,5 @@ // @flow -import React, { useState, type ComponentType } from 'react'; +import React, { useState } from 'react'; import { withStyles } from '@material-ui/core'; import { DataTableBody, @@ -113,4 +113,4 @@ const RelationshipsTablePlain = ({ headers, linkedEntityData, classes, onLinkedR ); }; -export const RelationshipsTable: ComponentType = withStyles(styles)(RelationshipsTablePlain); +export const RelationshipsTable = withStyles(styles)(RelationshipsTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js index 2839034633..0adedfa360 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType } from 'react'; +import React from 'react'; import { withStyles } from '@material-ui/core'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import { RelationshipsTable } from './RelationshipsTable.component'; @@ -39,8 +39,6 @@ const RelationshipsTablesPlain = ({ relationships, onLinkedRecordClick, classes return (
{relationshipName}
- {/* TODO: investigate why flow expect classes here */} - {/* $FlowFixMe */} ); -export const RelationshipTables: ComponentType = withStyles(styles)(RelationshipsTablesPlain); +export const RelationshipTables = withStyles(styles)(RelationshipsTablesPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js index 4c118e4d23..4dbdc83a50 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js @@ -61,8 +61,6 @@ const RelationshipsWidgetPlain = ({ onClose={() => setOpenStatus(false)} open={open} > - {/* TODO: investigate why flow expect classes here */} - {/* $FlowFixMe */} , }, trackedEntity?: { trackedEntity: string, + program: string, + orgUnit: string, orgUnitName: string, attributes: Array<{ attribute: string, value: string }>, } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js index 13cf8a4823..40822ea705 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import log from 'loglevel'; import moment from 'moment'; import { errorCreator } from 'capture-core-utils'; -import { getBaseConfigHeaders, relationshipEntities } from '../../constants'; +import { getBaseConfigHeaders, RELATIONSHIP_ENTITIES } from '../../constants'; import { convertServerToClient, convertClientToList } from '../../../../converters'; import type { ApiLinkedEntity, @@ -40,9 +40,9 @@ const convertAttributes = ( const getDisplayFields = (linkedEntity) => { let displayFields; - if (linkedEntity.relationshipEntity === relationshipEntities.TRACKED_ENTITY_INSTANCE) { + if (linkedEntity.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { displayFields = linkedEntity.trackerDataView.attributes; - } else if (linkedEntity.relationshipEntity === relationshipEntities.PROGRAM_STAGE_INSTANCE) { + } else if (linkedEntity.relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { displayFields = linkedEntity.trackerDataView.dataElements; } if (!displayFields?.length) { @@ -73,7 +73,7 @@ const determineLinkedEntity = ( }; -const getLinkedRecordURLParameters = (linkedEntity: Object, entityConstraint: RelationshipConstraint) => { +const getLinkedRecordURLParameters = (linkedEntity: ApiLinkedEntity, entityConstraint: RelationshipConstraint) => { if (linkedEntity.event) { const { event: eventId, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js index a76b4695a2..eaf6d52615 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js @@ -3,14 +3,18 @@ import i18n from '@dhis2/d2-i18n'; import { dataElementTypes } from '../../metaData'; import { convertClientToList, convertServerToClient } from '../../converters'; -export const relationshipEntities = Object.freeze({ +export const RELATIONSHIP_ENTITIES: {| + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +|} = Object.freeze({ PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', }); export const getBaseConfigHeaders = { - [relationshipEntities.TRACKED_ENTITY_INSTANCE]: [{ + [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: [{ id: 'tetName', displayName: i18n.t('TET name'), convertValue: (props: any) => props.trackedEntityTypeName, @@ -21,7 +25,7 @@ export const getBaseConfigHeaders = { convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, ), }], - [relationshipEntities.PROGRAM_STAGE_INSTANCE]: [{ + [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: [{ id: 'programStageName', displayName: i18n.t('Program stage name'), convertValue: (props: any) => props.programStageName, @@ -33,7 +37,7 @@ export const getBaseConfigHeaders = { convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, ), }], - [relationshipEntities.PROGRAM_INSTANCE]: [{ + [RELATIONSHIP_ENTITIES.PROGRAM_INSTANCE]: [{ id: 'createdDate', displayName: i18n.t('Created date'), convertValue: (props: any) => convertClientToList( @@ -41,13 +45,3 @@ export const getBaseConfigHeaders = { ), }], }; - -export const RELATIONSHIP_ENTITIES: {| - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', -|} = Object.freeze({ - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', -}); diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js index 158ec902bb..be55829ea8 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js @@ -6,45 +6,7 @@ export const storeRelationshipTypes = () => { const query = { resource: 'relationshipTypes', params: { - fields: ` - id, - displayName, - bidirectional, - fromToName, - toFromName, - fromConstraint[ - relationshipEntity, - trackerDataView, - trackedEntityType[ - id, - name - ], - program[ - id, - name - ], - programStage[ - id, - name - ] - ], - toConstraint[ - relationshipEntity, - trackerDataView, - trackedEntityType[ - id, - name - ], - program[ - id, - name - ], - programStage[ - id, - name - ] - ], - access[*]`, + fields: 'id,displayName,bidirectional,fromToName,toFromName,fromConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],toConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],access[*]', }, }; diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js index 517c56cdd7..f9aef8290d 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js @@ -20,7 +20,6 @@ const { MISSING_MESSAGE_VIEW, DELETE_ENROLLMENT, UPDATE_TEI_DISPLAY_NAME, - SET_EVENT_RELATIONSHIPS_DATA, } = enrollmentPageActionTypes; export const enrollmentPageDesc = createReducerDescription({ @@ -75,11 +74,6 @@ export const enrollmentPageDesc = createReducerDescription({ teiDisplayName, }), [PAGE_CLEAN]: () => initialReducerValue, - [SET_EVENT_RELATIONSHIPS_DATA]: (state, { payload: { eventId, relationships } }) => ({ - ...state, - eventId, - relationships, - }), [DELETE_ENROLLMENT]: (state, { payload: { enrollmentId } }) => ({ ...state, enrollments: [ diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js index 2be2d8fbb6..2a5228f259 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js @@ -20,8 +20,7 @@ export const useApiDataQuery = ( refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false, - // TODO: Not sure what we want here either - staleTime: 4 * 60 * 1000, - cacheTime: 10 * 60 * 1000, + staleTime: 2 * 60 * 1000, + cacheTime: 5 * 60 * 1000, }); }; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index 86c8dbe41e..b49eb19c3e 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -60,7 +60,6 @@ export const useMetadataApiQuery = ( const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) .then(response => response.theQuerykey); return useAsyncMetadata(queryKey, queryFn, { - // TODO: What do you think is sensible here? cacheTime: Infinity, staleTime: Infinity, ...queryOptions, diff --git a/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js b/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js deleted file mode 100644 index 1236db23eb..0000000000 --- a/src/core_modules/capture-ui/ErrorHandler/ErrorHandler.component.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import React from 'react'; -import css from 'styled-jsx/css'; -import { colors, spacers } from '@dhis2/ui'; -import type { Props } from './errorHandler.types'; - -const style = css` -div { - color: ${colors.red500}; - margin: ${spacers.dp16}; -} -`; - -export const ErrorHandler = ({ error, children }: Props) => { - if (error) { - return ( -
- {error} - -
- ); - } - - return children; -}; diff --git a/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js b/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js deleted file mode 100644 index 6a951b8fdc..0000000000 --- a/src/core_modules/capture-ui/ErrorHandler/errorHandler.types.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow -import type { Node } from 'react'; - -export type Props = $ReadOnly<{| - error?: string, - children: Node, -|}>; diff --git a/src/core_modules/capture-ui/ErrorHandler/index.js b/src/core_modules/capture-ui/ErrorHandler/index.js deleted file mode 100644 index 2a4e83e8ca..0000000000 --- a/src/core_modules/capture-ui/ErrorHandler/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow - -export { ErrorHandler } from './ErrorHandler.component'; diff --git a/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js b/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js deleted file mode 100644 index 1d07febb94..0000000000 --- a/src/core_modules/capture-ui/LoadingHandler/LoadingHandler.component.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import type { Props } from './loadingHandler.types'; - -export const LoadingHandler = ({ loading, children }: Props) => { - if (loading) { - return null; - } - - return children; -}; diff --git a/src/core_modules/capture-ui/LoadingHandler/index.js b/src/core_modules/capture-ui/LoadingHandler/index.js deleted file mode 100644 index a6954365ec..0000000000 --- a/src/core_modules/capture-ui/LoadingHandler/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow - -export { LoadingHandler } from './LoadingHandler.component'; diff --git a/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js b/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js deleted file mode 100644 index 0028b4280b..0000000000 --- a/src/core_modules/capture-ui/LoadingHandler/loadingHandler.types.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow -import type { Node } from 'react'; - -export type Props = $ReadOnly<{| - loading: boolean, - children: Node, -|}>; diff --git a/src/core_modules/capture-ui/index.js b/src/core_modules/capture-ui/index.js index 67828f6c2a..d2a91dde4c 100644 --- a/src/core_modules/capture-ui/index.js +++ b/src/core_modules/capture-ui/index.js @@ -45,7 +45,3 @@ export { Button } from './Buttons/Button.component'; export { IconButton } from './IconButton'; export { NonBundledIcon } from './NonBundledIcon'; export { FlatList } from './FlatList'; - -// Generic Handlers -export { ErrorHandler } from './ErrorHandler'; -export { LoadingHandler } from './LoadingHandler'; From c3331c8e2b26ceaa175ebc70147f17981d2f00cf Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Tue, 23 May 2023 13:35:41 +0200 Subject: [PATCH 75/95] chore: review comments --- i18n/en.pot | 10 ++++++++-- .../Breadcrumbs/Breadcrumbs.component.js | 2 +- .../useApplicableTypesAndSides.js | 2 +- .../AddNewRelationship/AddNewRelationship.component.js | 5 +++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fcba2b2f53..4affb7a8fb 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-05-02T13:40:48.661Z\n" -"PO-Revision-Date: 2023-05-02T13:40:48.661Z\n" +"POT-Creation-Date: 2023-05-23T11:35:43.493Z\n" +"PO-Revision-Date: 2023-05-23T11:35:43.493Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1329,6 +1329,12 @@ msgstr "Something went wrong while loading relationships. Please try again later msgid "TEI's Relationships" msgstr "TEI's Relationships" +msgid "Event based relationship" +msgstr "Event based relationship" + +msgid "Event based relationships are not supported yet" +msgstr "Event based relationships are not supported yet" + msgid "TET name" msgstr "TET name" diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js index 678e19a88e..3bb1d3fa70 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js @@ -45,7 +45,7 @@ const FindExistingStep = ({ currentStep }) => { return ( <> - {'Search'} + {i18n.t('Search')} ); }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js index dcec3b6a79..047bd9f65d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -124,7 +124,7 @@ export const useApplicableTypesAndSides = ( programId: program?.id, trackedEntityTypeId: trackedEntityType.id, targetSide: TARGET_SIDES.TO, - name: fromToName, + name: fromToName ?? displayName, }], }; } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js index 69fd288d13..5929b414e1 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/AddNewRelationship/AddNewRelationship.component.js @@ -1,5 +1,6 @@ // @flow import React from 'react'; +import i18n from '@dhis2/d2-i18n'; import { NewTrackedEntityRelationship } from '../../WidgetTrackedEntityRelationship/NewTrackedEntityRelationship'; import type { Props } from './AddNewRelationship.types'; @@ -16,8 +17,8 @@ export const AddNewRelationship = ({ eventId, teiId, ...passOnProps }: Props) => if (eventId) { return (
-

Event based relationship

-

Event based relationships are not supported yet

+

{i18n.t('Event based relationship')}

+

{i18n.t('Event based relationships are not supported yet')}

); } From 518456ab5a78b1589c616937aac1150d0ee8aeea Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Fri, 26 May 2023 10:21:38 +0200 Subject: [PATCH 76/95] chore: review comments --- i18n/en.pot | 14 ++--- .../Enrollment/EnrollmentPage.actions.js | 6 -- .../Pages/Enrollment/EnrollmentPage.epics.js | 35 +----------- .../EnrollmentPageDefault.component.js | 2 +- .../EnrollmentPageDefault.container.js | 8 +-- .../EnrollmentPageDefault.types.js | 4 +- .../EnrollmentEditEventPage.component.js | 5 +- .../EnrollmentEditEventPage.container.js | 11 ++-- .../EnrollmentEditEventPage.types.js | 5 +- ...kedEntityRelationshipsWrapper.component.js | 5 +- ...TrackedEntityRelationshipsWrapper.types.js | 5 +- .../index.js | 0 .../common/TEIRelationshipsWidget/index.js | 1 + .../useLinkedRecordClick.js | 55 +++++++++++++++++++ .../linkedEntityMetadataSelector.types.js | 2 + .../useApplicableTypesAndSides.js | 11 +++- .../NewTrackedEntityRelationship.component.js | 7 ++- .../RetrieverModeSelector.component.js | 9 ++- .../retrieverModeSelector.types.js | 1 + ...dgetTrackedEntityRelationship.component.js | 2 +- .../WidgetTrackedEntityRelationship.types.js | 2 +- .../common/Types/RelationshipData.types.js | 1 + .../common/hooks/useRelationshipTypes.js | 14 ++--- .../common/hooks/useRelationships.js | 4 +- .../useProgramFromIndexedDB.js | 4 +- .../useTrackedEntityTypeFromIndexedDB.js | 4 +- .../utils/reactQueryHelpers/index.js | 9 ++- .../utils/reactQueryHelpers/query/index.js | 3 +- .../query/useApiDataQuery.js | 7 ++- .../query/useMetadataQuery.js | 17 +++--- .../reactQueryHelpers.const.js | 4 ++ src/epics/trackerCapture.epics.js | 2 - 32 files changed, 155 insertions(+), 104 deletions(-) rename src/core_modules/capture-core/components/Pages/{Enrollment/EnrollmentPageDefault => common/TEIRelationshipsWidget}/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js (86%) rename src/core_modules/capture-core/components/Pages/{Enrollment/EnrollmentPageDefault => common/TEIRelationshipsWidget}/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js (65%) rename src/core_modules/capture-core/components/Pages/{Enrollment/EnrollmentPageDefault => common/TEIRelationshipsWidget}/TrackedEntityRelationshipsWrapper/index.js (100%) create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js diff --git a/i18n/en.pot b/i18n/en.pot index 4affb7a8fb..c4dbbeeb1c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-05-23T11:35:43.493Z\n" -"PO-Revision-Date: 2023-05-23T11:35:43.493Z\n" +"POT-Creation-Date: 2023-05-26T08:53:52.765Z\n" +"PO-Revision-Date: 2023-05-26T08:53:52.765Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -628,9 +628,6 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" -msgid "Could not retrieve metadata. Please try again later." -msgstr "Could not retrieve metadata. Please try again later." - msgid "Program stage not found" msgstr "Program stage not found" @@ -980,6 +977,9 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Could not retrieve metadata. Please try again later." +msgstr "Could not retrieve metadata. Please try again later." + msgid "Possible duplicates found" msgstr "Possible duplicates found" @@ -1320,8 +1320,8 @@ msgstr "Go back without saving relationship" msgid "New Relationship" msgstr "New Relationship" -msgid "Link to an existing person" -msgstr "Link to an existing person" +msgid "Link to an existing {{tetName}}" +msgstr "Link to an existing {{tetName}}" msgid "Something went wrong while loading relationships. Please try again later." msgstr "Something went wrong while loading relationships. Please try again later." diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index 92edfbffe2..a1f8c3250a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -1,6 +1,5 @@ // @flow import { actionCreator } from '../../../actions/actions.utils'; -import type { Url } from '../../../utils/url'; export const enrollmentPageActionTypes = { INFORMATION_FETCH: 'EnrollmentPage.Fetch', @@ -23,8 +22,6 @@ export const enrollmentPageActionTypes = { DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', - - LINKED_RECORD_CLICK: 'EnrollmentPage.LinkedRecordClick', }; export const fetchEnrollmentPageInformation = () => @@ -76,6 +73,3 @@ export const updateTeiDisplayName = (teiDisplayName: string) => actionCreator(enrollmentPageActionTypes.UPDATE_TEI_DISPLAY_NAME)({ teiDisplayName, }); - -export const clickLinkedRecord = (parameters: Url) => - actionCreator(enrollmentPageActionTypes.LINKED_RECORD_CLICK)({ ...parameters }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index aea6eb9d75..6d905e0188 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -2,7 +2,7 @@ import { ofType } from 'redux-observable'; import { catchError, flatMap, map, startWith } from 'rxjs/operators'; import i18n from '@dhis2/d2-i18n'; -import { EMPTY, from, of } from 'rxjs'; +import { from, of } from 'rxjs'; import moment from 'moment'; import { enrollmentPageActionTypes, @@ -18,8 +18,6 @@ import { import { enrollmentAccessLevels, serverErrorMessages } from './EnrollmentPage.constants'; import { buildUrlQueryString, getLocationQuery } from '../../../utils/routing'; import { deriveTeiName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; -import { getProgramFromProgramIdThrowIfNotFound, EventProgram } - from '../../../metaData'; const sortByDate = (enrollments = []) => enrollments.sort((a, b) => moment.utc(b.enrolledAt).diff(moment.utc(a.enrolledAt))); @@ -210,34 +208,3 @@ export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxSto ), ); -export const clickLinkedRecordEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => - action$.pipe( - ofType(enrollmentPageActionTypes.LINKED_RECORD_CLICK), - flatMap(({ payload }) => { - let url; - const { programId, orgUnitId } = payload; - if (payload.eventId) { - const recordProgram = getProgramFromProgramIdThrowIfNotFound(programId); - if (recordProgram instanceof EventProgram) { - url = `/viewEvent?viewEventId=${payload.eventId}`; - } else { - url = `/enrollmentEventEdit?${buildUrlQueryString({ - orgUnitId, - eventId: payload.eventId, - })}`; - } - } else if (payload.teiId) { - url = `/enrollment?${buildUrlQueryString({ - programId, - orgUnitId, - teiId: payload.teiId, - enrollmentId: 'AUTO', - })}`; - } - if (url) { - history.push(url); - } - return EMPTY; - }, - ), - ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index c0ccfac3c2..624ad10170 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -13,7 +13,7 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; -import { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper'; +import { TrackedEntityRelationshipsWrapper } from '../../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; import { AddRelationshipRefWrapper } from '../../EnrollmentEditEvent/AddRelationshipRefWrapper'; const getStyles = () => ({ diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 740e3ca289..637c9d2fc9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -20,14 +20,16 @@ import { useRuleEffects, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; -import { clickLinkedRecord, deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; +import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useLinkedRecordClick } from '../../common/TEIRelationshipsWidget'; export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); + const { onLinkedRecordClick } = useLinkedRecordClick(); const program = useTrackerProgram(programId); const { @@ -75,10 +77,6 @@ export const EnrollmentPageDefault = () => { history.push(`/enrollmentEventEdit?${buildUrlQueryString({ orgUnitId, eventId })}`); }; - const onLinkedRecordClick = (parameters) => { - dispatch(clickLinkedRecord(parameters)); - }; - const onUpdateTeiAttributeValues = useCallback((updatedAttributeValues, teiDisplayName) => { dispatch(updateEnrollmentAttributeValues(updatedAttributeValues .map(({ attribute, value }) => ({ id: attribute, value })), diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index a49612cd07..e47f083a86 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -2,8 +2,8 @@ import type { TrackerProgram } from 'capture-core/metaData'; import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types'; import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; -import type { Url } from '../../../../utils/url'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { UrlParameters } from '../../../WidgetsRelationship/common/Types'; export type Props = {| program: TrackerProgram, @@ -20,7 +20,7 @@ export type Props = {| onCreateNew: (stageId: string) => void, onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, - onLinkedRecordClick: (parameters: Url) => void, + onLinkedRecordClick: (parameters: UrlParameters) => void, onEnrollmentError: (message: string) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 0f89b45fa3..e6df914990 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -20,7 +20,7 @@ import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; import { TrackedEntityRelationshipsWrapper, -} from '../Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper'; +} from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; const styles = ({ typography }) => ({ @@ -58,6 +58,7 @@ const EnrollmentEditEventPagePain = ({ programStage, teiId, enrollmentId, + trackedEntityTypeId, programId, enrollmentsAsOptions, trackedEntityName, @@ -154,7 +155,7 @@ const EnrollmentEditEventPagePain = ({ )} {addRelationShipContainerElement && { const eventDataConvertValue = convertDateWithTimeForView(event?.occurredAt || event?.scheduledAt); @@ -65,6 +66,8 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm const history = useHistory(); const dispatch = useDispatch(); + const { onLinkedRecordClick } = useLinkedRecordClick(); + useEffect(() => () => { dispatch(cleanUpDataEntry(dataEntryIds.ENROLLMENT_EVENT)); }, [dispatch]); @@ -94,12 +97,9 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm dispatch(updateEnrollmentEvents(eventId, eventData)); history.push(`enrollment?${buildUrlQueryString({ enrollmentId })}`); }; - const onLinkedRecordClick = (parameters) => { - dispatch(clickLinkedRecord(parameters)); - }; const { teiDisplayName } = useTeiDisplayName(teiId, programId); // $FlowFixMe - const trackedEntityName = program?.trackedEntityType?.name; + const { name: trackedEntityName, id: trackedEntityTypeId } = program?.trackedEntityType; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); const event = enrollmentSite?.events?.find(item => item.event === eventId); const eventDate = getEventDate(event); @@ -128,6 +128,7 @@ const EnrollmentEditEventPageWithContext = ({ programId, stageId, teiId, enrollm hideWidgets={hideWidgets} teiId={teiId} enrollmentId={enrollmentId} + trackedEntityTypeId={trackedEntityTypeId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} trackedEntityName={trackedEntityName} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 13283b925c..de47fabb21 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,7 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; -import type { Url } from '../../../utils/url'; +import type { UrlParameters } from '../../WidgetsRelationship/common/Types'; export type PlainProps = {| programStage: ?ProgramStage, @@ -10,6 +10,7 @@ export type PlainProps = {| teiId: string, enrollmentId: string, programId: string, + trackedEntityTypeId: string, mode: string, orgUnitId: string, trackedEntityName: string, @@ -20,7 +21,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, - onLinkedRecordClick: (parameters: Url) => void, + onLinkedRecordClick: (parameters: UrlParameters) => void, onEnrollmentError: (message: string) => void, onEnrollmentSuccess: () => void, onCancelEditEvent: () => void, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js similarity index 86% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js rename to src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index 6fd7a5be1c..08b944d584 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useTEIRelationshipsWidgetMetadata } from '../../../common/TEIRelationshipsWidget'; +import { useTEIRelationshipsWidgetMetadata } from '../index'; import { WidgetTrackedEntityRelationship } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { Props } from './TrackedEntityRelationshipsWrapper.types'; @@ -34,13 +34,12 @@ export const TrackedEntityRelationshipsWrapper = ({ programId={programId} trackedEntityTypeId={trackedEntityTypeId} teiId={teiId} - // $FlowFixMe - widget only needs partial URL params onLinkedRecordClick={onLinkedRecordClick} addRelationshipRenderElement={addRelationshipRenderElement} onOpenAddRelationship={onOpenAddRelationship} onCloseAddRelationship={onCloseAddRelationship} // optional props - cachedRelationshipTypes={relationshipTypes} + relationshipTypes={relationshipTypes} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js similarity index 65% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js rename to src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js index 7d2a1904f6..fa6aaff8c9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js @@ -1,6 +1,5 @@ // @flow - -import type { Url } from '../../../../../utils/url'; +import type { UrlParameters } from '../../../../WidgetsRelationship/common/Types'; export type Props = {| trackedEntityTypeId: string, @@ -10,5 +9,5 @@ export type Props = {| addRelationshipRenderElement: HTMLDivElement, onOpenAddRelationship: () => void, onCloseAddRelationship: () => void, - onLinkedRecordClick: (parameters: Url) => void, + onLinkedRecordClick: (parameters: UrlParameters) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js similarity index 100% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/TrackedEntityRelationshipsWrapper/index.js rename to src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js index 355ee4bfa6..0573777af3 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js @@ -1,3 +1,4 @@ // @flow export { useTEIRelationshipsWidgetMetadata } from './useTEIRelationshipsWidgetMetadata'; +export { useLinkedRecordClick } from './useLinkedRecordClick'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js new file mode 100644 index 0000000000..23710e4797 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js @@ -0,0 +1,55 @@ +// @flow + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import log from 'loglevel'; +import type { UrlParameters } from '../../../WidgetsRelationship/common/Types'; +import { EventProgram, getProgramFromProgramIdThrowIfNotFound } from '../../../../metaData'; +import { buildUrlQueryString } from '../../../../utils/routing'; +import { errorCreator } from '../../../../../capture-core-utils'; + +export const useLinkedRecordClick = () => { + const history = useHistory(); + + const onLinkedRecordClick = useCallback((urlParameters: UrlParameters) => { + let url; + const { + programId, + orgUnitId, + eventId, + teiId, + } = urlParameters; + + if (!programId || !orgUnitId) { + log.error(errorCreator('programId and orgUnitId must be provided')({ programId, orgUnitId })); + return; + } + + if (eventId) { + const recordProgram = getProgramFromProgramIdThrowIfNotFound(programId); + if (recordProgram instanceof EventProgram) { + url = `/viewEvent?viewEventId=${eventId}`; + } else { + url = `/enrollmentEventEdit?${buildUrlQueryString({ + orgUnitId, + eventId, + })}`; + } + } else if (teiId) { + url = `/enrollment?${buildUrlQueryString({ + programId, + orgUnitId, + teiId, + enrollmentId: 'AUTO', + })}`; + } + + if (url) { + history.push(url); + } + }, [history]); + + return { + onLinkedRecordClick, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js index 81c8a85153..1772a97922 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -5,6 +5,7 @@ import type { TargetSides } from '../../../common/LinkedEntityMetadataSelector'; export type Side = $ReadOnly<{| trackedEntityTypeId: string, + trackedEntityName: string, programId?: string, name: string, targetSide: TargetSides, @@ -24,6 +25,7 @@ export type LinkedEntityMetadata = $ReadOnly<{| name: string, targetSide: TargetSides, relationshipId: string, + trackedEntityName: string, |}>; export type Props = $ReadOnly<{| diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js index 047bd9f65d..838935c648 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -123,6 +123,7 @@ export const useApplicableTypesAndSides = ( sides: [{ programId: program?.id, trackedEntityTypeId: trackedEntityType.id, + trackedEntityName: trackedEntityType.name.toLowerCase(), targetSide: TARGET_SIDES.TO, name: fromToName ?? displayName, }], @@ -144,18 +145,26 @@ export const useApplicableTypesAndSides = ( id, name: displayName, sides: targetSides.map((targetSide) => { - const { trackedEntityTypeId, programId, name } = targetSide === TARGET_SIDES.TO ? { + const { + trackedEntityTypeId, + trackedEntityName, + programId, + name, + } = targetSide === TARGET_SIDES.TO ? { trackedEntityTypeId: toConstraint.trackedEntityType.id, + trackedEntityName: toConstraint.trackedEntityType.name.toLowerCase(), programId: toConstraint.program?.id, name: fromToName, } : { trackedEntityTypeId: fromConstraint.trackedEntityType.id, + trackedEntityName: fromConstraint.trackedEntityType.name.toLowerCase(), programId: fromConstraint.program?.id, name: toFromName, }; return { trackedEntityTypeId, + trackedEntityName, programId, targetSide, // $FlowFixMe diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 6c38954447..380cd7eee5 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -71,11 +71,15 @@ const NewTrackedEntityRelationshipPlain = ({ /> ); } - if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id) { + if ( + currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id + && selectedLinkedEntityMetadata?.trackedEntityName + ) { return ( ); } @@ -111,6 +115,7 @@ const NewTrackedEntityRelationshipPlain = ({ handleSearchRetrieverModeSelected, programId, relationshipTypes, + selectedLinkedEntityMetadata?.trackedEntityName, trackedEntityTypeId, ]); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js index 590878829b..a775fed023 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js @@ -16,7 +16,12 @@ const styles = { }, }; -const RetrieverModeSelectorPlain = ({ classes, onSearchSelected, onNewSelected }: PlainProps) => ( +const RetrieverModeSelectorPlain = ({ + classes, + onSearchSelected, + onNewSelected, + trackedEntityName, +}: PlainProps) => (
- ) : null; - }; - - return ( -
- - - {renderHeader()} - - - {renderRelationshipRows()} - - - - {renderShowMoreButton()} -
- ); -}; - -export const RelationshipsTable = withStyles(styles)(RelationshipsTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js deleted file mode 100644 index 0adedfa360..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsTables.component.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow -import React from 'react'; -import { withStyles } from '@material-ui/core'; -import { spacersNum, spacers, colors } from '@dhis2/ui'; -import { RelationshipsTable } from './RelationshipsTable.component'; -import type { OutputRelationshipData, UrlParameters } from '../Types'; - -type Props = { - relationships: Array, - onLinkedRecordClick: (parameters: UrlParameters) => void, - ...CssClasses, -} - -const styles = { - container: { - padding: `0 ${spacers.dp16} ${spacers.dp12} ${spacers.dp16}`, - }, - title: { - fontWeight: 500, - fontSize: 16, - color: colors.grey800, - paddingBottom: spacersNum.dp16, - }, - wrapper: { - '&:not(:last-first)': { - paddingTop: spacersNum.dp24, - }, - paddingBottom: spacersNum.dp16, - overflow: 'scroll', - }, -}; -const RelationshipsTablesPlain = ({ relationships, onLinkedRecordClick, classes }: Props) => ( -
- {relationships && relationships.map((relationship) => { - const { relationshipName, id, ...passOnProps } = relationship; - return ( -
-
{relationshipName}
- -
- ); - })} -
); - -export const RelationshipTables = withStyles(styles)(RelationshipsTablesPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/index.js deleted file mode 100644 index f111a734de..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { RelationshipsWidget } from './RelationshipsWidget.component'; - diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js new file mode 100644 index 0000000000..072177e943 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js @@ -0,0 +1,45 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { spacersNum, spacers, colors } from '@dhis2/ui'; +import { LinkedEntityTable } from './LinkedEntityTable.component'; +import type { Props, StyledProps } from './linkedEntitiesViewer.types'; + +const styles = { + container: { + padding: `0 ${spacers.dp16} ${spacers.dp12} ${spacers.dp16}`, + }, + title: { + fontWeight: 500, + fontSize: 16, + color: colors.grey800, + paddingBottom: spacersNum.dp16, + }, + wrapper: { + paddingBottom: spacersNum.dp16, + }, +}; + + +const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, classes }: StyledProps) => ( +
+ {groupedLinkedEntities?.map((linkedEntityGroup) => { + const { id, name, linkedEntities, columns, context } = linkedEntityGroup; + return ( +
+
{name}
+ +
+ ); + })} +
); + +export const LinkedEntitiesViewer: ComponentType = withStyles(styles)(LinkedEntitiesViewerPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js new file mode 100644 index 0000000000..a286b51ff4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js @@ -0,0 +1,71 @@ +// @flow +import React, { useState, useMemo, type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { + DataTable, + Button, + spacers, +} from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { LinkedEntityTableHeader } from './LinkedEntityTableHeader.component'; +import { LinkedEntityTableBody } from './LinkedEntityTableBody.component'; +import type { Props, StyledProps } from './linkedEntityTable.types'; + +const DEFAULT_VISIBLE_ROWS_COUNT = 5; + +const styles = { + button: { + marginTop: `${spacers.dp8}`, + }, + dataTableWrapper: { + overflowY: 'auto', + whiteSpace: 'nowrap', + }, +}; + +const LinkedEntityTablePlain = ({ linkedEntities, columns, onLinkedRecordClick, context, classes }: StyledProps) => { + const [visibleRowsCount, setVisibleRowsCount] = useState(DEFAULT_VISIBLE_ROWS_COUNT); + + const visibleLinkedEntities = useMemo(() => + linkedEntities.slice(0, visibleRowsCount), + [linkedEntities, visibleRowsCount]); + + const showMoreButtonVisible = linkedEntities.length > visibleRowsCount; + + return ( +
+ + + + + {showMoreButtonVisible && ( + + )} +
+ ); +}; + +export const LinkedEntityTable: ComponentType = withStyles(styles)(LinkedEntityTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js new file mode 100644 index 0000000000..a8bbed4ce4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js @@ -0,0 +1,56 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { + DataTableBody, + DataTableRow, + DataTableCell, +} from '@dhis2/ui'; +import { convertServerToClient, convertClientToList } from '../../../../converters'; +import type { Props, StyledProps } from './linkedEntityTableBody.types'; + +const styles = { + row: { + '&:hover': { + cursor: 'pointer', + }, + }, +}; + +const LinkedEntityTableBodyPlain = ({ + linkedEntities, + columns, + onLinkedRecordClick, + context, + classes, +}: StyledProps) => ( + + { + linkedEntities + .map(({ id: entityId, values, baseValues, navigation }) => ( + + { + // $FlowFixMe flow doesn't like destructering + columns.map(({ id, type, convertValue }) => { + const value = type ? + convertClientToList(convertServerToClient(values[id], type), type) : + convertValue(baseValues?.[id] ?? context.display[id]); + + return ( + onLinkedRecordClick({ ...context.navigation, ...navigation })} + > + {value} + + ); + })} + + )) + } + +); + +export const LinkedEntityTableBody: ComponentType = withStyles(styles)(LinkedEntityTableBodyPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js new file mode 100644 index 0000000000..e7681a6867 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js @@ -0,0 +1,25 @@ +// @flow +import React from 'react'; +import { + DataTableHead, + DataTableRow, + DataTableColumnHeader, +} from '@dhis2/ui'; +import type { Props } from './linkedEntityTableHeader.types'; + +export const LinkedEntityTableHeader = ({ columns }: Props) => ( + + + { + columns + .map(({ id, displayName }) => ( + + {displayName} + + )) + } + + +); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js similarity index 57% rename from src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js index 4dbdc83a50..9cb0d8596f 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsComponent/RelationshipsWidget.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js @@ -3,20 +3,10 @@ import React, { type ComponentType, useState } from 'react'; import { Chip, IconLink24, spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../../Widget'; -import { RelationshipTables } from './RelationshipsTables.component'; -import { AddNewRelationship } from '../AddNewRelationship'; -import type { OutputRelationshipData, UrlParameters } from '../Types'; - -type Props = {| - relationships: Array, - title: string, - onAddRelationship: () => void, - onLinkedRecordClick: (parameters: UrlParameters) => void, - teiId?: string, - eventId?: string, - addRelationshipRenderElement: HTMLElement, - ...CssClasses, -|} +import { useGroupedLinkedEntities } from './useGroupedLinkedEntities'; +import { useRelationshipTypes } from './useRelationshipTypes'; +import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; +import type { Props, StyledProps } from './relationshipsWidget.types'; const styles = { header: { @@ -29,16 +19,18 @@ const styles = { }; const RelationshipsWidgetPlain = ({ - relationships, title, - teiId, - eventId, - classes, + relationships, + cachedRelationshipTypes, + sourceId, onLinkedRecordClick, - ...passOnProps -}: Props) => { + children, + classes, +}: StyledProps) => { const [open, setOpenStatus] = useState(true); - const count = relationships.reduce((acc, curr) => { acc += curr.linkedEntityData.length; return acc; }, 0); + const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); + const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships); + return (
{title} {relationships && ( - {count} + {relationships.length} )}
@@ -61,16 +53,16 @@ const RelationshipsWidgetPlain = ({ onClose={() => setOpenStatus(false)} open={open} > - - - + { + groupedLinkedEntities && ( + + ) + }{ + relationshipTypes && children(relationshipTypes) + }
); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js new file mode 100644 index 0000000000..0390efec9f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js @@ -0,0 +1,3 @@ +// @flow +export { RelationshipsWidget } from './RelationshipsWidget.component'; +export type { NavigationArgsTrackedEntity, NavigationArgsEvent, NavigationArgs, LinkedRecordClick } from './types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js new file mode 100644 index 0000000000..93160917f9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js @@ -0,0 +1,12 @@ +// @flow +import type { GroupedLinkedEntities, LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + groupedLinkedEntities: GroupedLinkedEntities, + onLinkedRecordClick: LinkedRecordClick, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js new file mode 100644 index 0000000000..8d3fb8b72a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js @@ -0,0 +1,14 @@ +// @flow +import type { LinkedEntityData, TableColumn, LinkedRecordClick, Context } from './types'; + +export type Props = $ReadOnly<{| + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + onLinkedRecordClick: LinkedRecordClick, + context: Context, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js new file mode 100644 index 0000000000..1efc3202a5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js @@ -0,0 +1,14 @@ +// @flow +import type { LinkedEntityData, TableColumn, Context, LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + onLinkedRecordClick: LinkedRecordClick, + context: Context, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js new file mode 100644 index 0000000000..17def5ae63 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js @@ -0,0 +1,6 @@ +// @flow +import type { TableColumn } from './types'; + +export type Props = $ReadOnly<{| + columns: $ReadOnlyArray, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js new file mode 100644 index 0000000000..0fb87e55fc --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js @@ -0,0 +1,18 @@ +// @flow +import type { Node } from 'React'; +import type { InputRelationshipData, RelationshipTypes } from '../Types'; +import type { LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + title: string, + relationships?: Array, + cachedRelationshipTypes?: RelationshipTypes, + sourceId: string, + onLinkedRecordClick: LinkedRecordClick, + children: (relationshipTypes: RelationshipTypes) => Node, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js new file mode 100644 index 0000000000..91f747776b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js @@ -0,0 +1,50 @@ +// @flow +import { dataElementTypes } from '../../../../../metaData'; + +export type MetadataBasedColumn = $ReadOnly<{| + id: string, + displayName: string, + type: $Keys, +|}>; + +export type ManualColumn = $ReadOnly<{| + id: string, + displayName: string, + convertValue: (value: any) => any, +|}>; + +export type TableColumn = MetadataBasedColumn | ManualColumn; + +export type NavigationContextTrackedEntity = $ReadOnly<{| + programId?: string, +|}>; + +export type NavigationContextEvent = $ReadOnly<{||}>; + +export type LinkedEntityData = $ReadOnly<{| + id: string, + values: $ReadOnly<{| [id: string]: ?string |}>, + baseValues?: { + relatonshipCreatedAt?: string, + }, + navigation?: { + programId?: string, + eventId?: string, + trackedEntityId?: string, + }, +|}>; + +export type Context = $ReadOnly<{| + navigation: NavigationContextTrackedEntity | NavigationContextEvent, + display: Object, +|}>; + +export type LinkedEntityGroup = $ReadOnly<{| + id: string, + name: string, + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + context: Context, +|}>; + +export type GroupedLinkedEntities = $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js new file mode 100644 index 0000000000..268dc966e4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js @@ -0,0 +1,3 @@ +// @flow +export type * from './GroupedLinkedEntities.types'; +export type * from './navigation.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js new file mode 100644 index 0000000000..5fb9807b63 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js @@ -0,0 +1,15 @@ +// @flow + +export type NavigationArgsTrackedEntity = $ReadOnly<{| + programId?: string, + trackedEntityId: string, +|}>; + +export type NavigationArgsEvent = $ReadOnly<{| + eventId: string, + programId: string, +|}>; + +export type NavigationArgs = NavigationArgsTrackedEntity | NavigationArgsEvent; + +export type LinkedRecordClick = (navigationArgs: NavigationArgs) => void; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js new file mode 100644 index 0000000000..afd147fc10 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js @@ -0,0 +1,222 @@ +// @flow +import { useMemo } from 'react'; +import log from 'loglevel'; +import moment from 'moment'; +import i18n from '@dhis2/d2-i18n'; +import { errorCreator } from 'capture-core-utils'; +import { dataElementTypes } from '../../../../metaData'; +import { RELATIONSHIP_ENTITIES } from '../../common/constants'; +import { convertServerToClient, convertClientToList } from '../../../../converters'; +import type { GroupedLinkedEntities, LinkedEntityData } from './types'; +import type { + InputRelationshipData, + RelationshipTypes, +} from '../Types'; + + +const getFallbackFieldsByRelationshipEntity = { + [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ + id: 'trackedEntityTypeName', + displayName: i18n.t('Type'), + convertValue: trackedEntityTypeName => trackedEntityTypeName, + }, { + id: 'relatonshipCreatedAt', + displayName: i18n.t('Created date'), + convertValue: createdDate => convertClientToList( + convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, + ), + }], + [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: () => [{ + id: 'programStageName', + displayName: i18n.t('Program stage name'), + convertValue: programStageName => programStageName, + }, + { + id: 'relatonshipCreatedAt', + displayName: i18n.t('Created date'), + convertValue: createdDate => convertClientToList( + convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, + ), + }], +}; + +const getColumns = ({ relationshipEntity, trackerDataView }) => { + let fields; + if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + fields = trackerDataView.attributes; + } else if (relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { + fields = trackerDataView.dataElements; + } + + if (!fields?.length) { + fields = getFallbackFieldsByRelationshipEntity[relationshipEntity](); + } + + return fields; +}; + +// $FlowFixMe destructering +const getContext = ({ relationshipEntity, program, programStage, trackedEntityType }) => { + if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + return { + navigation: { + programId: program?.id, + }, + display: { + trackedEntityTypeName: trackedEntityType.name, + }, + }; + } + + if (relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { + return { + navigation: {}, + display: { + programStageName: programStage.name, + }, + }; + } + + return { + navigation: {}, + display: {}, + }; +}; + +const getEventData = ({ dataValues, event, program: programId }, relatonshipCreatedAt): LinkedEntityData => { + const values = dataValues.reduce((acc, dataValue) => { + acc[dataValue.dataElement] = dataValue.value; + return acc; + }, {}); + + return { + id: event, + values, + baseValues: { + relatonshipCreatedAt, + }, + navigation: { + eventId: event, + programId, + }, + }; +}; + +const getTrackedEntityData = ({ attributes, trackedEntity }, relatonshipCreatedAt): LinkedEntityData => { + const values = attributes.reduce((acc, attribute) => { + acc[attribute.attribute] = attribute.value; + return acc; + }, {}); + + return { + id: trackedEntity, + values, + baseValues: { + relatonshipCreatedAt, + }, + navigation: { + trackedEntityId: trackedEntity, + }, + }; +}; + +const getLinkedEntityData = (apiLinkedEntity, relatonshipCreatedAt) => { + if (apiLinkedEntity.trackedEntity) { + return getTrackedEntityData(apiLinkedEntity.trackedEntity, relatonshipCreatedAt); + } + + if (apiLinkedEntity.event) { + return getEventData(apiLinkedEntity.event, relatonshipCreatedAt); + } + + if (apiLinkedEntity.enrollment) { + log.warn(errorCreator('Linked entities of type enrollment are not currently supported')({ apiLinkedEntity })); + return null; + } + + log.error(errorCreator('Unsupported linked entity type')({ apiLinkedEntity })); + return null; +}; + +const determineLinkedEntity = (fromEntity, toEntity, sourceId) => { + if (fromEntity.trackedEntity?.trackedEntity === sourceId || fromEntity.event?.event === sourceId) { + return toEntity; + } + + if (toEntity.trackedEntity?.trackedEntity === sourceId || toEntity.event?.event === sourceId) { + return fromEntity; + } + + log.error(errorCreator('Could not determine linked entity')({ fromEntity, toEntity, sourceId })); + return null; +}; + +export const useGroupedLinkedEntities = ( + sourceId: string, + relationshipTypes: ?RelationshipTypes, + relationships?: Array, +): GroupedLinkedEntities => { + const groupedLinkedEntities = useMemo(() => { + if (!relationships?.length || !relationshipTypes?.length) { + return []; + } + + + return relationships + .sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt))) + .reduce((accGroupedLinkedEntities, relationship) => { + const { + relationshipType: relationshipTypeId, + from: fromEntity, + to: toEntity, + createdAt: relationshipCreatedAt, + } = relationship; + + const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); + if (!relationshipType) { + log.error( + errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), + ); + return accGroupedLinkedEntities; + } + + const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); + if (!apiLinkedEntity) { + return accGroupedLinkedEntities; + } + + const linkedEntityData = getLinkedEntityData(apiLinkedEntity, relationshipCreatedAt); + if (!linkedEntityData) { + return accGroupedLinkedEntities; + } + + const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; + const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); + if (group) { + group.linkedEntities = [ + ...group.linkedEntities, + linkedEntityData, + ]; + } else { + const { constraint, name } = apiLinkedEntity === fromEntity ? + { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : + { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; + + const columns = getColumns(constraint); + const context = getContext(constraint); + + accGroupedLinkedEntities.push({ + id: groupId, + name: name || relationshipType.displayName, + linkedEntities: [linkedEntityData], + columns, + context, + }); + } + + return accGroupedLinkedEntities; + }, []); + }, [relationships, relationshipTypes, sourceId]); + + return groupedLinkedEntities; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js similarity index 96% rename from src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js index d423df8c2a..10c6aee562 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationshipTypes.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js @@ -1,7 +1,7 @@ // @flow import { useMemo } from 'react'; import { useApiMetadataQuery } from '../../../../utils/reactQueryHelpers'; -import type { RelationshipTypes } from '../Types'; +import type { ApiRelationshipTypes, RelationshipTypes } from '../Types'; import { extractElementIdsFromRelationshipTypes, formatRelationshipTypes } from '../utils'; type Element = {| @@ -18,7 +18,7 @@ const relationshipTypesQuery = { }; export const useRelationshipTypes = (cachedRelationshipTypes?: RelationshipTypes) => { - const { data: apiRelationshipTypes, isError, isLoading } = useApiMetadataQuery( + const { data: apiRelationshipTypes, isError, isLoading } = useApiMetadataQuery( ['relationshipTypes'], relationshipTypesQuery, { diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js index 79237de57b..989ee5ee7d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js @@ -1,25 +1,5 @@ // @flow -export type RelationshipTableHeader = {| - id: string, - displayName: string, - convertValue: (value: any) => any, -|} - -export type UrlParameters = {| - programId?: string, - orgUnitId?: string, - teiId?: string, - enrollmentId?: string, - eventId?: string, -|} - -export type LinkedEntityData = { - id: string, - values: Array<{ id: string, value: ?string }>, - parameters: UrlParameters, -} - export type ApiLinkedEntity = {| event?: { event: string, @@ -35,7 +15,8 @@ export type ApiLinkedEntity = {| orgUnit: string, orgUnitName: string, attributes: Array<{ attribute: string, value: string }>, - } + }, + enrollment?: {}, |} export type InputRelationshipData = { @@ -48,10 +29,3 @@ export type InputRelationshipData = { from: ApiLinkedEntity, to: ApiLinkedEntity, } - -export type OutputRelationshipData = { - id: string, - relationshipName: string, - headers: Array, - linkedEntityData: Array, -} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js index c3ccc4184b..ea5b0b0a53 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js @@ -1,9 +1,5 @@ // @flow - -export type TrackerDataView = { - attributes: Array, - dataElements: Array, -}; +import { dataElementTypes } from '../../../../metaData'; export type ElementValue = {| attribute?: string, @@ -11,27 +7,78 @@ export type ElementValue = {| displayName: string, valueType: string, value: any, -|} -type CommonConstraintTypes = {| - trackerDataView: TrackerDataView, +|}; + +export type ApiTrackerDataView = $ReadOnly<{| + attributes: $ReadOnlyArray, + dataElements: $ReadOnlyArray, +|}>; + +type ApiCommonConstraintTypes = $ReadOnly<{| + trackerDataView: ApiTrackerDataView, +|}>; + +export type ApiTrackedEntityConstraint = $ReadOnly<{| + ...ApiCommonConstraintTypes, + relationshipEntity: 'TRACKED_ENTITY_INSTANCE', + trackedEntityType: { id: string, name: string }, program?: { id: string, name: string }, -|} +|}>; + +export type ApiProgramStageInstanceConstraint = $ReadOnly<{| + ...ApiCommonConstraintTypes, + relationshipEntity: 'PROGRAM_STAGE_INSTANCE', + program: { id: string, name: string }, + programStage: { id: string, name: string }, +|}>; + +export type ApiRelationshipConstraint = ApiTrackedEntityConstraint | ApiProgramStageInstanceConstraint; + +export type ApiRelationshipType = $ReadOnly<{| + id: string, + displayName: string, + bidirectional: boolean, + access: Object, + toFromName: string, + fromToName: string, + fromConstraint: ApiRelationshipConstraint, + toConstraint: ApiRelationshipConstraint, +|}>; + +export type ApiRelationshipTypes = $ReadOnlyArray; -export type TrackedEntityConstraint = { +export type TrackerDataViewEntity = $ReadOnly<{| + id: string, + type: $Keys, + displayName: string, +|}>; + +export type TrackerDataView = $ReadOnly<{| + attributes: $ReadOnlyArray, + dataElements: $ReadOnlyArray, +|}>; + +export type CommonConstraintTypes = $ReadOnly<{| + trackerDataView: TrackerDataView, +|}>; + +export type TrackedEntityConstraint = $ReadOnly<{| ...CommonConstraintTypes, relationshipEntity: 'TRACKED_ENTITY_INSTANCE', trackedEntityType: { id: string, name: string }, -} + program?: { id: string, name: string }, +|}>; -export type ProgramStageInstanceConstraint = {| +export type ProgramStageInstanceConstraint = $ReadOnly<{| ...CommonConstraintTypes, relationshipEntity: 'PROGRAM_STAGE_INSTANCE', + program: { id: string, name: string }, programStage: { id: string, name: string }, -|} +|}>; export type RelationshipConstraint = TrackedEntityConstraint | ProgramStageInstanceConstraint; -export type RelationshipType = { +export type RelationshipType = $ReadOnly<{| id: string, displayName: string, bidirectional: boolean, @@ -40,6 +87,6 @@ export type RelationshipType = { fromToName: string, fromConstraint: RelationshipConstraint, toConstraint: RelationshipConstraint, -}; +|}>; -export type RelationshipTypes = Array; +export type RelationshipTypes = $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js new file mode 100644 index 0000000000..e3e3225010 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js @@ -0,0 +1,10 @@ +// @flow +export const RELATIONSHIP_ENTITIES: {| + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +|} = Object.freeze({ + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js deleted file mode 100644 index 87338a8a5e..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow - -export { useLinkedEntityGroups } from './useLinkedEntityGroups'; -export { useRelationships } from './useRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js deleted file mode 100644 index 40822ea705..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useLinkedEntityGroups.js +++ /dev/null @@ -1,204 +0,0 @@ -// @flow -import { useCallback, useEffect, useState } from 'react'; -import log from 'loglevel'; -import moment from 'moment'; -import { errorCreator } from 'capture-core-utils'; -import { getBaseConfigHeaders, RELATIONSHIP_ENTITIES } from '../../constants'; -import { convertServerToClient, convertClientToList } from '../../../../converters'; -import type { - ApiLinkedEntity, - ElementValue, - InputRelationshipData, ProgramStageInstanceConstraint, - RelationshipConstraint, - RelationshipType, - RelationshipTypes, TrackedEntityConstraint, -} from '../Types'; - -const convertAttributes = ( - attributes: Array, - displayFields: Array, - options: Object, -): Array<{id: string, value: any}> => displayFields.map((field) => { - if (field.convertValue) { - return { - id: field.id, - value: field.convertValue(options), - }; - } - - const attributeItem = attributes.find((att) => { - if (att.attribute) { return att.attribute === field.id; } - if (att.dataElement) { return att.dataElement === field.id; } - return undefined; - })?.value; - - return { - id: field.id, - value: convertClientToList(convertServerToClient(attributeItem, field.valueType), field.valueType), - }; -}); - -const getDisplayFields = (linkedEntity) => { - let displayFields; - if (linkedEntity.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { - displayFields = linkedEntity.trackerDataView.attributes; - } else if (linkedEntity.relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { - displayFields = linkedEntity.trackerDataView.dataElements; - } - if (!displayFields?.length) { - displayFields = getBaseConfigHeaders[linkedEntity.relationshipEntity]; - } - - return displayFields; -}; - -const determineLinkedEntity = ( - relationshipType: RelationshipType, - targetId: string, - fromEntity: ApiLinkedEntity, - toEntity: ApiLinkedEntity, -) => { - const { id, toConstraint, fromConstraint, toFromName, fromToName } = relationshipType; - - if ((toEntity.trackedEntity && toEntity.trackedEntity.trackedEntity === targetId) || (toEntity.event && toEntity.event.event === targetId)) { - return { side: fromEntity, constraint: fromConstraint, groupId: `${id}-from`, name: toFromName }; - } - - if ((fromEntity.trackedEntity && fromEntity.trackedEntity.trackedEntity === targetId) || (fromEntity.event && fromEntity.event.event === targetId)) { - return { side: toEntity, constraint: toConstraint, groupId: `${id}-to`, name: fromToName }; - } - - log.error(errorCreator('Relationship type is not handled')({ relationshipType })); - return undefined; -}; - - -const getLinkedRecordURLParameters = (linkedEntity: ApiLinkedEntity, entityConstraint: RelationshipConstraint) => { - if (linkedEntity.event) { - const { - event: eventId, - program: programId, - orgUnit: orgUnitId, - } = linkedEntity.event; - return { eventId, programId, orgUnitId }; - } else if (linkedEntity.trackedEntity) { - const programId = entityConstraint.program?.id; - const { trackedEntity: teiId, orgUnit: orgUnitId } = linkedEntity.trackedEntity; - return { programId, orgUnitId, teiId }; - } - return {}; -}; - -const getAttributeConstraintsForTEI = ( - linkedEntity: ApiLinkedEntity, - entityConstraint: RelationshipConstraint, - createdAt: string) => { - if (linkedEntity.event) { - const { event: eventId, orgUnitName, status } = linkedEntity.event; - const { program, programStage }: ProgramStageInstanceConstraint = (entityConstraint: any); - - return { - id: eventId, - values: linkedEntity.event.dataValues, - parameters: getLinkedRecordURLParameters(linkedEntity, entityConstraint), - options: { - orgUnitName, - status, - programName: program?.name, - programStageName: programStage?.name, - created: createdAt, - }, - }; - } else if (linkedEntity.trackedEntity) { - const { trackedEntity, attributes } = linkedEntity.trackedEntity; - const { trackedEntityType }: TrackedEntityConstraint = (entityConstraint: any); - - return { - id: trackedEntity, - values: attributes, - parameters: getLinkedRecordURLParameters(linkedEntity, entityConstraint), - options: { - trackedEntityTypeName: trackedEntityType?.name ?? '', - created: createdAt, - }, - }; - } - log.error(errorCreator('Relationship type is not handled')({ linkedEntity })); - return undefined; -}; - -const getLinkedEntityInfo = ( - relationshipType: RelationshipType, - targetId: string, - fromEntity: ApiLinkedEntity, - toEntity: ApiLinkedEntity, - createdAt: string, -) => { - const linkedEntityData = determineLinkedEntity(relationshipType, targetId, fromEntity, toEntity); - if (!linkedEntityData) { return undefined; } - - const metadata = getAttributeConstraintsForTEI(linkedEntityData.side, linkedEntityData.constraint, createdAt); - if (!metadata) { return undefined; } - - const { id, values, options, parameters } = metadata; - const displayFields = getDisplayFields(linkedEntityData.constraint); - - return { - id, - displayFields, - parameters, - groupId: linkedEntityData.groupId, - // $FlowFixMe - value should have either attribute or dataElement - values: convertAttributes(values, displayFields, options), - name: linkedEntityData.name, - }; -}; - - -export const useLinkedEntityGroups = ( - targetId: string, - relationshipTypes: ?RelationshipTypes, - relationships?: Array, -) => { - const [relationshipsByType, setRelationshipByType] = useState([]); - - const computeData = useCallback(() => { - if (relationships?.length && relationshipTypes?.length) { - const linkedEntityGroups = relationships - .sort((a, b) => moment.utc(b.createdAt).diff(moment.utc(a.createdAt))) - .reduce((acc, relationship) => { - const { relationshipType: typeId, from: fromEntity, to: toEntity, createdAt } = relationship; - const relationshipType = relationshipTypes.find(item => item.id === typeId); - - if (!relationshipType) { return acc; } - const metadata = getLinkedEntityInfo(relationshipType, targetId, fromEntity, toEntity, createdAt); - if (!metadata) { return acc; } - const { displayFields, id, values, parameters, groupId, name } = metadata; - - const typeExist = acc.find(item => item.id === groupId); - if (typeExist) { - typeExist.linkedEntityData.push({ id, values, parameters }); - } else { - acc.push({ - id: groupId, - relationshipName: name || relationshipType.displayName, - linkedEntityData: [{ id, values, parameters }], - headers: displayFields, - }); - } - - return acc; - }, []); - - setRelationshipByType(linkedEntityGroups); - } - }, [relationships, relationshipTypes, targetId]); - - useEffect(() => { - computeData(); - }, [computeData]); - - return { - relationships: relationshipsByType, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js new file mode 100644 index 0000000000..2c93b6f33c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js @@ -0,0 +1,2 @@ +// @flow +export { useRelationships, RelationshipSearchEntities } from './useRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js similarity index 100% rename from src/core_modules/capture-core/components/WidgetsRelationship/common/hooks/useRelationships.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js index 3563b98c5e..ac437c364b 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js @@ -1,8 +1,8 @@ // @flow -import type { RelationshipTypes } from '../Types'; +import type { ApiRelationshipTypes } from '../Types'; -export const extractElementIdsFromRelationshipTypes = (relationshipTypes: RelationshipTypes) => { +export const extractElementIdsFromRelationshipTypes = (relationshipTypes: ApiRelationshipTypes) => { const attributeIds = relationshipTypes .flatMap((relationshipType) => { const { fromConstraint, toConstraint } = relationshipType; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js index ba021ba68f..ce575581db 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js @@ -1,6 +1,6 @@ // @flow -import type { RelationshipTypes } from '../Types'; -import { mapRelationshipElementToId } from './mapRelationshipElementToId'; +import type { ApiRelationshipTypes, RelationshipTypes } from '../Types'; +import { replaceElementIdsWithElement } from './replaceElementIdsWithElement'; const elementTypes = { ATTRIBUTE: 'ATTRIBUTE', @@ -14,7 +14,7 @@ type Element = {| |} type Props = {| - relationshipTypes: RelationshipTypes, + relationshipTypes: ApiRelationshipTypes, attributes: Array, dataElements: Array, |} @@ -23,7 +23,7 @@ export const formatRelationshipTypes = ({ relationshipTypes = [], attributes, dataElements, -}: Props) => { +}: Props): RelationshipTypes => { const attributesById = attributes.reduce((acc, { id, valueType, displayName }) => { acc[id] = { valueType, displayName }; return acc; @@ -34,26 +34,25 @@ export const formatRelationshipTypes = ({ return acc; }, {}); - // $FlowFixMe return relationshipTypes .map((relationshipType) => { const { fromConstraint, toConstraint } = relationshipType; - const fromAttributes = mapRelationshipElementToId( + const fromAttributes = replaceElementIdsWithElement( fromConstraint.trackerDataView?.attributes, attributesById, { relationshipType, elementType: elementTypes.ATTRIBUTE }, ); - const toAttributes = mapRelationshipElementToId( + const toAttributes = replaceElementIdsWithElement( toConstraint.trackerDataView?.attributes, attributesById, { relationshipType, elementType: elementTypes.ATTRIBUTE }, ); - const fromDataElements = mapRelationshipElementToId( + const fromDataElements = replaceElementIdsWithElement( fromConstraint.trackerDataView?.dataElements, dataElementsById, { relationshipType, elementType: elementTypes.DATA_ELEMENT }, ); - const toDataElements = mapRelationshipElementToId( + const toDataElements = replaceElementIdsWithElement( toConstraint.trackerDataView?.dataElements, dataElementsById, { relationshipType, elementType: elementTypes.DATA_ELEMENT }, @@ -61,6 +60,7 @@ export const formatRelationshipTypes = ({ return { ...relationshipType, + // $FlowFixMe Might be fixable with generics fromConstraint: { ...fromConstraint, trackerDataView: { @@ -68,6 +68,7 @@ export const formatRelationshipTypes = ({ dataElements: fromDataElements, }, }, + // $FlowFixMe Might be fixable with generics toConstraint: { ...toConstraint, trackerDataView: { diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js index 1752ef1214..bbc3c5da67 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js @@ -1,4 +1,3 @@ // @flow export { extractElementIdsFromRelationshipTypes } from './extractElementIdsFromRelationshipTypes'; -export { mapRelationshipElementToId } from './mapRelationshipElementToId'; export { formatRelationshipTypes } from './formatRelationshipTypes'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js similarity index 68% rename from src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js rename to src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js index 62a72ce6a9..e9e8d64332 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/mapRelationshipElementToId.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js @@ -1,7 +1,10 @@ // @flow import log from 'loglevel'; import { errorCreator } from '../../../../../capture-core-utils'; -import type { RelationshipType } from '../Types'; +import type { ApiRelationshipType } from '../Types'; +import { + dataElementTypes, +} from '../../../../metaData'; const elementTypes = { ATTRIBUTE: 'attribute', @@ -12,19 +15,25 @@ type Elements = {| [key: string]: { id: string, displayName: string, - valueType?: string, + valueType: $Keys, }, |} type Context = {| - relationshipType: RelationshipType, + relationshipType: ApiRelationshipType, elementType: $Values |} -export const mapRelationshipElementToId = ( - elementIds: ?Array, +type ElementArray = $ReadOnlyArray<{| + id: string, + type: $Keys, + displayName: string, +|}>; + +export const replaceElementIdsWithElement = ( + elementIds: ?$ReadOnlyArray, elements: Elements, - { relationshipType, elementType }: Context) => + { relationshipType, elementType }: Context): ElementArray => // $FlowFixMe (elementIds || []) .map((elementId) => { @@ -36,6 +45,8 @@ export const mapRelationshipElementToId = ( { elementId, relationshipType }, ), ); + + // $FlowFixMe filter is unhandled return null; } @@ -44,6 +55,8 @@ export const mapRelationshipElementToId = ( errorCreator(`cached ${elementType} is missing value type`)( { elementId, element }), ); + + // $FlowFixMe filter is unhandled return null; } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/constants.js deleted file mode 100644 index eaf6d52615..0000000000 --- a/src/core_modules/capture-core/components/WidgetsRelationship/constants.js +++ /dev/null @@ -1,47 +0,0 @@ -// @flow -import i18n from '@dhis2/d2-i18n'; -import { dataElementTypes } from '../../metaData'; -import { convertClientToList, convertServerToClient } from '../../converters'; - -export const RELATIONSHIP_ENTITIES: {| - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', -|} = Object.freeze({ - PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', - TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', - PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', -}); - -export const getBaseConfigHeaders = { - [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: [{ - id: 'tetName', - displayName: i18n.t('TET name'), - convertValue: (props: any) => props.trackedEntityTypeName, - }, { - id: 'createdDate', - displayName: i18n.t('Created date'), - convertValue: (props: any) => convertClientToList( - convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, - ), - }], - [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: [{ - id: 'programStageName', - displayName: i18n.t('Program stage name'), - convertValue: (props: any) => props.programStageName, - }, - { - id: 'createdDate', - displayName: i18n.t('Created date'), - convertValue: (props: any) => convertClientToList( - convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, - ), - }], - [RELATIONSHIP_ENTITIES.PROGRAM_INSTANCE]: [{ - id: 'createdDate', - displayName: i18n.t('Created date'), - convertValue: (props: any) => convertClientToList( - convertServerToClient(props.created, dataElementTypes.DATE), dataElementTypes.DATE, - ), - }], -}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/index.js index 8341b80d97..f28c1cfc5f 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/index.js @@ -1,4 +1,3 @@ // @flow -export { RelationshipTypes, RelationshipType, TrackerDataView } from './common/Types'; +export type { ApiRelationshipTypes, RelationshipTypes, RelationshipType, TrackerDataView } from './common/Types'; export { formatRelationshipTypes, extractElementIdsFromRelationshipTypes } from './common/utils'; -export { RELATIONSHIP_ENTITIES } from './constants'; diff --git a/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js b/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js index f30dfcd8af..aa5005d8d1 100644 --- a/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js +++ b/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js @@ -2,10 +2,11 @@ const LOCALE_EN = 'en'; -export const buildUrlQueryString = (queryArgs: { [id: string]: string }) => +export const buildUrlQueryString = (queryArgs: $ReadOnly<{ [id: string]: ?string }>) => Object .entries(queryArgs) - .sort((a, b) => a[0].localeCompare(b[0], LOCALE_EN)) + .filter(([, value]) => value != null) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB, LOCALE_EN)) .reduce((searchParams, [key, value]) => { // $FlowFixMe value && searchParams.append(key, value); From c14d0ebf8a9b62d76e228c7d6c1a85d19db8e616 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Sat, 22 Jul 2023 14:44:28 +0200 Subject: [PATCH 78/95] feat: add search functionality --- i18n/en.pot | 32 +-- .../SearchOrgUnitSelector.component.js | 152 ++++++++++++ .../SearchOrgUnitSelector.container.js | 46 ++++ ...archOrgUnitSelectorRefHandler.component.js | 18 ++ .../searchOrgUnitSelector.actions.js | 30 +++ .../searchOrgUnitSelector.epics.js | 77 ++++++ .../SearchProgramSelector.component.js | 45 ++++ .../SearchProgramSelector.container.js | 20 ++ .../getProgramOptions.js | 12 + .../searchProgramSelector.actions.js | 15 ++ .../TeiSearch/TeiSearch.component.js | 172 ++++++++++++++ .../TeiSearch/TeiSearch.container.js | 52 ++++ .../TeiSearch/TeiSearch.types.js | 28 +++ .../TeiSearchForm/TeiSearchForm.component.js | 181 ++++++++++++++ .../TeiSearchForm/TeiSearchForm.container.js | 23 ++ .../TeiSearchResults.component.js | 11 + .../TeiSearchResults.container.js | 29 +++ .../TeiSearchResults.types.js | 24 ++ .../TeiSearch/actions/teiSearch.actions.js | 70 ++++++ .../TeiSearch/epics/teiSearch.epics.js | 223 ++++++++++++++++++ .../TeiSearch/getSearchFormId.js | 10 + .../TeiSearch/getSearchGroups.js | 19 ++ .../TeiSearch/serverToFilters.js | 38 +++ ...ackedEntityRelationshipsWrapper.actions.js | 14 ++ ...kedEntityRelationshipsWrapper.component.js | 38 ++- ...TrackedEntityRelationshipsWrapper.epics.js | 37 +++ .../index.js | 1 + .../SearchForm/SearchForm.component.js | 2 +- .../Section/SectionHeaderSimple.component.js | 12 +- .../NewTrackedEntityRelationship.component.js | 95 ++++++-- .../NewTrackedEntityRelationship.container.js | 6 + .../NewTrackedEntityRelationship.types.js | 14 ++ .../hooks/useAddRelationship.js | 51 ++++ ...dgetTrackedEntityRelationship.component.js | 5 + .../WidgetTrackedEntityRelationship.types.js | 22 ++ .../WidgetTrackedEntityRelationship/index.js | 8 +- .../useGroupedLinkedEntities.js | 129 +++++----- src/epics/trackerCapture.epics.js | 4 + 38 files changed, 1650 insertions(+), 115 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/getProgramOptions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchFormId.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchGroups.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js diff --git a/i18n/en.pot b/i18n/en.pot index 66f96d4642..c56802553f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-06-27T06:20:33.460Z\n" -"PO-Revision-Date: 2023-06-27T06:20:33.460Z\n" +"POT-Creation-Date: 2023-07-27T19:19:14.053Z\n" +"PO-Revision-Date: 2023-07-27T19:19:14.053Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -926,6 +926,20 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Selected program" +msgstr "Selected program" + +msgid "Search {{uniqueAttrName}}" +msgstr "Search {{uniqueAttrName}}" + +msgid "Search by attributes" +msgstr "Search by attributes" + +msgid "Fill in at least {{count}} attribute to search" +msgid_plural "Fill in at least {{count}} attribute to search" +msgstr[0] "Fill in at least {{count}} attribute to search" +msgstr[1] "Fill in at least {{count}} attributes to search" + msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." @@ -986,20 +1000,12 @@ msgstr "You can also choose a program from the top bar and search in that progra msgid "Choose a type to start searching" msgstr "Choose a type to start searching" -msgid "Fill in at least {{count}} attribute to search" -msgid_plural "Fill in at least {{count}} attribute to search" -msgstr[0] "Fill in at least {{count}} attribute to search" -msgstr[1] "Fill in at least {{count}} attributes to search" - msgid "Search {{name}}" msgstr "Search {{name}}" msgid "Search by {{name}}" msgstr "Search by {{name}}" -msgid "Search by attributes" -msgstr "Search by attributes" - msgid "all programs" msgstr "all programs" @@ -1057,12 +1063,6 @@ msgstr "Missing search criteria" msgid "Results found" msgstr "Results found" -msgid "Selected program" -msgstr "Selected program" - -msgid "Search {{uniqueAttrName}}" -msgstr "Search {{uniqueAttrName}}" - msgid "Saved lists in this program" msgstr "Saved lists in this program" diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js new file mode 100644 index 0000000000..9d69231c92 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.js @@ -0,0 +1,152 @@ +// @flow +import * as React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + SelectionBoxes, + withDefaultFieldContainer, + withLabel, + withFocusSaver, + withCalculateMessages, + withDisplayMessages, + SingleOrgUnitSelectField, +} from '../../../../../FormFields/New'; + +const TeiSearchOrgUnitField = withFocusSaver()(withCalculateMessages()(withDefaultFieldContainer()(withLabel()(withDisplayMessages()(SingleOrgUnitSelectField))))); +const TeiSearchSelectionBoxes = withDefaultFieldContainer()(withLabel()(SelectionBoxes)); + +type Props = { + searchId: string, + selectedOrgUnit?: ?any, + selectedOrgUnitScope?: ?string, + treeRoots: ?Array, + treeReady: ?boolean, + treeKey: ?string, + treeSearchText?: ?string, + onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => void, + onSetOrgUnit: (searchId: string, orgUnit: ?Object) => void, + onFilterOrgUnits: (searchId: string, searchText: ?string) => void, + searchAttempted: ?boolean, +} + +const orgUnitFieldStyles = { + labelContainerStyle: { + paddingTop: 12, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +const selectionBoxesStyles = { + labelContainerStyle: { + paddingTop: 13, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +const options = [ + { + name: 'All accessible', + value: 'ACCESSIBLE', + }, + { + name: 'Selected', + value: 'SELECTED', + }, +]; + +const errorMessage = 'Please select an organisation unit'; + +export class SearchOrgUnitSelector extends React.Component { + gotoInstance: any; + + onSelectOrgUnitScope = (value: any) => { + if (value) { + this.props.onSelectOrgUnitScope(this.props.searchId, value); + } + } + onSetOrgUnit = (orgUnit: ?Object) => { + this.props.onSetOrgUnit(this.props.searchId, orgUnit); + } + renderOrgUnitScopeSelector = () => { + const { selectedOrgUnitScope } = this.props; + return ( + + ); + } + + isValid = () => this.props.selectedOrgUnitScope === 'ACCESSIBLE' || this.props.selectedOrgUnit + + validateAndScrollToIfFailed() { + const isValid = this.isValid(); + if (!isValid) { + this.goto(); + } + + return isValid; + } + + goto() { + if (this.gotoInstance) { + this.gotoInstance.scrollIntoView(); + + const scrolledY = window.scrollY; + if (scrolledY) { + // TODO: Set the modifier some other way (caused be the fixed header) + window.scroll(0, scrolledY - 48); + } + } + } + + getErrorMessage = () => { + if (!this.isValid() && this.props.searchAttempted) { + return i18n.t(errorMessage); + } + return null; + } + + handleFilterOrgUnits = (searchText: ?string) => { + this.props.onFilterOrgUnits(this.props.searchId, searchText); + } + + renderOrgUnitField = () => { + const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; + return ( + + ); + } + + render() { + const { selectedOrgUnitScope } = this.props; + return ( +
{ this.gotoInstance = gotoInstance; }} + > + {this.renderOrgUnitScopeSelector()} + {selectedOrgUnitScope !== 'ACCESSIBLE' && this.renderOrgUnitField()} +
+ ); + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js new file mode 100644 index 0000000000..08df2c9b8b --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.js @@ -0,0 +1,46 @@ +// @flow +import { connect } from 'react-redux'; +import { + setOrgUnitScope, + setOrgUnit, + requestFilterOrgUnits, + clearOrgUnitsFilter, +} from './searchOrgUnitSelector.actions'; +import { get as getOrgUnitRoots } from '../../../../../FormFields/New/Fields/OrgUnitField/orgUnitRoots.store'; +import { SearchOrgUnitSelectorRefHandler } from './SearchOrgUnitSelectorRefHandler.component'; + +const mapStateToProps = (state: ReduxState, props: Object) => { + const searchId = props.searchId; + + const filteredRoots = getOrgUnitRoots(searchId); + const roots = filteredRoots || getOrgUnitRoots('searchRoots'); + + return { + selectedOrgUnit: state.teiSearch[searchId].selectedOrgUnit, + selectedOrgUnitScope: state.teiSearch[searchId].selectedOrgUnitScope, + treeRoots: roots, + treeSearchText: state.teiSearch[searchId].orgUnitsSearchText, + treeReady: !state.teiSearch[searchId].orgUnitsLoading, + treeKey: state.teiSearch[searchId].orgUnitsSearchText || 'initial', + }; +}; + +const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ + onFilterOrgUnits: (searchId: string, searchText: string) => { + const action = searchText ? + requestFilterOrgUnits(searchId, searchText) : + clearOrgUnitsFilter(searchId); + dispatch(action); + }, + onSetOrgUnit: (searchId: string, orgUnit: ?any) => { + dispatch(setOrgUnit(searchId, orgUnit)); + }, + onSelectOrgUnitScope: (searchId: string, orgUnitScope: string) => { + dispatch(setOrgUnitScope(searchId, orgUnitScope)); + }, +}); + +// $FlowFixMe[missing-annot] automated comment +export const SearchOrgUnitSelector = connect(mapStateToProps, mapDispatchToProps)( + SearchOrgUnitSelectorRefHandler, +); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js new file mode 100644 index 0000000000..9374a93993 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelectorRefHandler.component.js @@ -0,0 +1,18 @@ +// @flow +import * as React from 'react'; +import { SearchOrgUnitSelector } from './SearchOrgUnitSelector.component'; + +type Props = { + innerRef: Function, +}; + +export const SearchOrgUnitSelectorRefHandler = (props: Props) => { + const { innerRef, ...passOnProps } = props; + return ( + // $FlowFixMe[cannot-spread-inexact] automated comment + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js new file mode 100644 index 0000000000..8479b578a5 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.actions.js @@ -0,0 +1,30 @@ +// @flow + +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const actionTypes = { + TEI_SEARCH_REQUEST_FILTER_ORG_UNITS: 'TeiSearchRequestFilterOrgUnits', + TEI_SEARCH_CLEAR_ORG_UNITS_FILTER: 'TeiSearchClearOrgUnitsFilter', + TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED: 'TeiSearchFilteredOrgUnitsRetrieved', + TEI_SEARCH_FILTER_ORG_UNITS_FAILED: 'TeiSearchFilterOrgUnitsFailed', + TEI_SEARCH_SET_ORG_UNIT_SCOPE: 'TeiSearchSetOrgUnitScope', + TEI_SEARCH_SET_ORG_UNIT: 'TeiSearchSetOrgUnit', +}; + +export const setOrgUnitScope = (searchId: string, orgUnitScope: string) => + actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT_SCOPE)({ searchId, orgUnitScope }); + +export const setOrgUnit = (searchId: string, orgUnit: ?any) => + actionCreator(actionTypes.TEI_SEARCH_SET_ORG_UNIT)({ searchId, orgUnit }); + +export const requestFilterOrgUnits = (searchId: string, searchText: string) => + actionCreator(actionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS)({ searchId, searchText }); + +export const filteredOrgUnitsRetrieved = (searchId: string, roots: ?Array, searchText: string) => + actionCreator(actionTypes.TEI_SEARCH_FILTERED_ORG_UNITS_RETRIEVED)({ searchId, roots, searchText }); + +export const filterOrgUnitsFailed = (searchId: string, error: any) => + actionCreator(actionTypes.TEI_SEARCH_FILTER_ORG_UNITS_FAILED)({ searchId, error }); + +export const clearOrgUnitsFilter = (searchId: string) => + actionCreator(actionTypes.TEI_SEARCH_CLEAR_ORG_UNITS_FILTER)({ searchId }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js new file mode 100644 index 0000000000..83031a6ae8 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/searchOrgUnitSelector.epics.js @@ -0,0 +1,77 @@ +// @flow +import log from 'loglevel'; +import isArray from 'd2-utilizr/lib/isArray'; +import { from } from 'rxjs'; +import { errorCreator } from 'capture-core-utils'; +import { ofType } from 'redux-observable'; +import { map, concatMap, takeUntil, filter } from 'rxjs/operators'; +import { + actionTypes as teiSearchActionTypes, +} from '../actions/teiSearch.actions'; + +import { + actionTypes as searchOrgUnitActionTypes, + filterOrgUnitsFailed, + filteredOrgUnitsRetrieved, +} from './searchOrgUnitSelector.actions'; + + +const RETRIEVE_ERROR = 'Could not retrieve registering unit list'; + + +const isInitializeTeiSearch = (action: Object, searchId: string) => + action.type === teiSearchActionTypes.INITIALIZE_TEI_SEARCH && + action.payload.searchId === searchId; + +const isRequestFilterOrgUnits = (action: Object, searchId: string) => + action.type === searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS && + action.payload.searchId === searchId; + + +const cancelActionFilter = (action: Object, searchId: string) => { + if (isArray(action.payload)) { + return action.payload.some(innerAction => isInitializeTeiSearch(innerAction, searchId)); + } + return isInitializeTeiSearch(action, searchId) || isRequestFilterOrgUnits(action, searchId); +}; + +// get organisation units based on search criteria +export const teiSearchFilterOrgUnitsEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => + action$.pipe( + ofType(searchOrgUnitActionTypes.TEI_SEARCH_REQUEST_FILTER_ORG_UNITS), + concatMap((action) => { + const searchText = action.payload.searchText; + const searchId = action.payload.searchId; + + return from(querySingleResource({ + resource: 'organisationUnits', + params: { + fields: [ + 'id,displayName,path,publicAccess,access,lastUpdated', + 'children[id,displayName,publicAccess,access,path,children::isNotEmpty]', + ].join(','), + paging: true, + withinUserSearchHierarchy: true, + query: searchText, + pageSize: 15, + }, + }).then(({ organisationUnits }) => ({ regUnitArray: organisationUnits, searchText, searchId })) + .catch(error => ({ error, searchId }))) + .pipe(takeUntil(action$.pipe(filter(a => cancelActionFilter(a, searchId))))); + }), map((resultContainer) => { + if (resultContainer.error) { + log.error(errorCreator(RETRIEVE_ERROR)( + { error: resultContainer.error, method: 'searchRegisteringUnitListEpic' }), + ); + return filterOrgUnitsFailed(resultContainer.searchId, RETRIEVE_ERROR); + } + + const regUnits = resultContainer.regUnitArray + .map(unit => ({ + id: unit.id, + path: unit.path, + displayName: unit.displayName, + })); + return filteredOrgUnitsRetrieved(resultContainer.searchId, regUnits, resultContainer.searchText); + })); + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js new file mode 100644 index 0000000000..e72a391cda --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.component.js @@ -0,0 +1,45 @@ +// @flow +import * as React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + OptionsSelectVirtualized, +} from '../../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; +import type { + VirtualizedOptionConfig, +} from '../../../../../FormFields/Options/SelectVirtualizedV2/OptionsSelectVirtualized.component'; +import { withDefaultFieldContainer, withLabel } from '../../../../../FormFields/New'; + +const SearchProgramField = withDefaultFieldContainer()(withLabel()(OptionsSelectVirtualized)); + +const programFieldStyles = { + labelContainerStyle: { + paddingTop: 12, + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; + +type Props = { + searchId: string, + selectedProgramId: ?string, + onSetProgram: (searchId: string, programId: ?string) => void, + programOptions: Array, +} +export class SearchProgramSelectorComponent extends React.Component { + onSelectProgram = (programId: ?string) => { + this.props.onSetProgram(this.props.searchId, programId); + } + render() { + return ( + + ); + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js new file mode 100644 index 0000000000..b596a23dc7 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/SearchProgramSelector.container.js @@ -0,0 +1,20 @@ +// @flow +import { connect } from 'react-redux'; +import { SearchProgramSelectorComponent } from './SearchProgramSelector.component'; +import { startSetProgram } from './searchProgramSelector.actions'; +import { getProgramOptions } from './getProgramOptions'; + +const mapStateToProps = (state: ReduxState, props: Object) => ({ + programOptions: getProgramOptions(props.selectedTrackedEntityTypeId), +}); + +const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ + onSetProgram: (searchId: string, programId: ?string) => { + dispatch(startSetProgram(searchId, programId)); + }, +}); + +// $FlowFixMe[missing-annot] automated comment +export const SearchProgramSelector = connect(mapStateToProps, mapDispatchToProps)( + SearchProgramSelectorComponent, +); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/getProgramOptions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/getProgramOptions.js new file mode 100644 index 0000000000..e4ff179bc1 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/getProgramOptions.js @@ -0,0 +1,12 @@ +// @flow +import { programCollection } from '../../../../../../metaDataMemoryStores'; +import { TrackerProgram } from '../../../../../../metaData'; + +// $FlowFixMe +export const getProgramOptions = (trackedEntityTypeId: string) => Array.from(programCollection.values()) + .filter(program => + program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.read, + ) + .map(p => ({ value: p.id, label: p.name })); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js new file mode 100644 index 0000000000..73a0c2ec2a --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchProgramSelector/searchProgramSelector.actions.js @@ -0,0 +1,15 @@ +// @flow + +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const batchActionTypes = { + BATCH_SET_TEI_SEARCH_PROGRAM: 'BatchSetTeiSearchProgram', +}; + +export const actionTypes = { + TEI_SEARCH_SET_PROGRAM: 'TeiSearchSetProgram', + TEI_SEARCH_START_SET_PROGRAM: 'TeiSearchStartSetProgram', +}; + +export const startSetProgram = (searchId: string, programId: ?string) => + actionCreator(actionTypes.TEI_SEARCH_START_SET_PROGRAM)({ searchId, programId }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js new file mode 100644 index 0000000000..e15c1f3771 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js @@ -0,0 +1,172 @@ +// @flow +import React, { type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import withStyles from '@material-ui/core/styles/withStyles'; +import { SearchGroup } from '../../../../../metaData'; +import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; +import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container'; +import { SearchProgramSelector } from './SearchProgramSelector/SearchProgramSelector.container'; +import { Section, SectionHeaderSimple } from '../../../../Section'; +import { ResultsPageSizeContext } from '../../../shared-contexts'; +import type { Props } from './TeiSearch.types'; + +const getStyles = (theme: Theme) => ({ + container: { + margin: theme.typography.pxToRem(10), + }, + programSection: { + backgroundColor: 'white', + maxWidth: theme.typography.pxToRem(900), + marginBottom: theme.typography.pxToRem(20), + }, + formContainerSection: { + maxWidth: theme.typography.pxToRem(900), + marginBottom: theme.typography.pxToRem(20), + }, +}); + +type State = { + programSectionOpen: boolean, +} + +class TeiSearchPlain extends React.Component { + constructor(props) { + super(props); + this.state = { programSectionOpen: true }; + } + + + getFormId = (searchGroupId: string) => { + const contextId = this.props.selectedProgramId || this.props.selectedTrackedEntityTypeId || ''; + return `${this.props.id}-${contextId}-${searchGroupId}`; + } + + handleSearch = (formId: string, searchGroupId: string) => { + const { id } = this.props; + this.props.onSearch(formId, searchGroupId, id); + } + + handleSearchResultsChangePage = (pageNumber: number) => { + this.props.onSearchResultsChangePage(this.props.id, pageNumber); + } + + handleNewSearch = () => { + this.props.onNewSearch(this.props.id); + } + + handleEditSearch = () => { + this.props.onEditSearch(this.props.id); + } + + handleSearchValidationFailed = (...args) => { + const { id } = this.props; + this.props.onSearchValidationFailed(...args, id); + } + + renderSearchForms = (searchGroups: Array) => ( +
+ {this.renderProgramSection()} + {this.renderSearchGroups(searchGroups)} +
+ ); + + renderProgramSection = () => { + const isCollapsed = !this.state.programSectionOpen; + return ( +
{ this.setState({ programSectionOpen: !!isCollapsed }); }} + title={i18n.t('Program')} + /> + } + > + +
+ ); + } + + onChangeSectionCollapseState = (id) => { + if (this.props.openSearchGroupSection === id) { + this.props.onSetOpenSearchGroupSection(this.props.id, null); + return; + } + this.props.onSetOpenSearchGroupSection(this.props.id, id); + } + + renderSearchGroups = (searchGroups: Array) => searchGroups.map((sg, i) => { + const searchGroupId = i.toString(); + const formId = this.getFormId(searchGroupId); + const header = sg.unique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : i18n.t('Search by attributes'); + const collapsed = this.props.openSearchGroupSection !== searchGroupId; + return ( +
{ this.onChangeSectionCollapseState(searchGroupId); }} + isCollapsed={collapsed} + title={header} + />} + > + +
+ ); + }) + renderSearchResult = () => { + const { + id, + searchGroups, + getResultsView, + selectedProgramId, + selectedTrackedEntityTypeId, + trackedEntityTypeName, + } = this.props; + return ( + + + + ); + } + + render() { + const searchGroups = this.props.searchGroups; + + if (this.props.showResults) { + return this.renderSearchResult(); + } + + return searchGroups ? this.renderSearchForms(searchGroups) : (
); + } +} + +export const TeiSearchComponent: ComponentType<$Diff> = withStyles(getStyles)(TeiSearchPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.js new file mode 100644 index 0000000000..d83ee31c00 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.js @@ -0,0 +1,52 @@ +// @flow +import { type ComponentType } from 'react'; +import { connect } from 'react-redux'; +import { TeiSearchComponent } from './TeiSearch.component'; +import { + requestSearchTei, + searchFormValidationFailed, + teiNewSearch, + teiEditSearch, + teiSearchResultsChangePage, + setOpenSearchGroupSection, +} from './actions/teiSearch.actions'; +import type { Props, OwnProps } from './TeiSearch.types'; +import { getSearchGroups } from './getSearchGroups'; +import { getTrackedEntityTypeThrowIfNotFound } from '../../../../../metaData'; + +const mapStateToProps = (state: ReduxState, props: OwnProps) => { + const { selectedProgramId, selectedTrackedEntityTypeId } = props; + const searchGroups = getSearchGroups(selectedTrackedEntityTypeId, selectedProgramId); + const { name } = getTrackedEntityTypeThrowIfNotFound(selectedTrackedEntityTypeId); + const currentTeiSearch = state.teiSearch[props.id] ?? {}; + return { + searchGroups, + showResults: !!currentTeiSearch.searchResults, + openSearchGroupSection: currentTeiSearch.openSearchGroupSection, + trackedEntityTypeName: name.toLowerCase(), + }; +}; + +const mapDispatchToProps = (dispatch: ReduxDispatch, ownProps: OwnProps) => ({ + onSearch: (formId: string, searchGroupId: string, searchId: string) => { + dispatch(requestSearchTei(formId, searchGroupId, searchId, ownProps.resultsPageSize)); + }, + onSearchResultsChangePage: (searchId: string, pageNumber: number) => { + dispatch(teiSearchResultsChangePage(searchId, pageNumber, ownProps.resultsPageSize)); + }, + onSearchValidationFailed: (formId: string, searchGroupId: string, searchId: string) => { + dispatch(searchFormValidationFailed(formId, searchGroupId, searchId)); + }, + onNewSearch: (searchId: string) => { + dispatch(teiNewSearch(searchId)); + }, + onEditSearch: (searchId: string) => { + dispatch(teiEditSearch(searchId)); + }, + onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => { + dispatch(setOpenSearchGroupSection(searchId, searchGroupId)); + }, +}); + +export const TeiSearch: ComponentType = + connect<$Diff, OwnProps, _, _, _, _>(mapStateToProps, mapDispatchToProps)(TeiSearchComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.js new file mode 100644 index 0000000000..b730ab69e4 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.js @@ -0,0 +1,28 @@ +// @flow +import { type SearchGroup } from '../../../../../metaData'; + +type PropsFromRedux = {| + searchGroups: ?Array, + showResults?: ?boolean, + openSearchGroupSection: ?string, + trackedEntityTypeName: string, +|} + +type DispatchersFromRedux = {| + onSearch: Function, + onSearchValidationFailed: Function, + onSetOpenSearchGroupSection: (searchId: string, searchGroupId: ?string) => void, + onSearchResultsChangePage: (searchId: string, pageNumber: number) => void, + onNewSearch: (searchId: string) => void, + onEditSearch: (searchId: string) => void, +|} + +export type OwnProps = {| + id: string, + getResultsView: Function, + resultsPageSize: number, + selectedTrackedEntityTypeId: string, + selectedProgramId: ?string, +|} + +export type Props = {| ...OwnProps, ...DispatchersFromRedux, ...PropsFromRedux, ...CssClasses |} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.js new file mode 100644 index 0000000000..4e753bdfe8 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.js @@ -0,0 +1,181 @@ +// @flow +import * as React from 'react'; +import log from 'loglevel'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import classNames from 'classnames'; +import { errorCreator } from 'capture-core-utils'; +import { Button } from '@dhis2/ui'; +import { D2Form } from '../../../../../D2Form'; +import { SearchOrgUnitSelector } from '../SearchOrgUnitSelector/SearchOrgUnitSelector.container'; +import type { SearchGroup } from '../../../../../../metaData'; +import { withGotoInterface } from '../../../../../FormFields/New'; + +const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); + +const getStyles = (theme: Theme) => ({ + orgUnitSection: { + backgroundColor: 'white', + padding: theme.typography.pxToRem(8), + maxWidth: theme.typography.pxToRem(892), + }, + searchButtonContainer: { + padding: theme.typography.pxToRem(10), + display: 'flex', + alignItems: 'center', + }, + minAttributesRequired: { + flexGrow: 1, + textAlign: 'right', + fontSize: theme.typography.pxToRem(14), + }, + minAttribtuesRequiredInvalid: { + color: theme.palette.error.main, + }, +}); + +type Props = { + id: string, + searchGroupId: string, + onSearch: (formId: string, searchGroupId: string) => void, + onSearchValidationFailed: (formId: string, SearchGroupId: string) => void, + searchAttempted: boolean, + searchId: string, + searchGroup: SearchGroup, + attributesWithValuesCount: number, + classes: { + container: string, + searchButtonContainer: string, + orgUnitSection: string, + minAttributesRequired: string, + minAttribtuesRequiredInvalid: string, + }, +}; + +class SearchFormPlain extends React.Component { + formInstance: any; + orgUnitSelectorInstance: SearchOrgUnitSelector; + + static errorMessages = { + NO_ITEM_SELECTED: 'No item selected', + SEARCH_FORM_MISSING: 'search form is missing. see log for details', + }; + + validNumberOfAttributes = () => { + const attributesWithValuesCount = this.props.attributesWithValuesCount; + const minAttributesRequiredToSearch = this.props.searchGroup.minAttributesRequiredToSearch; + return attributesWithValuesCount >= minAttributesRequiredToSearch; + } + + validateForm() { + if (!this.formInstance) { + log.error( + errorCreator( + SearchFormPlain.errorMessages.SEARCH_FORM_MISSING)({ Search: this }), + ); + return { + error: true, + isValid: false, + }; + } + + let isValid = this.formInstance.validateFormScrollToFirstFailedField({}); + + // $FlowFixMe[prop-missing] automated comment + if (isValid && !this.props.searchGroup.unique) isValid = this.orgUnitSelectorInstance.validateAndScrollToIfFailed(); + + if (isValid && !this.props.searchGroup.unique) isValid = this.validNumberOfAttributes(); + + return { + isValid, + error: false, + }; + } + + handleSearchAttempt = () => { + const { error: validateFormError, isValid: isFormValid } = this.validateForm(); + if (validateFormError || !isFormValid) { + this.props.onSearchValidationFailed(this.props.id, this.props.searchGroupId); + return; + } + this.props.onSearch(this.props.id, this.props.searchGroupId); + } + + getUniqueSearchButtonText = (searchForm) => { + const attributeName = searchForm.getElements()[0].formName; + return `Search ${attributeName}`; + } + + renderOrgUnitSelector = () => ( + { + this.orgUnitSelectorInstance = instance; + }} + searchId={this.props.searchId} + searchAttempted={this.props.searchAttempted} + /> + ); + + renderMinAttributesRequired = () => { + const { classes, searchAttempted, searchGroup } = this.props; + const displayInvalidNumberOfAttributes = searchAttempted && !this.validNumberOfAttributes(); + const minAttributesRequiredClass = classNames( + classes.minAttributesRequired, { + [classes.minAttribtuesRequiredInvalid]: displayInvalidNumberOfAttributes, + }, + ); + + return ( +
+ { + i18n.t('Fill in at least {{count}} attribute to search', { + count: searchGroup.minAttributesRequiredToSearch, + defaultValue: 'Fill in at least {{count}} attribute to search', + defaultValue_plural: 'Fill in at least {{count}} attributes to search', + }) + } +
+ ); + } + + render() { + const { searchGroup, classes, id } = this.props; + + const searchForm = searchGroup && searchGroup.searchForm; + + if (!searchForm) { + return ( +
+ {SearchFormPlain.errorMessages.SEARCH_FORM_MISSING} +
+ ); + } + const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) : i18n.t('Search by attributes'); + return ( +
+ { this.formInstance = formInstance; }} + formFoundation={searchGroup.searchForm} + id={id} + /> + {!searchGroup.unique && this.renderOrgUnitSelector()} +
+ + {!searchGroup.unique && this.renderMinAttributesRequired()} +
+
+ ); + } +} + +export const TeiSearchFormComponent = withStyles(getStyles)(SearchFormPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.container.js new file mode 100644 index 0000000000..898d2db20c --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.container.js @@ -0,0 +1,23 @@ +// @flow +import { connect } from 'react-redux'; +import { TeiSearchFormComponent } from './TeiSearchForm.component'; + +const getAttributesWithValuesCount = (state: ReduxState, formId: string) => { + const formValues = state.formsValues[formId] || {}; + return Object.keys(formValues).filter(key => formValues[key]).length; +}; + +const mapStateToProps = (state: ReduxState, props: Object) => { + const searchId = props.searchId; + const formId = props.id; + const formState = state.teiSearch[searchId] && state.teiSearch[searchId][formId] ? state.teiSearch[searchId][formId] : {}; + + return { + searchAttempted: formState.validationFailed, + attributesWithValuesCount: getAttributesWithValuesCount(state, formId), + }; +}; + +// $FlowSuppress +// $FlowFixMe[missing-annot] automated comment +export const TeiSearchForm = connect(mapStateToProps, () => ({}))(TeiSearchFormComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.component.js new file mode 100644 index 0000000000..0ca0ad0668 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.component.js @@ -0,0 +1,11 @@ +// @flow +import React from 'react'; +import type { Props } from './TeiSearchResults.types'; + + +export const TeiSearchResultsComponent = ({ getResultsView, ...passOnProps }: Props) => ( +
+ { getResultsView && getResultsView(passOnProps)} +
+); + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.container.js new file mode 100644 index 0000000000..0f939cab82 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.container.js @@ -0,0 +1,29 @@ +// @flow +import { type ComponentType } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { TeiSearchResultsComponent } from './TeiSearchResults.component'; +import { withLoadingIndicator } from '../../../../../../HOC'; +import type { OwnProps, Props } from './TeiSearchResults.types'; + +const mapStateToProps = (state: ReduxState, props: OwnProps) => { + const currentTeiSearch = state.teiSearch[props.id] || {}; + const searchResults = currentTeiSearch.searchResults || {}; + const searchValues = state.formsValues[searchResults.formId]; + const searchGroup = props.searchGroups[parseInt(searchResults.searchGroupId, 10)]; + return { + resultsLoading: searchResults.resultsLoading, + teis: searchResults.teis || [], + currentPage: searchResults.currentPage, + searchValues, + searchGroup, + }; +}; + +const mapDispatchToProps = () => ({}); + +export const TeiSearchResults: ComponentType = + compose( + connect(mapStateToProps, mapDispatchToProps), + withLoadingIndicator(() => ({ padding: '100px 0' }), null, props => (!props.resultsLoading)), + )(TeiSearchResultsComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.types.js new file mode 100644 index 0000000000..a55e711d15 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchResults/TeiSearchResults.types.js @@ -0,0 +1,24 @@ +// @flow +import { type SearchGroup } from '../../../../../../metaData'; + +export type OwnProps = {| + id: string, + searchGroups: any, + onChangePage: Function, + onNewSearch: Function, + onEditSearch: Function, + getResultsView: Function, + selectedProgramId: ?string, + selectedTrackedEntityTypeId: string, + trackedEntityTypeName: string, +|} + +type PropsFromRedux = {| + resultsLoading: boolean, + teis: any, + currentPage: number, + searchValues: any, + searchGroup: SearchGroup +|} + +export type Props = {|...OwnProps, ...PropsFromRedux |} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js new file mode 100644 index 0000000000..3b83a097fd --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js @@ -0,0 +1,70 @@ +// @flow + +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const batchActionTypes = { + BATCH_SET_TEI_SEARCH_PROGRAM_AND_TET: 'BatchSetTeiSearchProgramAndTet', + RESET_SEARCH_FORMS: 'ResetSearchForms', +}; + +export const actionTypes = { + INITIALIZE_TEI_SEARCH: 'InitializeTeiSearch', + REQUEST_SEARCH_TEI: 'RequestSearchTei', + SEARCH_FORM_VALIDATION_FAILED: 'SearchFormValidationFailed', + SEARCH_TEI_FAILED: 'SearchTeiFailed', + SEARCH_TEI_RESULT_RETRIEVED: 'SearchTeiResultRetrieved', + SET_TEI_SEARCH_PROGRAM_AND_TET: 'SetTeiSearchProgramAndTet', + TEI_NEW_SEARCH: 'TeiNewSearch', + TEI_EDIT_SEARCH: 'TeiEditSearch', + TEI_SEARCH_RESULTS_CHANGE_PAGE: 'TeiSearchResultsChangePage', + TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION: 'TeiSearchSetOpenSearchGroupSection', +}; + + +export const initializeTeiSearch = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => + actionCreator(actionTypes.INITIALIZE_TEI_SEARCH)({ searchId, programId, trackedEntityTypeId }); + +export const requestSearchTei = ( + formId: string, + searchGroupId: string, + searchId: string, + resultsPageSize: number, +) => + actionCreator(actionTypes.REQUEST_SEARCH_TEI)({ formId, searchGroupId, searchId, resultsPageSize }); + +export const searchTeiFailed = ( + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_TEI_FAILED)({ formId, searchGroupId, searchId }); + +export const searchTeiResultRetrieved = ( + data: any, + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_TEI_RESULT_RETRIEVED)({ data, formId, searchGroupId, searchId }); + +export const setProgramAndTrackedEntityType = (searchId: string, programId: ?string, trackedEntityTypeId: ?string) => + actionCreator(actionTypes.SET_TEI_SEARCH_PROGRAM_AND_TET)({ searchId, programId, trackedEntityTypeId }); + +export const searchFormValidationFailed = ( + formId: string, + searchGroupId: string, + searchId: string, +) => + actionCreator(actionTypes.SEARCH_FORM_VALIDATION_FAILED)({ formId, searchGroupId, searchId }); + +export const teiNewSearch = (searchId: string) => + actionCreator(actionTypes.TEI_NEW_SEARCH)({ searchId }); + +export const teiEditSearch = (searchId: string) => + actionCreator(actionTypes.TEI_EDIT_SEARCH)({ searchId }); + +export const teiSearchResultsChangePage = (searchId: string, pageNumber: number, resultsPageSize: number) => + actionCreator(actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE)({ searchId, pageNumber, resultsPageSize }); + +export const setOpenSearchGroupSection = (searchId: string, searchGroupId: ?string) => + actionCreator(actionTypes.TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION)({ searchId, searchGroupId }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js new file mode 100644 index 0000000000..6c73d6d2ff --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js @@ -0,0 +1,223 @@ +// @flow +import isArray from 'd2-utilizr/src/isArray'; +import { from, of } from 'rxjs'; +import { ofType } from 'redux-observable'; +import { map, takeUntil, switchMap, filter, catchError } from 'rxjs/operators'; +import { batchActions } from 'redux-batched-actions'; +import { convertValue as convertToClient } from '../../../../../../converters/formToClient'; +import { convertValue as convertToServer } from '../../../../../../converters/clientToServer'; +import { + convertValue as convertToFilters, + convertValueToEqual as convertToUniqueFilters, +} from '../serverToFilters'; +import { + actionTypes, + batchActionTypes, + searchTeiResultRetrieved, + searchTeiFailed, + setProgramAndTrackedEntityType, +} from '../actions/teiSearch.actions'; +import { + actionTypes as programSelectorActionTypes, +} from '../SearchProgramSelector/searchProgramSelector.actions'; +import { getSearchGroups } from '../getSearchGroups'; +import { getTrackedEntityInstances } from '../../../../../../trackedEntityInstances/trackedEntityInstanceRequests'; + +import { + addFormData, +} from '../../../../../D2Form/actions/form.actions'; +import { + getTrackerProgramThrowIfNotFound as getTrackerProgram, + getTrackedEntityTypeThrowIfNotFound as getTrackedEntityType, +} from '../../../../../../metaData'; +import { getSearchFormId } from '../getSearchFormId'; +import type { QuerySingleResource } from '../../../../../../utils/api'; + +const getOuQueryArgs = (orgUnit: ?Object, orgUnitScope: string) => + (orgUnitScope !== 'ACCESSIBLE' ? + { ou: orgUnit && orgUnit.id, ouMode: orgUnitScope } : + { ouMode: orgUnitScope }); + +const getContextQueryArgs = (programId: ?string, trackedEntityTypeId: string) => + (programId ? { program: programId } : { trackedEntityType: trackedEntityTypeId }); + +const getPagingQueryArgs = (pageNumber: ?number = 1) => ({ page: pageNumber, pageSize: 5 }); + + +const searchTei = ({ + state, + searchId, + formId, + searchGroupId, + pageNumber, + resultsPageSize, + absoluteApiPath, + querySingleResource, +}: { + state: ReduxState, + searchId: string, + formId: string, + searchGroupId: any, + pageNumber?: ?number, + resultsPageSize: number, + absoluteApiPath: string, + querySingleResource: QuerySingleResource +}) => { + const currentTeiSearch = state.teiSearch[searchId]; + const formValues = state.formsValues[formId]; + + const { + selectedProgramId, + selectedTrackedEntityTypeId, + selectedOrgUnit, + selectedOrgUnitScope, + } = currentTeiSearch; + + const searchGroups = getSearchGroups(selectedTrackedEntityTypeId, selectedProgramId); + const searchGroup = searchGroups[searchGroupId]; + const filterConverter = searchGroup.unique ? convertToUniqueFilters : convertToFilters; + + const filterValues = searchGroup.searchForm.convertValues(formValues, + (value, type, element) => + filterConverter(convertToServer(convertToClient(value, type), type), type, element)); + + const filters = Object.keys(filterValues).reduce((accFilters, key) => { + const value = filterValues[key]; + return isArray(value) ? [...accFilters, ...value] : [...accFilters, value]; + }, []).filter(f => f !== null && f !== undefined); + + const queryArgs = { + filter: filters, + fields: 'attributes,enrollments,trackedEntity,orgUnit', + ...getOuQueryArgs(selectedOrgUnit, selectedOrgUnitScope), + // $FlowFixMe[exponential-spread] automated comment + ...getContextQueryArgs(selectedProgramId, selectedTrackedEntityTypeId), + ...getPagingQueryArgs(pageNumber), + pageSize: resultsPageSize, + }; + + const attributes = selectedProgramId ? + getTrackerProgram(selectedProgramId).attributes : + getTrackedEntityType(selectedTrackedEntityTypeId).attributes; + + return from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource)).pipe( + map(({ trackedEntityInstanceContainers, pagingData }) => + searchTeiResultRetrieved( + { trackedEntityInstanceContainers, currentPage: pagingData.currentPage }, + formId, + searchGroupId, + searchId, + )), + catchError(() => of(searchTeiFailed(formId, searchGroupId, searchId))), + ); +}; + +export const teiSearchChangePageEpic = (action$: InputObservable, store: ReduxStore, { absoluteApiPath, querySingleResource }: ApiUtils) => + action$.pipe( + ofType(actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE), + switchMap((action) => { + const state = store.value; + const { pageNumber, searchId, resultsPageSize } = action.payload; + const currentTeiSearch = state.teiSearch[searchId]; + const searchTeiStream = searchTei({ + state, + searchId, + formId: currentTeiSearch.searchResults.formId, + searchGroupId: currentTeiSearch.searchResults.searchGroupId, + pageNumber, + resultsPageSize, + absoluteApiPath, + querySingleResource, + }); + + return from(searchTeiStream).pipe( + takeUntil( + action$.pipe(ofType( + actionTypes.REQUEST_SEARCH_TEI, + actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE, + ))), + takeUntil( + action$.pipe( + filter(ab => + isArray(ab.payload) && ab.payload.some(a => a.type === actionTypes.INITIALIZE_TEI_SEARCH)))), + ); + })); + +export const teiSearchEpic = (action$: InputObservable, store: ReduxStore, { absoluteApiPath, querySingleResource }: ApiUtils) => + action$.pipe( + ofType(actionTypes.REQUEST_SEARCH_TEI), + switchMap((action) => { + const state = store.value; + const { formId, searchGroupId, searchId, resultsPageSize } = action.payload; + const searchTeiStream = searchTei({ + state, + searchId, + formId, + searchGroupId, + pageNumber: 1, + resultsPageSize, + absoluteApiPath, + querySingleResource, + }); + return from(searchTeiStream).pipe( + takeUntil(action$.pipe( + ofType( + actionTypes.REQUEST_SEARCH_TEI, + actionTypes.TEI_SEARCH_RESULTS_CHANGE_PAGE, + ))), + takeUntil( + action$.pipe( + filter(ab => + isArray(ab.payload) && ab.payload.some(a => a.type === actionTypes.INITIALIZE_TEI_SEARCH))))); + })); + +export const teiSearchSetProgramEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType(programSelectorActionTypes.TEI_SEARCH_START_SET_PROGRAM), + map((action) => { + const state = store.value; + const searchId = action.payload.searchId; + const programId = action.payload.programId; + let trackedEntityTypeId = state.teiSearch[searchId].selectedTrackedEntityTypeId; + const contextId = programId || trackedEntityTypeId; + if (programId) { + const program = getTrackerProgram(programId); + trackedEntityTypeId = program.trackedEntityType.id; + } + let searchGroups = []; + if (trackedEntityTypeId) { + searchGroups = getSearchGroups(trackedEntityTypeId, programId); + } + + const addFormDataActions = searchGroups ? searchGroups.map((sg, i) => { + const key = getSearchFormId(searchId, contextId, i.toString()); + return addFormData(key, {}); + }) : []; + + return batchActions([ + ...addFormDataActions, + setProgramAndTrackedEntityType(searchId, programId, trackedEntityTypeId), + ], batchActionTypes.BATCH_SET_TEI_SEARCH_PROGRAM_AND_TET); + })); + +export const teiNewSearchEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType(actionTypes.TEI_NEW_SEARCH), + map((action) => { + const state = store.value; + const searchId = action.payload.searchId; + const currentTeiSearch = state.teiSearch[searchId]; + + const contextId = currentTeiSearch.selectedProgramId || currentTeiSearch.selectedTrackedEntityTypeId; + + const searchGroups = getSearchGroups(currentTeiSearch.selectedTrackedEntityTypeId, currentTeiSearch.selectedProgramId); + + const addFormDataActions = searchGroups ? searchGroups.map((sg, i) => { + const key = getSearchFormId(searchId, contextId, i.toString()); + return addFormData(key, {}); + }) : []; + + return batchActions([ + ...addFormDataActions, + ], batchActionTypes.RESET_SEARCH_FORMS); + })); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchFormId.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchFormId.js new file mode 100644 index 0000000000..21eaebfce3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchFormId.js @@ -0,0 +1,10 @@ +// @flow + +export function getSearchFormId( + searchId: string, + contextId: string, + searchGroupId: string, +) { + return `${searchId}-${contextId}-${searchGroupId}`; +} + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchGroups.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchGroups.js new file mode 100644 index 0000000000..59ec69fdbf --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/getSearchGroups.js @@ -0,0 +1,19 @@ +// @flow +import { + getTrackedEntityTypeThrowIfNotFound, + getTrackerProgramThrowIfNotFound, +} from '../../../../../metaData'; +import type { + SearchGroup, +} from '../../../../../metaData'; + + +export function getSearchGroups(trackedEntityTypeId: string, programId: ?string): Array { + if (programId) { + const program = getTrackerProgramThrowIfNotFound(programId); + return program.searchGroups; + } + const trackedEntityType = getTrackedEntityTypeThrowIfNotFound(trackedEntityTypeId); + return trackedEntityType.searchGroups; +} + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js new file mode 100644 index 0000000000..8fb29787f3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js @@ -0,0 +1,38 @@ +// @flow +import { type DataElement, dataElementTypes } from '../../../../../metaData'; + +type RangeValue = { + from: number, + to: number, +} + +const equals = (value: any, elementId: string) => `${elementId}:eq:${value}`; +const like = (value: any, elementId: string) => `${elementId}:like:${value}`; + + +function convertRange(value: RangeValue, elementId: string) { + return `${elementId}:ge:${value.from}:le:${value.to}`; +} + +const valueConvertersForType = { + [dataElementTypes.TEXT]: like, + [dataElementTypes.NUMBER_RANGE]: convertRange, + [dataElementTypes.DATE_RANGE]: convertRange, + [dataElementTypes.DATETIME_RANGE]: convertRange, + [dataElementTypes.TIME_RANGE]: convertRange, +}; + +export function convertValue(value: any, type: $Keys, metaElement: DataElement) { + if (!value && value !== 0 && value !== false) { + return value; + } + // $FlowFixMe dataElementTypes flow error + return valueConvertersForType[type] ? valueConvertersForType[type](value, metaElement.id) : equals(value, metaElement.id); +} + +export function convertValueToEqual(value: any, type: $Keys, metaElement: DataElement) { + if (!value && value !== 0 && value !== false) { + return value; + } + return equals(value, metaElement.id); +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js new file mode 100644 index 0000000000..3e03b3606f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js @@ -0,0 +1,14 @@ +// @flow +import { actionCreator } from '../../../../../actions/actions.utils'; +import type { OnSelectFindModeProps } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; + +export const actionTypes = { + WIDGET_SELECT_FIND_MODE: 'widgetSelectFindMode', +}; + +export const batchActionTypes = { + BATCH_OPEN_TEI_SEARCH_WIDGET: 'batchOpenTeiSearchWidget', +}; + +export const selectFindMode = ({ findMode, relationshipConstraint }: OnSelectFindModeProps) => + actionCreator(actionTypes.WIDGET_SELECT_FIND_MODE)({ findMode, relationshipConstraint }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index 7f60c1bb53..3ad179ad85 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -1,9 +1,21 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useDispatch } from 'react-redux'; +import { selectFindMode } from './TrackedEntityRelationshipsWrapper.actions'; import { useTEIRelationshipsWidgetMetadata } from '../useTEIRelationshipsWidgetMetadata'; -import { WidgetTrackedEntityRelationship } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; +import { + WidgetTrackedEntityRelationship, +} from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; +import type { OnSelectFindModeProps } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { Props } from './TrackedEntityRelationshipsWrapper.types'; +import { TeiSearch } from '../TeiSearch/TeiSearch.container'; +import { + TeiRelationshipSearchResults, +} from '../../../NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; +import type { + OnLinkToTrackedEntity, +} from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types'; export const TrackedEntityRelationshipsWrapper = ({ trackedEntityTypeId, @@ -14,8 +26,13 @@ export const TrackedEntityRelationshipsWrapper = ({ onCloseAddRelationship, onLinkedRecordClick, }: Props) => { + const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); + const onSelectFindMode = ({ findMode, relationshipConstraint }: OnSelectFindModeProps) => { + dispatch(selectFindMode({ findMode, relationshipConstraint })); + }; + if (isError) { return (
@@ -39,7 +56,26 @@ export const TrackedEntityRelationshipsWrapper = ({ onOpenAddRelationship={onOpenAddRelationship} onCloseAddRelationship={onCloseAddRelationship} // optional props + onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} + renderTrackedEntitySearch={( + searchTrackedEntityTypeId: string, + searchProgramId: string, + onLinkToTrackedEntity: OnLinkToTrackedEntity, + ) => ( + ( + + )} + /> + )} /> ); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js new file mode 100644 index 0000000000..51c7751174 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js @@ -0,0 +1,37 @@ +// @flow +import { filter, map } from 'rxjs/operators'; +import { ofType } from 'redux-observable'; +import { batchActions } from 'redux-batched-actions'; +import { actionTypes, batchActionTypes } from './TrackedEntityRelationshipsWrapper.actions'; +import { getSearchGroups } from '../../../../TeiSearch/getSearchGroups'; +import { getSearchFormId } from '../../../../TeiSearch/getSearchFormId'; +import { addFormData } from '../../../../D2Form/actions/form.actions'; +import { initializeTeiSearch } from '../../../../TeiSearch/actions/teiSearch.actions'; + +const searchId = 'relationshipTeiSearchWidget'; + +export const openRelationshipTeiSearchWidgetEpic = + (action$: InputObservable) => + action$.pipe( + ofType(actionTypes.WIDGET_SELECT_FIND_MODE), + filter(action => action.payload.findMode && action.payload.findMode === 'TEI_SEARCH'), + map((action) => { + const { relationshipConstraint } = action.payload; + const { trackedEntityTypeId, programId } = relationshipConstraint; + + const contextId = programId || trackedEntityTypeId; + + const searchGroups = getSearchGroups(trackedEntityTypeId, programId); + + + const addFormDataActions = searchGroups ? searchGroups.map((sg, i) => { + const key = getSearchFormId(searchId, contextId, i.toString()); + return addFormData(key, {}); + }) : []; + + return batchActions([ + ...addFormDataActions, + initializeTeiSearch(searchId, programId, trackedEntityTypeId), + ], batchActionTypes.BATCH_OPEN_TEI_SEARCH_WIDGET); + }), + ); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js index 6ca3eb46ab..f2348d839d 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js @@ -1,2 +1,3 @@ // @flow export { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper.component'; +export { openRelationshipTeiSearchWidgetEpic } from './TrackedEntityRelationshipsWrapper.epics'; diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.js index 35e13c4ff3..84161942ef 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.js @@ -286,8 +286,8 @@ const SearchFormIndex = ({ header={ { setExpandedFormId(formId); }} isCollapseButtonEnabled={isSearchSectionCollapsed} isCollapsed={isSearchSectionCollapsed} diff --git a/src/core_modules/capture-core/components/Section/SectionHeaderSimple.component.js b/src/core_modules/capture-core/components/Section/SectionHeaderSimple.component.js index 8355acb967..9da358e20b 100644 --- a/src/core_modules/capture-core/components/Section/SectionHeaderSimple.component.js +++ b/src/core_modules/capture-core/components/Section/SectionHeaderSimple.component.js @@ -71,11 +71,13 @@ class SectionHeaderSimplePlain extends Component { style={accContainerStyle} {...containerProps} > -
- {this.props.title} +
+
+ {this.props.title} +
{ + const { mutate } = useAddRelationship({ + teiId, + onMutate: () => onSave(), + }); + const [currentStep, setCurrentStep] = useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); const [selectedLinkedEntityMetadata: LinkedEntityMetadata, setSelectedLinkedEntityMetadata] = useState(undefined); + const onLinkToTrackedEntityFromSearch = useCallback( + (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => { + if (!selectedLinkedEntityMetadata) return; + const { relationshipId, targetSide } = selectedLinkedEntityMetadata; + + const apiData = targetSide === TARGET_SIDES.TO ? + { from: { trackedEntity: { trackedEntity: teiId } }, to: { trackedEntity: { trackedEntity: linkedTrackedEntityId } } } : + { from: { trackedEntity: { trackedEntity: linkedTrackedEntityId } }, to: { trackedEntity: { trackedEntity: teiId } } }; + + const clientData = { + createdAt: new Date().toISOString(), + ...apiData, + }; + + if (attributes) { + const targetSideLC = targetSide.toLowerCase(); + clientData[targetSideLC].trackedEntity = { + ...clientData[targetSideLC].trackedEntity, + attributes: Object.keys(attributes).map(attributeId => ({ + attribute: attributeId, + value: attributes[attributeId], + })), + }; + } + + mutate({ + relationship: { + relationshipType: relationshipId, + ...apiData, + }, + clientRelationship: { + relationshipType: relationshipId, + ...clientData, + }, + }); + }, [mutate, selectedLinkedEntityMetadata, teiId]); + const handleNavigation = useCallback( (destination: $Values) => { setCurrentStep(destination); @@ -55,11 +103,23 @@ const NewTrackedEntityRelationshipPlain = ({ setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE); }, []); - const handleSearchRetrieverModeSelected = useCallback(() => - setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY), []); + const handleSearchRetrieverModeSelected = useCallback(() => { + if (selectedLinkedEntityMetadata) { + onSelectFindMode && onSelectFindMode({ + findMode: 'TEI_SEARCH', + relationshipConstraint: { + programId: selectedLinkedEntityMetadata?.programId, + trackedEntityTypeId: selectedLinkedEntityMetadata.trackedEntityTypeId, + }, + }); + } + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY); + }, [onSelectFindMode, selectedLinkedEntityMetadata]); + const handleNewRetrieverModeSelected = useCallback(() => setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY), []); + const stepContents = useMemo(() => { if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.id) { return ( @@ -85,39 +145,28 @@ const NewTrackedEntityRelationshipPlain = ({ } // Steps below will be implemented by new PR - /* if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { + if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { const { trackedEntityTypeId: linkedEntityTrackedEntityTypeId, programId: linkedEntityProgramId, // $FlowFixMe business logic dictates that we will have the linkedEntityMetadata at this step }: LinkedEntityMetadata = selectedLinkedEntityMetadata; - return ( - - ); - } */ + if (renderTrackedEntitySearch) { + return renderTrackedEntitySearch( + linkedEntityTrackedEntityTypeId, + linkedEntityProgramId, + onLinkToTrackedEntityFromSearch, + ); + } + } return (
{i18n.t('Missing implementation step')}
); - }, [ - currentStep.id, - handleLinkedEntityMetadataSelection, - handleNewRetrieverModeSelected, - handleSearchRetrieverModeSelected, - programId, - relationshipTypes, - selectedLinkedEntityMetadata?.trackedEntityName, - trackedEntityTypeId, - ]); + }, [currentStep.id, handleLinkedEntityMetadataSelection, handleNewRetrieverModeSelected, handleSearchRetrieverModeSelected, onLinkToTrackedEntityFromSearch, programId, relationshipTypes, renderTrackedEntitySearch, selectedLinkedEntityMetadata, trackedEntityTypeId]); return (
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js index e887e4d0f3..a7e94b5233 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -14,11 +14,14 @@ const styles = { export const NewTrackedEntityRelationshipPlain = ({ renderElement, + teiId, programId, relationshipTypes, trackedEntityTypeId, onCloseAddRelationship, onOpenAddRelationship, + renderTrackedEntitySearch, + onSelectFindMode, classes, }: StyledContainerProps) => { const [addWizardVisible, setAddWizardVisibility] = useState(false); @@ -47,11 +50,14 @@ export const NewTrackedEntityRelationshipPlain = ({ addWizardVisible && ( ) } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index 7c23d8e7b1..f9a7178df3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -1,13 +1,21 @@ // @flow +import * as React from 'react'; import type { RelationshipTypes } from '../../common/Types'; +import type { OnLinkToTrackedEntity, OnSelectFindMode } from '../WidgetTrackedEntityRelationship.types'; + +type RenderTrackedEntitySearch = + (trackedEntityTypeId: string, programId: string, onLinkToTrackedEntity: OnLinkToTrackedEntity) => React.Element export type ContainerProps = $ReadOnly<{| + teiId: string, renderElement: HTMLElement, relationshipTypes: RelationshipTypes, trackedEntityTypeId: string, programId: string, + renderTrackedEntitySearch?: RenderTrackedEntitySearch, onCloseAddRelationship?: () => void, onOpenAddRelationship?: () => void, + onSelectFindMode?: OnSelectFindMode, |}>; export type StyledContainerProps = $ReadOnly<{| @@ -17,11 +25,14 @@ export type StyledContainerProps = $ReadOnly<{| export type PortalProps = $ReadOnly<{| renderElement: HTMLElement, + teiId: string, relationshipTypes: RelationshipTypes, trackedEntityTypeId: string, programId: string, onSave: () => void, onCancel: () => void, + onSelectFindMode?: OnSelectFindMode, + renderTrackedEntitySearch?: RenderTrackedEntitySearch, |}>; export type StyledPortalProps = $ReadOnly<{| @@ -31,11 +42,14 @@ export type StyledPortalProps = $ReadOnly<{| export type ComponentProps = $ReadOnly<{| + teiId: string, relationshipTypes: RelationshipTypes, trackedEntityTypeId: string, programId: string, onSave: () => void, onCancel: () => void, + renderTrackedEntitySearch?: RenderTrackedEntitySearch, + onSelectFindMode?: OnSelectFindMode, |}>; export type StyledComponentProps = $ReadOnly<{| diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js new file mode 100644 index 0000000000..e8405e6142 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js @@ -0,0 +1,51 @@ +// @flow +import { useMutation, useQueryClient } from 'react-query'; +import { useDataEngine } from '@dhis2/app-runtime'; + +type Props = { + teiId: string; + onMutate?: () => void; + onSuccess?: () => void; +} + +const ReactQueryAppNamespace = 'capture'; + +const addRelationshipMutation = { + resource: '/tracker?async=false&importStrategy=CREATE', + type: 'create', + data: ({ relationship }) => ({ + relationships: [relationship], + }), +}; + +export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { + const queryClient = useQueryClient(); + const dataEngine = useDataEngine(); + // $FlowFixMe - Is there something wrong with the types? + const { mutate } = useMutation( + ({ relationship }) => dataEngine.mutate(addRelationshipMutation, { + variables: { + relationship, + }, + }), + { + onMutate: (...props) => { + onMutate && onMutate(...props); + const { clientRelationship } = props[0]; + queryClient.setQueryData([ReactQueryAppNamespace, 'relationships', teiId], (oldData) => { + const instances = oldData?.instances || []; + const updatedInstances = [clientRelationship, ...instances]; + return { instances: updatedInstances }; + }); + }, + onSuccess: (...props) => { + queryClient.invalidateQueries([ReactQueryAppNamespace, 'relationships']); + onSuccess && onSuccess(...props); + }, + }, + ); + + return { + mutate, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js index d1e787d187..b468d184a3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js @@ -15,6 +15,8 @@ export const WidgetTrackedEntityRelationship = ({ onLinkedRecordClick, onOpenAddRelationship, onCloseAddRelationship, + onSelectFindMode, + renderTrackedEntitySearch, }: WidgetTrackedEntityRelationshipProps) => { const { data: relationships, isError } = useRelationships(teiId, RelationshipSearchEntities.TRACKED_ENTITY); @@ -37,12 +39,15 @@ export const WidgetTrackedEntityRelationship = ({ { relationshipTypes => ( ) } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js index fae3a3061a..83da745386 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -1,7 +1,23 @@ // @flow +import * as React from 'react'; import type { RelationshipTypes } from '../common/Types'; import type { LinkedRecordClick } from '../common/RelationshipsWidget'; +export type RelationshipConstraint = {| + trackedEntityTypeId: string, + programId: ?string, +|} + +export type OnLinkToTrackedEntity = + (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => void; + +export type OnSelectFindModeProps = {| + findMode: string, + relationshipConstraint: RelationshipConstraint, +|} + +export type OnSelectFindMode = (OnSelectFindModeProps) => void + export type WidgetTrackedEntityRelationshipProps = {| trackedEntityTypeId: string, teiId: string, @@ -11,4 +27,10 @@ export type WidgetTrackedEntityRelationshipProps = {| onOpenAddRelationship?: () => void, onCloseAddRelationship?: () => void, relationshipTypes?: RelationshipTypes, + onSelectFindMode?: OnSelectFindMode, + renderTrackedEntitySearch?: ( + trackedEntityTypeId: string, + programId: string, + onLinkToTrackedEntity: OnLinkToTrackedEntity, + ) => React.Element, |}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js index 0176a26897..a1a65f2107 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js @@ -1,6 +1,11 @@ // @flow export { WidgetTrackedEntityRelationship } from './WidgetTrackedEntityRelationship.component'; -export type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; +export type { + WidgetTrackedEntityRelationshipProps, + RelationshipConstraint, + OnSelectFindModeProps, + OnSelectFindMode, +} from './WidgetTrackedEntityRelationship.types'; export type { RelationshipTypes } from '../common/Types'; export type { LinkedRecordClick, @@ -8,3 +13,4 @@ export type { NavigationArgsEvent, NavigationArgsTrackedEntity, } from '../common/RelationshipsWidget'; +export { useAddRelationship } from './NewTrackedEntityRelationship/hooks/useAddRelationship'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js index afd147fc10..0d36658b89 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js @@ -5,13 +5,10 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; import { dataElementTypes } from '../../../../metaData'; -import { RELATIONSHIP_ENTITIES } from '../../common/constants'; -import { convertServerToClient, convertClientToList } from '../../../../converters'; +import { RELATIONSHIP_ENTITIES } from '../constants'; +import { convertClientToList, convertServerToClient } from '../../../../converters'; import type { GroupedLinkedEntities, LinkedEntityData } from './types'; -import type { - InputRelationshipData, - RelationshipTypes, -} from '../Types'; +import type { InputRelationshipData, RelationshipTypes } from '../Types'; const getFallbackFieldsByRelationshipEntity = { @@ -155,68 +152,64 @@ export const useGroupedLinkedEntities = ( sourceId: string, relationshipTypes: ?RelationshipTypes, relationships?: Array, -): GroupedLinkedEntities => { - const groupedLinkedEntities = useMemo(() => { - if (!relationships?.length || !relationshipTypes?.length) { - return []; - } - - - return relationships - .sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt))) - .reduce((accGroupedLinkedEntities, relationship) => { - const { - relationshipType: relationshipTypeId, - from: fromEntity, - to: toEntity, - createdAt: relationshipCreatedAt, - } = relationship; - - const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); - if (!relationshipType) { - log.error( - errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), - ); - return accGroupedLinkedEntities; - } - - const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); - if (!apiLinkedEntity) { - return accGroupedLinkedEntities; - } - - const linkedEntityData = getLinkedEntityData(apiLinkedEntity, relationshipCreatedAt); - if (!linkedEntityData) { - return accGroupedLinkedEntities; - } - - const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; - const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); - if (group) { - group.linkedEntities = [ - ...group.linkedEntities, - linkedEntityData, - ]; - } else { - const { constraint, name } = apiLinkedEntity === fromEntity ? - { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : - { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; - - const columns = getColumns(constraint); - const context = getContext(constraint); - - accGroupedLinkedEntities.push({ - id: groupId, - name: name || relationshipType.displayName, - linkedEntities: [linkedEntityData], - columns, - context, - }); - } +): GroupedLinkedEntities => useMemo(() => { + if (!relationships?.length || !relationshipTypes?.length) { + return []; + } + return relationships + .sort((a, b) => moment(b.createdAt) + .diff(moment(a.createdAt))) + .reduce((accGroupedLinkedEntities, relationship) => { + const { + relationshipType: relationshipTypeId, + from: fromEntity, + to: toEntity, + createdAt: relationshipCreatedAt, + } = relationship; + + const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); + if (!relationshipType) { + log.error( + errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), + ); return accGroupedLinkedEntities; - }, []); - }, [relationships, relationshipTypes, sourceId]); + } - return groupedLinkedEntities; -}; + const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); + if (!apiLinkedEntity) { + return accGroupedLinkedEntities; + } + + const linkedEntityData = getLinkedEntityData(apiLinkedEntity, relationshipCreatedAt); + if (!linkedEntityData) { + return accGroupedLinkedEntities; + } + + const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; + const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); + if (group) { + group.linkedEntities = [ + ...group.linkedEntities, + linkedEntityData, + ]; + } else { + const { constraint, name } = apiLinkedEntity === fromEntity ? + { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : + { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; + + const columns = getColumns(constraint); + const context = getContext(constraint); + + accGroupedLinkedEntities.push({ + id: groupId, + name: name || relationshipType.displayName, + linkedEntities: [linkedEntityData], + columns, + context, + }); + } + + return accGroupedLinkedEntities; + }, []); +}, [relationships, relationshipTypes, sourceId]); diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index 6cce6ae2eb..2bc7d1dfda 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -214,6 +214,9 @@ import { import { orgUnitFetcherEpic, } from '../core_modules/capture-core/components/OrgUnitFetcher'; +import { + openRelationshipTeiSearchWidgetEpic, +} from '../core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; export const epics = combineEpics( resetProgramAfterSettingOrgUnitIfApplicableEpic, @@ -225,6 +228,7 @@ export const epics = combineEpics( initEventListEpic, initTeiViewEpic, updateTeiListEpic, + openRelationshipTeiSearchWidgetEpic, updateEventListEpic, retrieveTemplatesEpic, updateTemplateEpic, From 350d2ac75304a580a8f555006fb83027d00ec23b Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Fri, 28 Jul 2023 11:41:46 +0200 Subject: [PATCH 79/95] chore: flow --- .../hooks/useEnrollmentFormFoundation.js | 1 + .../hooks/useTrackedEntityTypeCollection.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js index 49a7b484be..0238087811 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js @@ -30,6 +30,7 @@ export const useEnrollmentFormFoundation = ({ locale, }: Props) => { const { data: enrollment, isLoading, error } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['enrollmentForm', program?.id], () => buildEnrollmentForm({ // $FlowFixMe - Flow does not understand that the values are not null here diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js index ae3d4c3e5a..05a173cfc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js @@ -26,6 +26,7 @@ export const useTrackedEntityTypeCollection = ({ locale, }: Props): ReturnValues => { const { data: trackedEntityAttributes } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityAttributes', trackedEntityType?.id], () => getTrackedEntityAttributes( trackedEntityType @@ -40,6 +41,7 @@ export const useTrackedEntityTypeCollection = ({ ); const { data: trackedEntityTypeCollection } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityTypeCollection', trackedEntityType?.id], () => buildTrackedEntityTypeCollection({ // $FlowFixMe From 438d2cd5c82696ad1bf063f45bea23bd04fb6d9b Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Fri, 28 Jul 2023 11:49:49 +0200 Subject: [PATCH 80/95] feat: [DHIS2-12362][DHIS2-12455] View and filter relationships (#3241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: jasminenguyennn Co-authored-by: JasmineNg <89806888+jasminenguyennn@users.noreply.github.com> Co-authored-by: Joakim Storløkken Melseth --- i18n/en.pot | 37 ++- src/components/App/AppPages.component.js | 26 +- .../hooks/useEnrollmentFormFoundation.js | 1 + .../hooks/useTrackedEntityTypeCollection.js | 2 + .../Pages/Enrollment/EnrollmentPage.epics.js | 1 + .../EnrollmentPageDefault.component.js | 138 ++++++----- .../EnrollmentPageDefault.container.js | 5 + .../EnrollmentPageDefault.types.js | 6 +- .../AddRelationshipRefWrapper.component.js | 21 ++ .../AddRelationshipRefWrapper/index.js | 3 + .../EnrollmentEditEventPage.component.js | 180 ++++++++------ .../EnrollmentEditEventPage.container.js | 10 +- .../EnrollmentEditEventPage.types.js | 3 + .../useCommonEnrollmentDomainData.types.js | 2 +- ...kedEntityRelationshipsWrapper.component.js | 46 ++++ ...TrackedEntityRelationshipsWrapper.types.js | 13 + .../index.js | 2 + .../common/TEIRelationshipsWidget/index.js | 4 + .../useLinkedRecordClick.js | 43 ++++ .../useTEIRelationshipsWidgetMetadata.js | 47 ++++ .../Relationships/Relationships.component.js | 3 +- .../Widget/WidgetCollapsible.component.js | 1 - .../Breadcrumbs/Breadcrumbs.component.js | 76 ++++++ .../Breadcrumbs/breadcrumbs.types.js | 13 + .../Breadcrumbs/index.js | 2 + .../LinkedEntityMetadataSelector.component.js | 28 +++ .../LinkedEntityMetadataSelector/index.js | 3 + .../linkedEntityMetadataSelector.types.js | 37 +++ .../useApplicableTypesAndSides.js | 179 ++++++++++++++ .../NewTrackedEntityRelationship.component.js | 146 ++++++++++++ .../NewTrackedEntityRelationship.const.js | 5 + .../NewTrackedEntityRelationship.container.js | 63 +++++ .../NewTrackedEntityRelationship.portal.js | 10 + .../NewTrackedEntityRelationship.types.js | 44 ++++ .../RetrieverModeSelector.component.js | 45 ++++ .../RetrieverModeSelector/index.js | 2 + .../retrieverModeSelector.types.js | 12 + .../common/index.js | 2 + .../common/targetSides.js | 9 + .../NewTrackedEntityRelationship/index.js | 2 + .../wizardSteps.const.js | 6 + ...dgetTrackedEntityRelationship.component.js | 51 ++++ .../WidgetTrackedEntityRelationship.types.js | 14 ++ .../WidgetTrackedEntityRelationship/index.js | 10 + .../LinkedEntityMetadataSelector.component.js | 73 ++++++ .../LinkedEntityMetadataSelector/index.js | 3 + .../linkedEntityMetadataSelector.types.js | 36 +++ .../LinkedEntitiesViewer.component.js | 45 ++++ .../LinkedEntityTable.component.js | 71 ++++++ .../LinkedEntityTableBody.component.js | 56 +++++ .../LinkedEntityTableHeader.component.js | 25 ++ .../RelationshipsWidget.component.js | 71 ++++++ .../common/RelationshipsWidget/index.js | 3 + .../linkedEntitiesViewer.types.js | 12 + .../linkedEntityTable.types.js | 14 ++ .../linkedEntityTableBody.types.js | 14 ++ .../linkedEntityTableHeader.types.js | 6 + .../relationshipsWidget.types.js | 18 ++ .../types/GroupedLinkedEntities.types.js | 50 ++++ .../common/RelationshipsWidget/types/index.js | 3 + .../types/navigation.types.js | 15 ++ .../useGroupedLinkedEntities.js | 222 ++++++++++++++++++ .../useRelationshipTypes.js | 92 ++++++++ .../common/Types/RelationshipData.types.js | 31 +++ .../common/Types/RelationshipTypes.types.js | 92 ++++++++ .../WidgetsRelationship/common/Types/index.js | 4 + .../WidgetsRelationship/common/constants.js | 10 + .../common/useRelationships/index.js | 2 + .../useRelationships/useRelationships.js | 31 +++ .../extractElementIdsFromRelationshipTypes.js | 32 +++ .../common/utils/formatRelationshipTypes.js | 82 +++++++ .../WidgetsRelationship/common/utils/index.js | 3 + .../utils/replaceElementIdsWithElement.js | 69 ++++++ .../components/WidgetsRelationship/index.js | 3 + .../RelationshipType/RelationshipType.js | 18 ++ .../storeRelationshipTypes.js | 2 +- .../enrollmentPage.reducerDescription.js | 1 + .../organisationUnits.reducerDescription.js | 2 +- .../relationships/relationshipRequests.js | 2 +- .../storageControllers/cache.types.js | 2 + .../capture-core/storageControllers/index.js | 1 + .../useProgramFromIndexedDB.js | 4 +- .../useTrackedEntityTypeFromIndexedDB.js | 4 +- .../utils/reactQueryHelpers/index.js | 9 +- .../utils/reactQueryHelpers/query/index.js | 3 +- .../query/useApiDataQuery.js | 27 +++ .../query/useMetadataQuery.js | 38 ++- .../query/useMetadataQuery.types.js | 9 +- .../reactQueryHelpers.const.js | 4 + .../utils/routing/buildUrlQueryString.js | 5 +- 90 files changed, 2509 insertions(+), 163 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js create mode 100644 src/core_modules/capture-core/components/WidgetsRelationship/index.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js create mode 100644 src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js diff --git a/i18n/en.pot b/i18n/en.pot index 3119626801..f69fbe22a0 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-06-01T08:11:59.116Z\n" -"PO-Revision-Date: 2023-06-01T08:11:59.116Z\n" +"POT-Creation-Date: 2023-07-25T09:23:57.018Z\n" +"PO-Revision-Date: 2023-07-25T09:23:57.018Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -938,6 +938,9 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Could not retrieve metadata. Please try again later." +msgstr "Could not retrieve metadata. Please try again later." + msgid "Possible duplicates found" msgstr "Possible duplicates found" @@ -1347,6 +1350,36 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "New TEI Relationship" +msgstr "New TEI Relationship" + +msgid "Missing implementation step" +msgstr "Missing implementation step" + +msgid "Go back without saving relationship" +msgstr "Go back without saving relationship" + +msgid "New Relationship" +msgstr "New Relationship" + +msgid "Link to an existing {{tetName}}" +msgstr "Link to an existing {{tetName}}" + +msgid "Something went wrong while loading relationships. Please try again later." +msgstr "Something went wrong while loading relationships. Please try again later." + +msgid "TEI's Relationships" +msgstr "TEI's Relationships" + +msgid "Type" +msgstr "Type" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "Working list could not be loaded" msgstr "Working list could not be loaded" diff --git a/src/components/App/AppPages.component.js b/src/components/App/AppPages.component.js index 2203baf361..0af49adb99 100644 --- a/src/components/App/AppPages.component.js +++ b/src/components/App/AppPages.component.js @@ -9,17 +9,21 @@ import { EnrollmentPage } from 'capture-core/components/Pages/Enrollment'; import { StageEventListPage } from 'capture-core/components/Pages/StageEvent'; import { EnrollmentEditEventPage } from 'capture-core/components/Pages/EnrollmentEditEvent'; import { EnrollmentAddEventPage } from 'capture-core/components/Pages/EnrollmentAddEvent'; +import { ReactQueryDevtools } from 'react-query/devtools'; export const AppPages = () => ( - - - - - - - - - - - + <> + + + + + + + + + + + + + ); diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js index 49a7b484be..939be92f51 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js @@ -30,6 +30,7 @@ export const useEnrollmentFormFoundation = ({ locale, }: Props) => { const { data: enrollment, isLoading, error } = useIndexedDBQuery( + // $FlowFixMe ['enrollmentForm', program?.id], () => buildEnrollmentForm({ // $FlowFixMe - Flow does not understand that the values are not null here diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js index ae3d4c3e5a..abe732281d 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js @@ -26,6 +26,7 @@ export const useTrackedEntityTypeCollection = ({ locale, }: Props): ReturnValues => { const { data: trackedEntityAttributes } = useIndexedDBQuery( + // $FlowFixMe ['trackedEntityAttributes', trackedEntityType?.id], () => getTrackedEntityAttributes( trackedEntityType @@ -40,6 +41,7 @@ export const useTrackedEntityTypeCollection = ({ ); const { data: trackedEntityTypeCollection } = useIndexedDBQuery( + // $FlowFixMe ['trackedEntityTypeCollection', trackedEntityType?.id], () => buildTrackedEntityTypeCollection({ // $FlowFixMe diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index 99857025f9..6d905e0188 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -207,3 +207,4 @@ export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxSto }, ), ); + diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index d2b5b6f091..624ad10170 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useState, useCallback } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -13,8 +13,13 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; +import { TrackedEntityRelationshipsWrapper } from '../../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from '../../EnrollmentEditEvent/AddRelationshipRefWrapper'; const getStyles = () => ({ + container: { + position: 'relative', + }, columns: { display: 'flex', }, @@ -59,61 +64,88 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, + onLinkedRecordClick, onUpdateTeiAttributeValues, onEnrollmentError, -}: PlainProps) => ( - <> -
{i18n.t('Enrollment Dashboard')}
-
-
- - -
-
- - - - {!hideWidgets.indicator && ( - - )} - {!hideWidgets.feedback && ( - - )} - - {enrollmentId !== 'AUTO' && } +}: PlainProps) => { + const [mainContentVisible, setMainContentVisibility] = useState(true); + const [addRelationShipContainerElement, setAddRelationshipContainerElement] = + useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); + + return ( + <> + +
+
{i18n.t('Enrollment Dashboard')}
+
+
+ + +
+
+ {addRelationShipContainerElement && + {}} + onOpenAddRelationship={toggleVisibility} + onCloseAddRelationship={toggleVisibility} + teiId={teiId} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + + + {!hideWidgets.indicator && ( + + )} + {!hideWidgets.feedback && ( + + )} + + {enrollmentId !== 'AUTO' && } +
+
-
- -); + + ); +}; export const EnrollmentPageDefaultComponent: ComponentType = withStyles( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 976e03492a..637c9d2fc9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -22,12 +22,14 @@ import { import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useLinkedRecordClick } from '../../common/TEIRelationshipsWidget'; export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); + const { onLinkedRecordClick } = useLinkedRecordClick(); const program = useTrackerProgram(programId); const { @@ -74,6 +76,7 @@ export const EnrollmentPageDefault = () => { const onEventClick = (eventId: string) => { history.push(`/enrollmentEventEdit?${buildUrlQueryString({ orgUnitId, eventId })}`); }; + const onUpdateTeiAttributeValues = useCallback((updatedAttributeValues, teiDisplayName) => { dispatch(updateEnrollmentAttributeValues(updatedAttributeValues .map(({ attribute, value }) => ({ id: attribute, value })), @@ -86,6 +89,7 @@ export const EnrollmentPageDefault = () => { }; const onEnrollmentError = message => dispatch(showEnrollmentError({ message })); + if (error) { return error.errorComponent; } @@ -106,6 +110,7 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} + onLinkedRecordClick={onLinkedRecordClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} onEnrollmentError={onEnrollmentError} /> diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 3822b286a0..b5c1efcc5a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -1,11 +1,12 @@ // @flow -import type { Program } from 'capture-core/metaData'; +import type { TrackerProgram } from 'capture-core/metaData'; import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types'; import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { LinkedRecordClick } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type Props = {| - program: Program, + program: TrackerProgram, enrollmentId: string, teiId: string, events: ?Array, @@ -19,6 +20,7 @@ export type Props = {| onCreateNew: (stageId: string) => void, onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, + onLinkedRecordClick: LinkedRecordClick, onEnrollmentError: (message: string) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js new file mode 100644 index 0000000000..7235bb1a14 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js @@ -0,0 +1,21 @@ +// @flow +import React, { useEffect, useRef } from 'react'; + +type Props = { + setRelationshipRef: (HTMLDivElement) => void, +} + +export const AddRelationshipRefWrapper = ({ setRelationshipRef }: Props) => { + const renderRelationshipRef = useRef(undefined); + + // Extracting the logic to separate component because of the OrgUnitFetcher + useEffect(() => { + if (renderRelationshipRef.current) { + setRelationshipRef(renderRelationshipRef.current); + } + }, [setRelationshipRef]); + + return ( +
+ ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js new file mode 100644 index 0000000000..7400ed7ff3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js @@ -0,0 +1,3 @@ +// @flow + +export { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper.component'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index b5efd930b9..6876f31002 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useCallback, useState } from 'react'; import type { ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; @@ -18,12 +18,19 @@ import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; +import { + TrackedEntityRelationshipsWrapper, +} from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import { NoticeBox } from '../../NoticeBox'; const styles = ({ typography }) => ({ page: { margin: spacersNum.dp16, }, + addRelationshipContainer: { + margin: spacersNum.dp16, + }, columns: { display: 'flex', }, @@ -52,6 +59,7 @@ const EnrollmentEditEventPagePain = ({ programStage, teiId, enrollmentId, + trackedEntityTypeId, programId, enrollmentsAsOptions, trackedEntityName, @@ -62,6 +70,7 @@ const EnrollmentEditEventPagePain = ({ onAddNew, classes, onGoBack, + onLinkedRecordClick, orgUnitId, eventDate, scheduleDate, @@ -71,84 +80,109 @@ const EnrollmentEditEventPagePain = ({ onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, -}: PlainProps) => ( - - -
-
- {mode === dataEntryKeys.VIEW - ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) - : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +}: PlainProps) => { + const [mainContentVisible, setMainContentVisible] = useState(true); + const [addRelationShipContainerElement, setAddRelationShipContainerElement] = useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisible(current => !current), []); + + return ( + + +
+
-
-
- {pageStatus === pageStatuses.DEFAULT && programStage && ( - +
+ {mode === dataEntryKeys.VIEW + ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) + : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +
+
+
+ {pageStatus === pageStatuses.DEFAULT && programStage && ( + + )} + {pageStatus === pageStatuses.MISSING_DATA && ( + {i18n.t('The enrollment event data could not be found')} + )} + {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( + + {i18n.t('Choose a registering unit to start reporting')} + + )} +
+
+ + + + {!hideWidgets.feedback && ( + + )} + {!hideWidgets.indicator && ( + + )} + {addRelationShipContainerElement && + {}} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + - )} - {pageStatus === pageStatuses.MISSING_DATA && ( - {i18n.t('The enrollment event data could not be found')} - )} - {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( - - {i18n.t('Choose a registering unit to start reporting')} - - )} -
-
- - - - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - - +
+
- -
-
-); + + ); +}; export const EnrollmentEditEventPageComponent: ComponentType<$Diff> = withStyles(styles)(EnrollmentEditEventPagePain); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index c46d3dbdab..51c67f7fb9 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -21,6 +21,7 @@ import { useEvent } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; import { cleanUpDataEntry } from '../../DataEntry'; +import { useLinkedRecordClick } from '../common/TEIRelationshipsWidget'; import { pageKeys } from '../../App/withAppUrlSync'; import { withErrorMessageHandler } from '../../../HOC'; @@ -81,6 +82,8 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en const history = useHistory(); const dispatch = useDispatch(); + const { onLinkedRecordClick } = useLinkedRecordClick(); + useEffect(() => () => { dispatch(cleanUpDataEntry(dataEntryIds.ENROLLMENT_EVENT)); }, [dispatch]); @@ -102,6 +105,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); }; + const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -109,10 +113,9 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en dispatch(updateEnrollmentEvents(eventId, eventData)); history.push(`enrollment?${buildUrlQueryString({ enrollmentId })}`); }; - const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; const { teiDisplayName } = useTeiDisplayName(teiId, programId); // $FlowFixMe - const trackedEntityName = program?.trackedEntityType?.name; + const { name: trackedEntityName, id: trackedEntityTypeId } = program?.trackedEntityType; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); const event = enrollmentSite?.events?.find(item => item.event === eventId); const eventDate = getEventDate(event); @@ -121,6 +124,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); + const pageStatus = getPageStatus({ orgUnitId, enrollmentSite, @@ -140,6 +144,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en hideWidgets={hideWidgets} teiId={teiId} enrollmentId={enrollmentId} + trackedEntityTypeId={trackedEntityTypeId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} trackedEntityName={trackedEntityName} @@ -148,6 +153,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en onAddNew={onAddNew} orgUnitId={orgUnitId} eventDate={eventDate} + onLinkedRecordClick={onLinkedRecordClick} onEnrollmentError={onEnrollmentError} onEnrollmentSuccess={onEnrollmentSuccess} eventStatus={event?.status} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 59ff58c190..9b748c190f 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; +import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type PlainProps = {| programStage: ?ProgramStage, @@ -9,6 +10,7 @@ export type PlainProps = {| teiId: string, enrollmentId: string, programId: string, + trackedEntityTypeId: string, mode: string, orgUnitId: string, trackedEntityName: string, @@ -19,6 +21,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, + onLinkedRecordClick: LinkedRecordClick, onEnrollmentError: (message: string) => void, onEnrollmentSuccess: () => void, onCancelEditEvent: () => void, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index fb48b2a555..6ee03b475f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,5 +1,4 @@ // @flow - export type DataValue = { dataElement: string, value: string, @@ -49,6 +48,7 @@ export type AttributeValue = {| value: string, |}; + export type Output = {| error?: any, enrollment?: EnrollmentData, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js new file mode 100644 index 0000000000..7f60c1bb53 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -0,0 +1,46 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { useTEIRelationshipsWidgetMetadata } from '../useTEIRelationshipsWidgetMetadata'; +import { WidgetTrackedEntityRelationship } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; +import type { Props } from './TrackedEntityRelationshipsWrapper.types'; + +export const TrackedEntityRelationshipsWrapper = ({ + trackedEntityTypeId, + teiId, + programId, + addRelationshipRenderElement, + onOpenAddRelationship, + onCloseAddRelationship, + onLinkedRecordClick, +}: Props) => { + const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); + + if (isError) { + return ( +
+ {i18n.t('Could not retrieve metadata. Please try again later.')} +
+ ); + } + + if (!relationshipTypes || !addRelationshipRenderElement) { + return null; + } + + return ( + <> + + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js new file mode 100644 index 0000000000..d76c0550b9 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.js @@ -0,0 +1,13 @@ +// @flow +import type { LinkedRecordClick } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; + +export type Props = {| + trackedEntityTypeId: string, + teiId: string, + programId: string, + onAddRelationship: () => void, + addRelationshipRenderElement: HTMLDivElement, + onOpenAddRelationship: () => void, + onCloseAddRelationship: () => void, + onLinkedRecordClick: LinkedRecordClick, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js new file mode 100644 index 0000000000..6ca3eb46ab --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/index.js @@ -0,0 +1,2 @@ +// @flow +export { TrackedEntityRelationshipsWrapper } from './TrackedEntityRelationshipsWrapper.component'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js new file mode 100644 index 0000000000..0573777af3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/index.js @@ -0,0 +1,4 @@ +// @flow + +export { useTEIRelationshipsWidgetMetadata } from './useTEIRelationshipsWidgetMetadata'; +export { useLinkedRecordClick } from './useLinkedRecordClick'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js new file mode 100644 index 0000000000..ad5bb77fce --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useLinkedRecordClick.js @@ -0,0 +1,43 @@ +// @flow +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import type { NavigationArgs } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; +import { EventProgram, getProgramFromProgramIdThrowIfNotFound } from '../../../../metaData'; +import { useLocationQuery, buildUrlQueryString } from '../../../../utils/routing'; + +export const useLinkedRecordClick = () => { + const history = useHistory(); + const { orgUnitId } = useLocationQuery(); + + const onLinkedRecordClick = useCallback((navigationArgs: NavigationArgs) => { + let url; + if (navigationArgs.eventId) { + const { eventId, programId } = navigationArgs; + const recordProgram = getProgramFromProgramIdThrowIfNotFound(programId); + if (recordProgram instanceof EventProgram) { + url = `/viewEvent?viewEventId=${eventId}`; + } else { + url = `/enrollmentEventEdit?${buildUrlQueryString({ + orgUnitId, + eventId, + })}`; + } + } else if (navigationArgs.trackedEntityId) { + const { trackedEntityId, programId } = navigationArgs; + url = `/enrollment?${buildUrlQueryString({ + programId, + orgUnitId, + teiId: trackedEntityId, + enrollmentId: 'AUTO', + })}`; + } + + if (url) { + history.push(url); + } + }, [history, orgUnitId]); + + return { + onLinkedRecordClick, + }; +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js new file mode 100644 index 0000000000..4cb736b91f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/useTEIRelationshipsWidgetMetadata.js @@ -0,0 +1,47 @@ +// @flow +import { useIndexedDBQuery } from '../../../../utils/reactQueryHelpers'; +import { getUserStorageController, userStores } from '../../../../storageControllers'; +import type { RelationshipTypes } from '../../../WidgetsRelationship'; +import { + extractElementIdsFromRelationshipTypes, + formatRelationshipTypes, +} from '../../../WidgetsRelationship'; + +const getRelationshipTypes = async (): Promise => { + const userStorageController = getUserStorageController(); + const cachedRelationshipTypes = await userStorageController.getAll(userStores.RELATIONSHIP_TYPES); + + const { dataElementIds, attributeIds } = extractElementIdsFromRelationshipTypes(cachedRelationshipTypes); + + const attributes = (await userStorageController.getAll(userStores.TRACKED_ENTITY_ATTRIBUTES, { + predicate: ({ id }) => attributeIds[id], + project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), + })); + + const dataElements = (await userStorageController.getAll(userStores.DATA_ELEMENTS, { + predicate: ({ id }) => dataElementIds[id], + project: ({ id, valueType, displayName }) => ({ id, valueType, displayName }), + })); + + return formatRelationshipTypes({ + relationshipTypes: cachedRelationshipTypes, + attributes, + dataElements, + }); +}; + +export const useTEIRelationshipsWidgetMetadata = (): { + relationshipTypes: ?RelationshipTypes, + isError: boolean, +} => { + const { data: relationshipTypes, isError } = + useIndexedDBQuery( + ['cachedRelationshipTypes'], + getRelationshipTypes, + ); + + return { + relationshipTypes, + isError, + }; +}; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/Relationships/Relationships.component.js index 008653c5e9..eb2e6589fd 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.js @@ -111,7 +111,8 @@ class RelationshipsPlain extends React.Component { return numberOfChanges > 0; } - renderRelationships = () => this.props.relationships.map(r => this.renderRelationship(r)) + renderRelationships = () => this.props.relationships.map(relationship => relationship && + this.renderRelationship(relationship)) renderRelationship = (relationship: Relationship) => { const { classes, onRemoveRelationship } = this.props; diff --git a/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js b/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js index c097dc89c2..6d794477aa 100644 --- a/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js +++ b/src/core_modules/capture-core/components/Widget/WidgetCollapsible.component.js @@ -39,7 +39,6 @@ const styles = { borderColor: colors.grey400, borderWidth: 1, borderTopWidth: 0, - overflow: 'hidden', '&.open': { animation: 'slidein 200ms normal forwards ease-in-out', transformOrigin: '50% 0%', diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js new file mode 100644 index 0000000000..3bb1d3fa70 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.js @@ -0,0 +1,76 @@ +// @flow +import React, { type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { LinkButton } from '../../../../Buttons/LinkButton.component'; +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; +import type { PlainProps, Props } from './breadcrumbs.types'; + +const styles = { + container: { + padding: `${spacers.dp8} 0`, + }, +}; + +const Slash = withStyles({ slash: { padding: 5 } })(({ classes }) => /); + +const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate }) => { + const initialText = i18n.t('New TEI Relationship'); + return (currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.value ? + {initialText} : + {initialText}); +}; + +const RetrieverModeStep = ({ currentStep, onNavigate, linkedEntityMetadataName }) => { + if (currentStep.value < NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.value) { + return null; + } + + return ( + <> + + {currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.value ? + {linkedEntityMetadataName} : + {linkedEntityMetadataName}} + + ); +}; + +const FindExistingStep = ({ currentStep }) => { + if (currentStep.value < NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.value) { + return null; + } + + return ( + <> + + {i18n.t('Search')} + + ); +}; + +const BreadcrumbsPlain = ({ + currentStep, + onNavigate, + linkedEntityMetadataName, + classes, +}: PlainProps) => ( +
+ + onNavigate(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA)} + /> + onNavigate(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE)} + /> + +
+); + +export const Breadcrumbs: ComponentType = withStyles(styles)(BreadcrumbsPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js new file mode 100644 index 0000000000..5f59de427b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.js @@ -0,0 +1,13 @@ +// @flow +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; + +export type Props = {| + currentStep: $Values, + onNavigate: ($Values) => void, + linkedEntityMetadataName?: string, +|}; + +export type PlainProps = {| + ...Props, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js new file mode 100644 index 0000000000..fb78354934 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/index.js @@ -0,0 +1,2 @@ +// @flow +export { Breadcrumbs } from './Breadcrumbs.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js new file mode 100644 index 0000000000..286f7ea49e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js @@ -0,0 +1,28 @@ +// @flow +import React from 'react'; + +import { useApplicableTypesAndSides } from './useApplicableTypesAndSides'; +import { + LinkedEntityMetadataSelector, + type LinkedEntityMetadataSelectorType, +} from '../../../common/LinkedEntityMetadataSelector'; +import type { Props, Side, LinkedEntityMetadata } from './linkedEntityMetadataSelector.types'; + +export const LinkedEntityMetadataSelectorFromTrackedEntity = ({ + relationshipTypes, + trackedEntityTypeId, + programId, + onSelectLinkedEntityMetadata, +}: Props) => { + const applicableTypesInfo = useApplicableTypesAndSides(relationshipTypes, trackedEntityTypeId, [programId]); + + const LinkedEntityMetadataSelectorCommon: LinkedEntityMetadataSelectorType = + LinkedEntityMetadataSelector; + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js new file mode 100644 index 0000000000..f4da8e02ea --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/index.js @@ -0,0 +1,3 @@ +// @flow +export { LinkedEntityMetadataSelectorFromTrackedEntity } from './LinkedEntityMetadataSelector.component'; +export type { LinkedEntityMetadata, Side } from './linkedEntityMetadataSelector.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js new file mode 100644 index 0000000000..7d089dbfa9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -0,0 +1,37 @@ +// @flow +import type { RelationshipTypes } from '../../../common/Types'; +import type { TargetSides } from '../../../common/LinkedEntityMetadataSelector'; + + +export type Side = $ReadOnly<{| + trackedEntityTypeId: string, + trackedEntityName: string, + programId?: string, + name: string, + targetSide: TargetSides, +|}>; + +export type ApplicableTypeInfo = $ReadOnly<{| + id: string, + name: string, + sides: $ReadOnlyArray, +|}>; + +export type ApplicableTypesInfo = $ReadOnlyArray; + +export type LinkedEntityMetadata = $ReadOnly<{| + trackedEntityTypeId: string, + programId: string, + name: string, + targetSide: TargetSides, + relationshipId: string, + trackedEntityName: string, +|}>; + +export type Props = $ReadOnly<{| + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: LinkedEntityMetadata) => void, + +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js new file mode 100644 index 0000000000..60f6ab77f8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/LinkedEntityMetadataSelector/useApplicableTypesAndSides.js @@ -0,0 +1,179 @@ +// @flow +import { useMemo } from 'react'; +import { TARGET_SIDES } from '../common'; +import type { ApplicableTypesInfo } from './linkedEntityMetadataSelector.types'; +import type { RelationshipType } from '../../../common/Types'; +import { RELATIONSHIP_ENTITIES } from '../../../common/constants'; +import type { TargetSides } from '../../../common/LinkedEntityMetadataSelector'; + +const isApplicableProgram = (programId, sourceProgramIds) => + (!sourceProgramIds || !programId || sourceProgramIds.includes(programId)); + +const isApplicableUnidirectionalRelationshipType = ( + { trackedEntityType, program }, + sourceTrackedEntityTypeId, + sourceProgramIds, +) => { + const trackedEntityTypeId = trackedEntityType.id; + const programId = program?.id; + + return Boolean( + sourceTrackedEntityTypeId === trackedEntityTypeId && + isApplicableProgram(programId, sourceProgramIds), + ); +}; + +const computeTargetSidesDualMatchingTET = (() => { + const getProgramMatchInfo = (sourceProgramIds, programId) => { + if (!programId) { + return { noProgram: true, programMatch: false }; + } + + if (sourceProgramIds.includes(programId)) { + return { programMatch: true, noProgram: false }; + } + + return { programMatch: false, noProgram: false }; + }; + + return (sourceProgramIds, fromProgramId, toProgramId): Array => { + if (!sourceProgramIds) { + return [TARGET_SIDES.FROM, TARGET_SIDES.TO]; + } + const { programMatch: fromProgramMatch, noProgram: fromNoProgram } = + getProgramMatchInfo(sourceProgramIds, fromProgramId); + const { programMatch: toProgramMatch, noProgram: toNoProgram } = + getProgramMatchInfo(sourceProgramIds, toProgramId); + + if (fromProgramMatch) { + return [TARGET_SIDES.TO]; + } else if (toProgramMatch) { + return [TARGET_SIDES.FROM]; + } else if (fromNoProgram && toNoProgram) { + return [TARGET_SIDES.FROM, TARGET_SIDES.TO]; + } else if (fromNoProgram) { + return [TARGET_SIDES.TO]; + } else if (toNoProgram) { + return [TARGET_SIDES.FROM]; + } + + return []; + }; +})(); + +const getApplicableTargetSidesForBidirectionalRelationshipType = ({ + fromConstraint, + toConstraint, +}, sourceTrackedEntityTypeId, sourceProgramIds): Array => { + const { trackedEntityType: fromTrackedEntityType } = fromConstraint; + const { trackedEntityType: toTrackedEntityType } = toConstraint; + + if (fromTrackedEntityType.id === sourceTrackedEntityTypeId && + toTrackedEntityType.id === sourceTrackedEntityTypeId) { + return computeTargetSidesDualMatchingTET( + sourceProgramIds, + fromConstraint.program?.id, + toConstraint.program?.id, + ); + } + + if (fromTrackedEntityType.id === sourceTrackedEntityTypeId || + toTrackedEntityType.id === sourceTrackedEntityTypeId) { + const { programId, targetSide } = fromTrackedEntityType.id === sourceTrackedEntityTypeId ? + { programId: fromConstraint.program?.id, targetSide: TARGET_SIDES.TO } : + { programId: toConstraint.program?.id, targetSide: TARGET_SIDES.FROM }; + return isApplicableProgram(programId, sourceProgramIds) ? [targetSide] : []; + } + + return []; +}; + +export const useApplicableTypesAndSides = ( + relationshipTypes: $ReadOnlyArray, + sourceTrackedEntityTypeId: string, + sourceProgramIds: $ReadOnlyArray, +): ApplicableTypesInfo => useMemo(() => + relationshipTypes.map(({ + fromConstraint, + toConstraint, + bidirectional, + id, + displayName, + fromToName, + toFromName, + }) => { + if (fromConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE && + toConstraint.relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + if (!bidirectional) { + const applicable = isApplicableUnidirectionalRelationshipType( + fromConstraint, + sourceTrackedEntityTypeId, + sourceProgramIds, + ); + + if (!applicable) { + // $FlowFixMe filter + return null; + } + const { trackedEntityType, program } = toConstraint; + + return { + id, + name: displayName, + sides: [{ + programId: program?.id, + trackedEntityTypeId: trackedEntityType.id, + trackedEntityName: trackedEntityType.name.toLowerCase(), + targetSide: TARGET_SIDES.TO, + name: fromToName ?? displayName, + }], + }; + } + + const targetSides = getApplicableTargetSidesForBidirectionalRelationshipType( + { fromConstraint, toConstraint }, + sourceTrackedEntityTypeId, + sourceProgramIds, + ); + + if (!targetSides.length) { + // $FlowFixMe filter + return null; + } + + return { + id, + name: displayName, + sides: targetSides.map((targetSide) => { + const { + trackedEntityTypeId, + trackedEntityName, + programId, + name, + } = targetSide === TARGET_SIDES.TO ? { + trackedEntityTypeId: toConstraint.trackedEntityType.id, + trackedEntityName: toConstraint.trackedEntityType.name.toLowerCase(), + programId: toConstraint.program?.id, + name: fromToName, + } : { + trackedEntityTypeId: fromConstraint.trackedEntityType.id, + trackedEntityName: fromConstraint.trackedEntityType.name.toLowerCase(), + programId: fromConstraint.program?.id, + name: toFromName, + }; + + return { + trackedEntityTypeId, + trackedEntityName, + programId, + targetSide, + // $FlowFixMe + name, + }; + }), + }; + } + // $FlowFixMe filter + return null; + }).filter(applicableType => applicableType), +[relationshipTypes, sourceTrackedEntityTypeId, sourceProgramIds]); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js new file mode 100644 index 0000000000..6162a09e66 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -0,0 +1,146 @@ +// @flow +import React, { useCallback, useState, type ComponentType, useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../../Widget'; +import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { Breadcrumbs } from './Breadcrumbs'; +import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from './wizardSteps.const'; +import { + LinkedEntityMetadataSelectorFromTrackedEntity, + type LinkedEntityMetadata, +} from './LinkedEntityMetadataSelector'; +import { RetrieverModeSelector } from './RetrieverModeSelector'; +import type { ComponentProps, StyledComponentProps } from './NewTrackedEntityRelationship.types'; + +const styles = { + container: { + backgroundColor: '#FAFAFA', + maxWidth: 900, + }, + bar: { + color: '#494949', + padding: '8px', + display: 'inline-block', + fontSize: '14px', + borderRadius: '4px', + marginBottom: '10px', + backgroundColor: '#E9EEF4', + }, + linkText: { + backgroundColor: 'transparent', + fontSize: 'inherit', + color: 'inherit', + }, +}; + +const NewTrackedEntityRelationshipPlain = ({ + relationshipTypes, + trackedEntityTypeId, + programId, + onCancel, + classes, +}: StyledComponentProps) => { + const [currentStep, setCurrentStep] = + useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); + const [selectedLinkedEntityMetadata: LinkedEntityMetadata, setSelectedLinkedEntityMetadata] = useState(undefined); + + const handleNavigation = useCallback( + (destination: $Values) => { + setCurrentStep(destination); + }, []); + + const handleLinkedEntityMetadataSelection = useCallback((linkedEntityMetadata: LinkedEntityMetadata) => { + setSelectedLinkedEntityMetadata(linkedEntityMetadata); + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE); + }, []); + + const handleSearchRetrieverModeSelected = useCallback(() => + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY), []); + const handleNewRetrieverModeSelected = useCallback(() => + setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY), []); + + const stepContents = useMemo(() => { + if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.id) { + return ( + + ); + } + if ( + currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_RETRIEVER_MODE.id + && selectedLinkedEntityMetadata?.trackedEntityName + ) { + return ( + + ); + } + + // Steps below will be implemented by new PR + /* if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { + const { + trackedEntityTypeId: linkedEntityTrackedEntityTypeId, + programId: linkedEntityProgramId, + // $FlowFixMe business logic dictates that we will have the linkedEntityMetadata at this step + }: LinkedEntityMetadata = selectedLinkedEntityMetadata; + + return ( + + ); + } */ + + return ( +
+ {i18n.t('Missing implementation step')} +
+ ); + }, [ + currentStep.id, + handleLinkedEntityMetadataSelection, + handleNewRetrieverModeSelected, + handleSearchRetrieverModeSelected, + programId, + relationshipTypes, + selectedLinkedEntityMetadata?.trackedEntityName, + trackedEntityTypeId, + ]); + + return ( +
+
+ + {i18n.t('Go back without saving relationship')} + +
+ + )} + > + {stepContents} + +
+ ); +}; + +export const NewTrackedEntityRelationshipComponent: ComponentType = + withStyles(styles)(NewTrackedEntityRelationshipPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js new file mode 100644 index 0000000000..5b0ee59804 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.const.js @@ -0,0 +1,5 @@ + +export const creationModeStatuses = Object.freeze({ + SEARCH: 'search', + NEW: 'new', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js new file mode 100644 index 0000000000..e887e4d0f3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.js @@ -0,0 +1,63 @@ +// @flow +import React, { useCallback, useState, type ComponentType } from 'react'; +import { Button, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import i18n from '@dhis2/d2-i18n'; +import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; +import type { ContainerProps, StyledContainerProps } from './NewTrackedEntityRelationship.types'; + +const styles = { + container: { + padding: `0 ${spacers.dp16} ${spacers.dp24} ${spacers.dp16}`, + }, +}; + +export const NewTrackedEntityRelationshipPlain = ({ + renderElement, + programId, + relationshipTypes, + trackedEntityTypeId, + onCloseAddRelationship, + onOpenAddRelationship, + classes, +}: StyledContainerProps) => { + const [addWizardVisible, setAddWizardVisibility] = useState(false); + + const closeAddWizard = useCallback(() => { + setAddWizardVisibility(false); + onCloseAddRelationship && onCloseAddRelationship(); + }, [onCloseAddRelationship]); + + const openAddWizard = useCallback(() => { + setAddWizardVisibility(true); + onOpenAddRelationship && onOpenAddRelationship(); + }, [onOpenAddRelationship]); + + return ( +
+ + + { + addWizardVisible && ( + + ) + } +
+ ); +}; + +export const NewTrackedEntityRelationship: ComponentType = + withStyles(styles)(NewTrackedEntityRelationshipPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js new file mode 100644 index 0000000000..eec749787f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.portal.js @@ -0,0 +1,10 @@ +// @flow +import React from 'react'; +import ReactDOM from 'react-dom'; +import { NewTrackedEntityRelationshipComponent } from './NewTrackedEntityRelationship.component'; +import type { PortalProps } from './NewTrackedEntityRelationship.types'; + +export const NewTrackedEntityRelationshipPortal = ({ renderElement, ...passOnProps }: PortalProps) => + ReactDOM.createPortal(( + + ), renderElement); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..7c23d8e7b1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -0,0 +1,44 @@ +// @flow +import type { RelationshipTypes } from '../../common/Types'; + +export type ContainerProps = $ReadOnly<{| + renderElement: HTMLElement, + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onCloseAddRelationship?: () => void, + onOpenAddRelationship?: () => void, +|}>; + +export type StyledContainerProps = $ReadOnly<{| + ...ContainerProps, + ...CssClasses, +|}>; + +export type PortalProps = $ReadOnly<{| + renderElement: HTMLElement, + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onSave: () => void, + onCancel: () => void, +|}>; + +export type StyledPortalProps = $ReadOnly<{| + ...PortalProps, + ...CssClasses, +|}>; + + +export type ComponentProps = $ReadOnly<{| + relationshipTypes: RelationshipTypes, + trackedEntityTypeId: string, + programId: string, + onSave: () => void, + onCancel: () => void, +|}>; + +export type StyledComponentProps = $ReadOnly<{| + ...ComponentProps, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js new file mode 100644 index 0000000000..a775fed023 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/RetrieverModeSelector.component.js @@ -0,0 +1,45 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { Button, IconSearch16, IconAdd16, spacersNum, spacers } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import type { PlainProps, Props } from './retrieverModeSelector.types'; + +const styles = { + container: { + padding: spacersNum.dp16, + paddingTop: 0, + }, + retrieverSelector: { + display: 'flex', + gap: spacers.dp8, + }, +}; + +const RetrieverModeSelectorPlain = ({ + classes, + onSearchSelected, + onNewSelected, + trackedEntityName, +}: PlainProps) => ( +
+
+ + +
+
+); + +export const RetrieverModeSelector: ComponentType = withStyles(styles)(RetrieverModeSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js new file mode 100644 index 0000000000..f0d9af5395 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/index.js @@ -0,0 +1,2 @@ +// @flow +export { RetrieverModeSelector } from './RetrieverModeSelector.component'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js new file mode 100644 index 0000000000..8bf6c93de9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/RetrieverModeSelector/retrieverModeSelector.types.js @@ -0,0 +1,12 @@ +// @flow + +export type Props = $ReadOnly<{| + onSearchSelected: () => void, + onNewSelected: () => void, + trackedEntityName: string, +|}>; + +export type PlainProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js new file mode 100644 index 0000000000..e5ad1f726a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/index.js @@ -0,0 +1,2 @@ +// @flow +export { TARGET_SIDES } from './targetSides'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js new file mode 100644 index 0000000000..3880543dc2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/common/targetSides.js @@ -0,0 +1,9 @@ +// @flow + +export const TARGET_SIDES: {| + FROM: 'FROM', + TO: 'TO', +|} = Object.freeze({ + FROM: 'FROM', + TO: 'TO', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js new file mode 100644 index 0000000000..18ce242d75 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/index.js @@ -0,0 +1,2 @@ +// @flow +export { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship.container'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js new file mode 100644 index 0000000000..55c01e50de --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/wizardSteps.const.js @@ -0,0 +1,6 @@ +export const NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS = Object.freeze({ + SELECT_LINKED_ENTITY_METADATA: Object.freeze({ value: 1, id: 'selectLinkedEntityMetadata' }), + SELECT_RETRIEVER_MODE: Object.freeze({ value: 2, id: 'selectRetrieverMode' }), + FIND_EXISTING_LINKED_ENTITY: Object.freeze({ value: 3, id: 'findExistingLinkedEntity' }), + NEW_LINKED_ENTITY: Object.freeze({ value: 4, id: 'newLinkedEntity' }), +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js new file mode 100644 index 0000000000..d1e787d187 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.js @@ -0,0 +1,51 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; +import { RelationshipsWidget } from '../common/RelationshipsWidget'; +import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; +import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; + +export const WidgetTrackedEntityRelationship = ({ + relationshipTypes: cachedRelationshipTypes, + teiId, + trackedEntityTypeId, + programId, + addRelationshipRenderElement, + onLinkedRecordClick, + onOpenAddRelationship, + onCloseAddRelationship, +}: WidgetTrackedEntityRelationshipProps) => { + const { data: relationships, isError } = useRelationships(teiId, RelationshipSearchEntities.TRACKED_ENTITY); + + if (isError) { + return ( +
+ {i18n.t('Something went wrong while loading relationships. Please try again later.')} +
+ ); + } + + return ( + + { + relationshipTypes => ( + + ) + } + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js new file mode 100644 index 0000000000..fae3a3061a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -0,0 +1,14 @@ +// @flow +import type { RelationshipTypes } from '../common/Types'; +import type { LinkedRecordClick } from '../common/RelationshipsWidget'; + +export type WidgetTrackedEntityRelationshipProps = {| + trackedEntityTypeId: string, + teiId: string, + programId: string, + addRelationshipRenderElement: HTMLElement, + onLinkedRecordClick: LinkedRecordClick, + onOpenAddRelationship?: () => void, + onCloseAddRelationship?: () => void, + relationshipTypes?: RelationshipTypes, +|}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js new file mode 100644 index 0000000000..0176a26897 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/index.js @@ -0,0 +1,10 @@ +// @flow +export { WidgetTrackedEntityRelationship } from './WidgetTrackedEntityRelationship.component'; +export type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; +export type { RelationshipTypes } from '../common/Types'; +export type { + LinkedRecordClick, + NavigationArgs, + NavigationArgsEvent, + NavigationArgsTrackedEntity, +} from '../common/RelationshipsWidget'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js new file mode 100644 index 0000000000..98c34a8cb6 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/LinkedEntityMetadataSelector.component.js @@ -0,0 +1,73 @@ +// @flow +import React from 'react'; +import { Button, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import type { PlainProps, LinkedEntityMetadata, Side } from './linkedEntityMetadataSelector.types'; + +const styles = { + container: { + padding: spacers.dp16, + paddingTop: 0, + }, + typeSelector: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp8, + marginBottom: spacers.dp16, + }, + selectorButton: { + display: 'flex', + flexDirection: 'column', + gap: spacers.dp4, + marginBottom: spacers.dp8, + }, + title: { + fontWeight: 500, + marginBottom: spacers.dp4, + }, + buttonContainer: { + display: 'flex', + gap: spacers.dp8, + }, +}; + +export const LinkedEntityMetadataSelectorPlain = ({ + applicableTypesInfo, + onSelectLinkedEntityMetadata, + classes }: PlainProps) => ( +
+
+ {applicableTypesInfo.map(({ id, name, sides }) => ( +
+
+ {name} +
+
+
+ {sides.map((side: TSide) => ( + + ))} +
+
+
+ ))} +
+
+ ); + + +export const LinkedEntityMetadataSelector = + withStyles(styles)(LinkedEntityMetadataSelectorPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js new file mode 100644 index 0000000000..3468a522b9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/index.js @@ -0,0 +1,3 @@ +// @flow +export { LinkedEntityMetadataSelector } from './LinkedEntityMetadataSelector.component'; +export type { TargetSides, LinkedEntityMetadataSelectorType } from './linkedEntityMetadataSelector.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js new file mode 100644 index 0000000000..d23c3d34ec --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/LinkedEntityMetadataSelector/linkedEntityMetadataSelector.types.js @@ -0,0 +1,36 @@ +// @flow +import type { ComponentType } from 'react'; + +export type TargetSides = 'FROM' | 'TO'; + +export type Side = $ReadOnly<{ + name: string, + targetSide: TargetSides, +}>; + +type ApplicableTypeInfo = $ReadOnly<{ + id: string, + name: string, + sides: $ReadOnlyArray, +}>; + +type ApplicableTypesInfo = $ReadOnlyArray>; + +export type LinkedEntityMetadata = $ReadOnly<{ + ...Side, + relationshipId: string, +}>; + +export type Props = $ReadOnly<{| + applicableTypesInfo: ApplicableTypesInfo, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: TLinkedEntityMetadata) => void, +|}>; + +export type PlainProps = $ReadOnly<{| + applicableTypesInfo: ApplicableTypesInfo, + onSelectLinkedEntityMetadata: (linkedEntityMetadata: TLinkedEntityMetadata) => void, + ...CssClasses, +|}>; + +export type LinkedEntityMetadataSelectorType = + ComponentType>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js new file mode 100644 index 0000000000..072177e943 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.js @@ -0,0 +1,45 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { spacersNum, spacers, colors } from '@dhis2/ui'; +import { LinkedEntityTable } from './LinkedEntityTable.component'; +import type { Props, StyledProps } from './linkedEntitiesViewer.types'; + +const styles = { + container: { + padding: `0 ${spacers.dp16} ${spacers.dp12} ${spacers.dp16}`, + }, + title: { + fontWeight: 500, + fontSize: 16, + color: colors.grey800, + paddingBottom: spacersNum.dp16, + }, + wrapper: { + paddingBottom: spacersNum.dp16, + }, +}; + + +const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, classes }: StyledProps) => ( +
+ {groupedLinkedEntities?.map((linkedEntityGroup) => { + const { id, name, linkedEntities, columns, context } = linkedEntityGroup; + return ( +
+
{name}
+ +
+ ); + })} +
); + +export const LinkedEntitiesViewer: ComponentType = withStyles(styles)(LinkedEntitiesViewerPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js new file mode 100644 index 0000000000..a286b51ff4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.js @@ -0,0 +1,71 @@ +// @flow +import React, { useState, useMemo, type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { + DataTable, + Button, + spacers, +} from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { LinkedEntityTableHeader } from './LinkedEntityTableHeader.component'; +import { LinkedEntityTableBody } from './LinkedEntityTableBody.component'; +import type { Props, StyledProps } from './linkedEntityTable.types'; + +const DEFAULT_VISIBLE_ROWS_COUNT = 5; + +const styles = { + button: { + marginTop: `${spacers.dp8}`, + }, + dataTableWrapper: { + overflowY: 'auto', + whiteSpace: 'nowrap', + }, +}; + +const LinkedEntityTablePlain = ({ linkedEntities, columns, onLinkedRecordClick, context, classes }: StyledProps) => { + const [visibleRowsCount, setVisibleRowsCount] = useState(DEFAULT_VISIBLE_ROWS_COUNT); + + const visibleLinkedEntities = useMemo(() => + linkedEntities.slice(0, visibleRowsCount), + [linkedEntities, visibleRowsCount]); + + const showMoreButtonVisible = linkedEntities.length > visibleRowsCount; + + return ( +
+ + + + + {showMoreButtonVisible && ( + + )} +
+ ); +}; + +export const LinkedEntityTable: ComponentType = withStyles(styles)(LinkedEntityTablePlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js new file mode 100644 index 0000000000..a8bbed4ce4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.js @@ -0,0 +1,56 @@ +// @flow +import React, { type ComponentType } from 'react'; +import { withStyles } from '@material-ui/core'; +import { + DataTableBody, + DataTableRow, + DataTableCell, +} from '@dhis2/ui'; +import { convertServerToClient, convertClientToList } from '../../../../converters'; +import type { Props, StyledProps } from './linkedEntityTableBody.types'; + +const styles = { + row: { + '&:hover': { + cursor: 'pointer', + }, + }, +}; + +const LinkedEntityTableBodyPlain = ({ + linkedEntities, + columns, + onLinkedRecordClick, + context, + classes, +}: StyledProps) => ( + + { + linkedEntities + .map(({ id: entityId, values, baseValues, navigation }) => ( + + { + // $FlowFixMe flow doesn't like destructering + columns.map(({ id, type, convertValue }) => { + const value = type ? + convertClientToList(convertServerToClient(values[id], type), type) : + convertValue(baseValues?.[id] ?? context.display[id]); + + return ( + onLinkedRecordClick({ ...context.navigation, ...navigation })} + > + {value} + + ); + })} + + )) + } + +); + +export const LinkedEntityTableBody: ComponentType = withStyles(styles)(LinkedEntityTableBodyPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js new file mode 100644 index 0000000000..e7681a6867 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.js @@ -0,0 +1,25 @@ +// @flow +import React from 'react'; +import { + DataTableHead, + DataTableRow, + DataTableColumnHeader, +} from '@dhis2/ui'; +import type { Props } from './linkedEntityTableHeader.types'; + +export const LinkedEntityTableHeader = ({ columns }: Props) => ( + + + { + columns + .map(({ id, displayName }) => ( + + {displayName} + + )) + } + + +); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js new file mode 100644 index 0000000000..9cb0d8596f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.js @@ -0,0 +1,71 @@ +// @flow +import React, { type ComponentType, useState } from 'react'; +import { Chip, IconLink24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import { Widget } from '../../../Widget'; +import { useGroupedLinkedEntities } from './useGroupedLinkedEntities'; +import { useRelationshipTypes } from './useRelationshipTypes'; +import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; +import type { Props, StyledProps } from './relationshipsWidget.types'; + +const styles = { + header: { + display: 'flex', + alignItems: 'center', + }, + icon: { + paddingRight: spacers.dp8, + }, +}; + +const RelationshipsWidgetPlain = ({ + title, + relationships, + cachedRelationshipTypes, + sourceId, + onLinkedRecordClick, + children, + classes, +}: StyledProps) => { + const [open, setOpenStatus] = useState(true); + const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); + const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships); + + return ( +
+ + + + + {title} + {relationships && ( + + {relationships.length} + + )} +
+ )} + onOpen={() => setOpenStatus(true)} + onClose={() => setOpenStatus(false)} + open={open} + > + { + groupedLinkedEntities && ( + + ) + }{ + relationshipTypes && children(relationshipTypes) + } + +
+ ); +}; + +export const RelationshipsWidget: ComponentType = withStyles(styles)(RelationshipsWidgetPlain); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js new file mode 100644 index 0000000000..0390efec9f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/index.js @@ -0,0 +1,3 @@ +// @flow +export { RelationshipsWidget } from './RelationshipsWidget.component'; +export type { NavigationArgsTrackedEntity, NavigationArgsEvent, NavigationArgs, LinkedRecordClick } from './types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js new file mode 100644 index 0000000000..93160917f9 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.js @@ -0,0 +1,12 @@ +// @flow +import type { GroupedLinkedEntities, LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + groupedLinkedEntities: GroupedLinkedEntities, + onLinkedRecordClick: LinkedRecordClick, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js new file mode 100644 index 0000000000..8d3fb8b72a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.js @@ -0,0 +1,14 @@ +// @flow +import type { LinkedEntityData, TableColumn, LinkedRecordClick, Context } from './types'; + +export type Props = $ReadOnly<{| + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + onLinkedRecordClick: LinkedRecordClick, + context: Context, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js new file mode 100644 index 0000000000..1efc3202a5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.js @@ -0,0 +1,14 @@ +// @flow +import type { LinkedEntityData, TableColumn, Context, LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + onLinkedRecordClick: LinkedRecordClick, + context: Context, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js new file mode 100644 index 0000000000..17def5ae63 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.js @@ -0,0 +1,6 @@ +// @flow +import type { TableColumn } from './types'; + +export type Props = $ReadOnly<{| + columns: $ReadOnlyArray, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js new file mode 100644 index 0000000000..0fb87e55fc --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.js @@ -0,0 +1,18 @@ +// @flow +import type { Node } from 'React'; +import type { InputRelationshipData, RelationshipTypes } from '../Types'; +import type { LinkedRecordClick } from './types'; + +export type Props = $ReadOnly<{| + title: string, + relationships?: Array, + cachedRelationshipTypes?: RelationshipTypes, + sourceId: string, + onLinkedRecordClick: LinkedRecordClick, + children: (relationshipTypes: RelationshipTypes) => Node, +|}>; + +export type StyledProps = $ReadOnly<{| + ...Props, + ...CssClasses, +|}>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js new file mode 100644 index 0000000000..91f747776b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/GroupedLinkedEntities.types.js @@ -0,0 +1,50 @@ +// @flow +import { dataElementTypes } from '../../../../../metaData'; + +export type MetadataBasedColumn = $ReadOnly<{| + id: string, + displayName: string, + type: $Keys, +|}>; + +export type ManualColumn = $ReadOnly<{| + id: string, + displayName: string, + convertValue: (value: any) => any, +|}>; + +export type TableColumn = MetadataBasedColumn | ManualColumn; + +export type NavigationContextTrackedEntity = $ReadOnly<{| + programId?: string, +|}>; + +export type NavigationContextEvent = $ReadOnly<{||}>; + +export type LinkedEntityData = $ReadOnly<{| + id: string, + values: $ReadOnly<{| [id: string]: ?string |}>, + baseValues?: { + relatonshipCreatedAt?: string, + }, + navigation?: { + programId?: string, + eventId?: string, + trackedEntityId?: string, + }, +|}>; + +export type Context = $ReadOnly<{| + navigation: NavigationContextTrackedEntity | NavigationContextEvent, + display: Object, +|}>; + +export type LinkedEntityGroup = $ReadOnly<{| + id: string, + name: string, + linkedEntities: $ReadOnlyArray, + columns: $ReadOnlyArray, + context: Context, +|}>; + +export type GroupedLinkedEntities = $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js new file mode 100644 index 0000000000..268dc966e4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/index.js @@ -0,0 +1,3 @@ +// @flow +export type * from './GroupedLinkedEntities.types'; +export type * from './navigation.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js new file mode 100644 index 0000000000..5fb9807b63 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/types/navigation.types.js @@ -0,0 +1,15 @@ +// @flow + +export type NavigationArgsTrackedEntity = $ReadOnly<{| + programId?: string, + trackedEntityId: string, +|}>; + +export type NavigationArgsEvent = $ReadOnly<{| + eventId: string, + programId: string, +|}>; + +export type NavigationArgs = NavigationArgsTrackedEntity | NavigationArgsEvent; + +export type LinkedRecordClick = (navigationArgs: NavigationArgs) => void; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js new file mode 100644 index 0000000000..afd147fc10 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.js @@ -0,0 +1,222 @@ +// @flow +import { useMemo } from 'react'; +import log from 'loglevel'; +import moment from 'moment'; +import i18n from '@dhis2/d2-i18n'; +import { errorCreator } from 'capture-core-utils'; +import { dataElementTypes } from '../../../../metaData'; +import { RELATIONSHIP_ENTITIES } from '../../common/constants'; +import { convertServerToClient, convertClientToList } from '../../../../converters'; +import type { GroupedLinkedEntities, LinkedEntityData } from './types'; +import type { + InputRelationshipData, + RelationshipTypes, +} from '../Types'; + + +const getFallbackFieldsByRelationshipEntity = { + [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ + id: 'trackedEntityTypeName', + displayName: i18n.t('Type'), + convertValue: trackedEntityTypeName => trackedEntityTypeName, + }, { + id: 'relatonshipCreatedAt', + displayName: i18n.t('Created date'), + convertValue: createdDate => convertClientToList( + convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, + ), + }], + [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: () => [{ + id: 'programStageName', + displayName: i18n.t('Program stage name'), + convertValue: programStageName => programStageName, + }, + { + id: 'relatonshipCreatedAt', + displayName: i18n.t('Created date'), + convertValue: createdDate => convertClientToList( + convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, + ), + }], +}; + +const getColumns = ({ relationshipEntity, trackerDataView }) => { + let fields; + if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + fields = trackerDataView.attributes; + } else if (relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { + fields = trackerDataView.dataElements; + } + + if (!fields?.length) { + fields = getFallbackFieldsByRelationshipEntity[relationshipEntity](); + } + + return fields; +}; + +// $FlowFixMe destructering +const getContext = ({ relationshipEntity, program, programStage, trackedEntityType }) => { + if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { + return { + navigation: { + programId: program?.id, + }, + display: { + trackedEntityTypeName: trackedEntityType.name, + }, + }; + } + + if (relationshipEntity === RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE) { + return { + navigation: {}, + display: { + programStageName: programStage.name, + }, + }; + } + + return { + navigation: {}, + display: {}, + }; +}; + +const getEventData = ({ dataValues, event, program: programId }, relatonshipCreatedAt): LinkedEntityData => { + const values = dataValues.reduce((acc, dataValue) => { + acc[dataValue.dataElement] = dataValue.value; + return acc; + }, {}); + + return { + id: event, + values, + baseValues: { + relatonshipCreatedAt, + }, + navigation: { + eventId: event, + programId, + }, + }; +}; + +const getTrackedEntityData = ({ attributes, trackedEntity }, relatonshipCreatedAt): LinkedEntityData => { + const values = attributes.reduce((acc, attribute) => { + acc[attribute.attribute] = attribute.value; + return acc; + }, {}); + + return { + id: trackedEntity, + values, + baseValues: { + relatonshipCreatedAt, + }, + navigation: { + trackedEntityId: trackedEntity, + }, + }; +}; + +const getLinkedEntityData = (apiLinkedEntity, relatonshipCreatedAt) => { + if (apiLinkedEntity.trackedEntity) { + return getTrackedEntityData(apiLinkedEntity.trackedEntity, relatonshipCreatedAt); + } + + if (apiLinkedEntity.event) { + return getEventData(apiLinkedEntity.event, relatonshipCreatedAt); + } + + if (apiLinkedEntity.enrollment) { + log.warn(errorCreator('Linked entities of type enrollment are not currently supported')({ apiLinkedEntity })); + return null; + } + + log.error(errorCreator('Unsupported linked entity type')({ apiLinkedEntity })); + return null; +}; + +const determineLinkedEntity = (fromEntity, toEntity, sourceId) => { + if (fromEntity.trackedEntity?.trackedEntity === sourceId || fromEntity.event?.event === sourceId) { + return toEntity; + } + + if (toEntity.trackedEntity?.trackedEntity === sourceId || toEntity.event?.event === sourceId) { + return fromEntity; + } + + log.error(errorCreator('Could not determine linked entity')({ fromEntity, toEntity, sourceId })); + return null; +}; + +export const useGroupedLinkedEntities = ( + sourceId: string, + relationshipTypes: ?RelationshipTypes, + relationships?: Array, +): GroupedLinkedEntities => { + const groupedLinkedEntities = useMemo(() => { + if (!relationships?.length || !relationshipTypes?.length) { + return []; + } + + + return relationships + .sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt))) + .reduce((accGroupedLinkedEntities, relationship) => { + const { + relationshipType: relationshipTypeId, + from: fromEntity, + to: toEntity, + createdAt: relationshipCreatedAt, + } = relationship; + + const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); + if (!relationshipType) { + log.error( + errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), + ); + return accGroupedLinkedEntities; + } + + const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); + if (!apiLinkedEntity) { + return accGroupedLinkedEntities; + } + + const linkedEntityData = getLinkedEntityData(apiLinkedEntity, relationshipCreatedAt); + if (!linkedEntityData) { + return accGroupedLinkedEntities; + } + + const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; + const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); + if (group) { + group.linkedEntities = [ + ...group.linkedEntities, + linkedEntityData, + ]; + } else { + const { constraint, name } = apiLinkedEntity === fromEntity ? + { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : + { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; + + const columns = getColumns(constraint); + const context = getContext(constraint); + + accGroupedLinkedEntities.push({ + id: groupId, + name: name || relationshipType.displayName, + linkedEntities: [linkedEntityData], + columns, + context, + }); + } + + return accGroupedLinkedEntities; + }, []); + }, [relationships, relationshipTypes, sourceId]); + + return groupedLinkedEntities; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js new file mode 100644 index 0000000000..10c6aee562 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useRelationshipTypes.js @@ -0,0 +1,92 @@ +// @flow +import { useMemo } from 'react'; +import { useApiMetadataQuery } from '../../../../utils/reactQueryHelpers'; +import type { ApiRelationshipTypes, RelationshipTypes } from '../Types'; +import { extractElementIdsFromRelationshipTypes, formatRelationshipTypes } from '../utils'; + +type Element = {| + id: string, + displayName: string, + valueType: string, +|}; + +const relationshipTypesQuery = { + resource: 'relationshipTypes', + params: { + fields: 'id,displayName,fromToName,toFromName,fromConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],toConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]]', + }, +}; + +export const useRelationshipTypes = (cachedRelationshipTypes?: RelationshipTypes) => { + const { data: apiRelationshipTypes, isError, isLoading } = useApiMetadataQuery( + ['relationshipTypes'], + relationshipTypesQuery, + { + enabled: !cachedRelationshipTypes?.length, + select: ({ relationshipTypes }: any) => relationshipTypes, + }, + ); + + const { attributeQuery, dataElementQuery } = useMemo(() => { + if (!apiRelationshipTypes) return {}; + const { dataElementIds, attributeIds } = extractElementIdsFromRelationshipTypes(apiRelationshipTypes); + + const filteredAttributeQuery = { + resource: 'trackedEntityAttributes', + params: { + fields: 'id,displayName,valueType', + filter: `id:in:[${Object.keys(attributeIds).join(',')}]`, + paging: false, + }, + }; + + const filteredDataElementQuery = { + resource: 'dataElements', + params: { + fields: 'id,displayName,valueType', + filter: `id:in:[${Object.keys(dataElementIds).join(',')}]`, + paging: false, + }, + }; + + return { + attributeQuery: filteredAttributeQuery, + dataElementQuery: filteredDataElementQuery, + }; + }, [apiRelationshipTypes]); + + const { data: apiAttributes } = useApiMetadataQuery>( + ['attributes'], + attributeQuery, + { + enabled: !cachedRelationshipTypes?.length && !!attributeQuery, + select: ({ trackedEntityAttributes }: any) => trackedEntityAttributes, + }, + ); + + const { data: apiDataElements } = useApiMetadataQuery>( + ['dataElements'], + dataElementQuery, + { + enabled: !cachedRelationshipTypes?.length && !!dataElementQuery, + select: ({ dataElements }: any) => dataElements, + }, + ); + + + const relationshipTypes = useMemo(() => { + if (!apiRelationshipTypes || !apiAttributes || !apiDataElements) return null; + + return formatRelationshipTypes({ + relationshipTypes: apiRelationshipTypes, + attributes: apiAttributes, + dataElements: apiDataElements, + }); + }, [apiAttributes, apiDataElements, apiRelationshipTypes]); + + return { + data: relationshipTypes ?? cachedRelationshipTypes, + isError, + isLoading, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js new file mode 100644 index 0000000000..989ee5ee7d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipData.types.js @@ -0,0 +1,31 @@ +// @flow + +export type ApiLinkedEntity = {| + event?: { + event: string, + orgUnitName: string, + program: string, + orgUnit: string, + status: string, + dataValues: Array<{ dataElement: string, value: string }>, + }, + trackedEntity?: { + trackedEntity: string, + program: string, + orgUnit: string, + orgUnitName: string, + attributes: Array<{ attribute: string, value: string }>, + }, + enrollment?: {}, +|} + +export type InputRelationshipData = { + id: string, + relationshipName: string, + relationshipType: string, + relationship: string, + createdAt: string, + bidirectional: string, + from: ApiLinkedEntity, + to: ApiLinkedEntity, +} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js new file mode 100644 index 0000000000..ea5b0b0a53 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/RelationshipTypes.types.js @@ -0,0 +1,92 @@ +// @flow +import { dataElementTypes } from '../../../../metaData'; + +export type ElementValue = {| + attribute?: string, + dataElement?: string, + displayName: string, + valueType: string, + value: any, +|}; + +export type ApiTrackerDataView = $ReadOnly<{| + attributes: $ReadOnlyArray, + dataElements: $ReadOnlyArray, +|}>; + +type ApiCommonConstraintTypes = $ReadOnly<{| + trackerDataView: ApiTrackerDataView, +|}>; + +export type ApiTrackedEntityConstraint = $ReadOnly<{| + ...ApiCommonConstraintTypes, + relationshipEntity: 'TRACKED_ENTITY_INSTANCE', + trackedEntityType: { id: string, name: string }, + program?: { id: string, name: string }, +|}>; + +export type ApiProgramStageInstanceConstraint = $ReadOnly<{| + ...ApiCommonConstraintTypes, + relationshipEntity: 'PROGRAM_STAGE_INSTANCE', + program: { id: string, name: string }, + programStage: { id: string, name: string }, +|}>; + +export type ApiRelationshipConstraint = ApiTrackedEntityConstraint | ApiProgramStageInstanceConstraint; + +export type ApiRelationshipType = $ReadOnly<{| + id: string, + displayName: string, + bidirectional: boolean, + access: Object, + toFromName: string, + fromToName: string, + fromConstraint: ApiRelationshipConstraint, + toConstraint: ApiRelationshipConstraint, +|}>; + +export type ApiRelationshipTypes = $ReadOnlyArray; + +export type TrackerDataViewEntity = $ReadOnly<{| + id: string, + type: $Keys, + displayName: string, +|}>; + +export type TrackerDataView = $ReadOnly<{| + attributes: $ReadOnlyArray, + dataElements: $ReadOnlyArray, +|}>; + +export type CommonConstraintTypes = $ReadOnly<{| + trackerDataView: TrackerDataView, +|}>; + +export type TrackedEntityConstraint = $ReadOnly<{| + ...CommonConstraintTypes, + relationshipEntity: 'TRACKED_ENTITY_INSTANCE', + trackedEntityType: { id: string, name: string }, + program?: { id: string, name: string }, +|}>; + +export type ProgramStageInstanceConstraint = $ReadOnly<{| + ...CommonConstraintTypes, + relationshipEntity: 'PROGRAM_STAGE_INSTANCE', + program: { id: string, name: string }, + programStage: { id: string, name: string }, +|}>; + +export type RelationshipConstraint = TrackedEntityConstraint | ProgramStageInstanceConstraint; + +export type RelationshipType = $ReadOnly<{| + id: string, + displayName: string, + bidirectional: boolean, + access: Object, + toFromName: string, + fromToName: string, + fromConstraint: RelationshipConstraint, + toConstraint: RelationshipConstraint, +|}>; + +export type RelationshipTypes = $ReadOnlyArray; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js new file mode 100644 index 0000000000..1182761bee --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/Types/index.js @@ -0,0 +1,4 @@ +// @flow + +export type * from './RelationshipTypes.types'; +export type * from './RelationshipData.types'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js new file mode 100644 index 0000000000..e3e3225010 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/constants.js @@ -0,0 +1,10 @@ +// @flow +export const RELATIONSHIP_ENTITIES: {| + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +|} = Object.freeze({ + PROGRAM_STAGE_INSTANCE: 'PROGRAM_STAGE_INSTANCE', + TRACKED_ENTITY_INSTANCE: 'TRACKED_ENTITY_INSTANCE', + PROGRAM_INSTANCE: 'PROGRAM_INSTANCE', +}); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js new file mode 100644 index 0000000000..2c93b6f33c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/index.js @@ -0,0 +1,2 @@ +// @flow +export { useRelationships, RelationshipSearchEntities } from './useRelationships'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js new file mode 100644 index 0000000000..9c5b323232 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/useRelationships/useRelationships.js @@ -0,0 +1,31 @@ +// @flow +import { useMemo } from 'react'; +import { useApiDataQuery } from '../../../../utils/reactQueryHelpers'; +import type { InputRelationshipData } from '../Types'; + +export const RelationshipSearchEntities = Object.freeze({ + TRACKED_ENTITY: 'trackedEntity', + ENROLLMENT: 'enrollment', + EVENT: 'event', +}); + +type ReturnData = Array; + +export const useRelationships = (entityId: string, searchMode: string) => { + const query = useMemo(() => ({ + resource: 'tracker/relationships', + params: { + [searchMode]: entityId, + fields: 'relationshipType,createdAt,from[trackedEntity[trackedEntity,attributes,program,orgUnit,trackedEntityType],event[event,dataValues,program,orgUnit,orgUnitName,status,createdAt]],to[trackedEntity[trackedEntity,attributes,program,orgUnit,trackedEntityType],event[event,dataValues,program,orgUnit,orgUnitName,status,createdAt]]', + }, + }), [entityId, searchMode]); + + return useApiDataQuery( + ['relationships', entityId], + query, + { + enabled: !!entityId, + select: ({ instances }: any) => instances, + }, + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js new file mode 100644 index 0000000000..ac437c364b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/extractElementIdsFromRelationshipTypes.js @@ -0,0 +1,32 @@ +// @flow + +import type { ApiRelationshipTypes } from '../Types'; + +export const extractElementIdsFromRelationshipTypes = (relationshipTypes: ApiRelationshipTypes) => { + const attributeIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = fromConstraint.trackerDataView?.attributes || []; + const toAttributes = toConstraint.trackerDataView?.attributes || []; + return [...fromAttributes, ...toAttributes]; + }).reduce((acc, attributeId) => { + acc[attributeId] = true; + return acc; + }, {}); + + const dataElementIds = relationshipTypes + .flatMap((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromDataElements = fromConstraint.trackerDataView?.dataElements || []; + const toDataElements = toConstraint.trackerDataView?.dataElements || []; + return [...fromDataElements, ...toDataElements]; + }).reduce((acc, dataElementId) => { + acc[dataElementId] = true; + return acc; + }, {}); + + return { + attributeIds, + dataElementIds, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js new file mode 100644 index 0000000000..ce575581db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/formatRelationshipTypes.js @@ -0,0 +1,82 @@ +// @flow +import type { ApiRelationshipTypes, RelationshipTypes } from '../Types'; +import { replaceElementIdsWithElement } from './replaceElementIdsWithElement'; + +const elementTypes = { + ATTRIBUTE: 'ATTRIBUTE', + DATA_ELEMENT: 'DATA_ELEMENT', +}; + +type Element = {| + id: string, + valueType: string, + displayName: string, +|} + +type Props = {| + relationshipTypes: ApiRelationshipTypes, + attributes: Array, + dataElements: Array, +|} + +export const formatRelationshipTypes = ({ + relationshipTypes = [], + attributes, + dataElements, +}: Props): RelationshipTypes => { + const attributesById = attributes.reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; + return acc; + }, {}); + + const dataElementsById = dataElements.reduce((acc, { id, valueType, displayName }) => { + acc[id] = { valueType, displayName }; + return acc; + }, {}); + + return relationshipTypes + .map((relationshipType) => { + const { fromConstraint, toConstraint } = relationshipType; + const fromAttributes = replaceElementIdsWithElement( + fromConstraint.trackerDataView?.attributes, + attributesById, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const toAttributes = replaceElementIdsWithElement( + toConstraint.trackerDataView?.attributes, + attributesById, + { relationshipType, elementType: elementTypes.ATTRIBUTE }, + ); + const fromDataElements = replaceElementIdsWithElement( + fromConstraint.trackerDataView?.dataElements, + dataElementsById, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + const toDataElements = replaceElementIdsWithElement( + toConstraint.trackerDataView?.dataElements, + dataElementsById, + { relationshipType, elementType: elementTypes.DATA_ELEMENT }, + ); + + return { + ...relationshipType, + // $FlowFixMe Might be fixable with generics + fromConstraint: { + ...fromConstraint, + trackerDataView: { + attributes: fromAttributes, + dataElements: fromDataElements, + }, + }, + // $FlowFixMe Might be fixable with generics + toConstraint: { + ...toConstraint, + trackerDataView: { + attributes: toAttributes, + dataElements: toDataElements, + }, + }, + }; + }) + .filter(relationshipType => relationshipType); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js new file mode 100644 index 0000000000..bbc3c5da67 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/index.js @@ -0,0 +1,3 @@ +// @flow +export { extractElementIdsFromRelationshipTypes } from './extractElementIdsFromRelationshipTypes'; +export { formatRelationshipTypes } from './formatRelationshipTypes'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js new file mode 100644 index 0000000000..e9e8d64332 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/utils/replaceElementIdsWithElement.js @@ -0,0 +1,69 @@ +// @flow +import log from 'loglevel'; +import { errorCreator } from '../../../../../capture-core-utils'; +import type { ApiRelationshipType } from '../Types'; +import { + dataElementTypes, +} from '../../../../metaData'; + +const elementTypes = { + ATTRIBUTE: 'attribute', + DATA_ELEMENT: 'dataElement', +}; + +type Elements = {| + [key: string]: { + id: string, + displayName: string, + valueType: $Keys, + }, +|} + +type Context = {| + relationshipType: ApiRelationshipType, + elementType: $Values +|} + +type ElementArray = $ReadOnlyArray<{| + id: string, + type: $Keys, + displayName: string, +|}>; + +export const replaceElementIdsWithElement = ( + elementIds: ?$ReadOnlyArray, + elements: Elements, + { relationshipType, elementType }: Context): ElementArray => + // $FlowFixMe + (elementIds || []) + .map((elementId) => { + const element = elements[elementId]; + + if (!element) { + log.error( + errorCreator(`${elementType} from relationshipType not found in cache`)( + { elementId, relationshipType }, + ), + ); + + // $FlowFixMe filter is unhandled + return null; + } + + if (!element.valueType) { + log.error( + errorCreator(`cached ${elementType} is missing value type`)( + { elementId, element }), + ); + + // $FlowFixMe filter is unhandled + return null; + } + + return { + id: elementId, + type: element.valueType, + displayName: element.displayName, + }; + }) + .filter(element => element); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/index.js b/src/core_modules/capture-core/components/WidgetsRelationship/index.js new file mode 100644 index 0000000000..f28c1cfc5f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsRelationship/index.js @@ -0,0 +1,3 @@ +// @flow +export type { ApiRelationshipTypes, RelationshipTypes, RelationshipType, TrackerDataView } from './common/Types'; +export { formatRelationshipTypes, extractElementIdsFromRelationshipTypes } from './common/utils'; diff --git a/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js b/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js index 27227868ab..ce6fc5e114 100644 --- a/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js +++ b/src/core_modules/capture-core/metaData/RelationshipType/RelationshipType.js @@ -13,6 +13,8 @@ type RelationshipConstraint = { export class RelationshipType { _id: string; _name: string; + _fromToName: string; + _toFromName: string; _access: Access; _from: RelationshipConstraint; _to: RelationshipConstraint; @@ -60,4 +62,20 @@ export class RelationshipType { get to(): RelationshipConstraint { return this._to; } + + get fromToName(): string { + return this._fromToName; + } + + set fromToName(value: string) { + this._fromToName = value; + } + + get toFromName(): string { + return this._toFromName; + } + + set toFromName(value: string) { + this._toFromName = value; + } } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js index 571fee90bb..be55829ea8 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/quickStoreOperations/storeRelationshipTypes.js @@ -6,7 +6,7 @@ export const storeRelationshipTypes = () => { const query = { resource: 'relationshipTypes', params: { - fields: 'id,displayName,fromConstraint[*],toConstraint[*],access[*]', + fields: 'id,displayName,bidirectional,fromToName,toFromName,fromConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],toConstraint[relationshipEntity,trackerDataView,trackedEntityType[id,name],program[id,name],programStage[id,name]],access[*]', }, }; diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js index bfff16fabb..f9aef8290d 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js @@ -82,4 +82,5 @@ export const enrollmentPageDesc = createReducerDescription({ ), ], }), + }, 'enrollmentPage', initialReducerValue); diff --git a/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js index 6610eb8e40..e39feb2874 100644 --- a/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/organisationUnits.reducerDescription.js @@ -42,5 +42,5 @@ export const organisationUnitRootsDesc = createReducerDescription({ roots: action.payload.roots, }, }), -}, 'organisationUnitRoots'); +}, 'organisationUnitRoots2222'); diff --git a/src/core_modules/capture-core/relationships/relationshipRequests.js b/src/core_modules/capture-core/relationships/relationshipRequests.js index d2b00f11c0..a9b21ec73d 100644 --- a/src/core_modules/capture-core/relationships/relationshipRequests.js +++ b/src/core_modules/capture-core/relationships/relationshipRequests.js @@ -28,7 +28,7 @@ export function getRelationshipsForEvent( const stage = program instanceof EventProgram ? program.stage : program.getStage(programStageId); const relationshipTypes = stage?.relationshipTypes || []; return getRelationships( - { event: eventId, fields: ['from,to,relationshipType,relationship'] }, + { event: eventId, fields: ['from,to,relationshipType,relationship,createdAt'] }, relationshipTypes, querySingleResource, ); diff --git a/src/core_modules/capture-core/storageControllers/cache.types.js b/src/core_modules/capture-core/storageControllers/cache.types.js index cfcaa75c92..18e2c3fbb0 100644 --- a/src/core_modules/capture-core/storageControllers/cache.types.js +++ b/src/core_modules/capture-core/storageControllers/cache.types.js @@ -250,3 +250,5 @@ export type CachedRelationshipType = { fromConstraint: CachedRelationshipConstraint, toConstraint: CachedRelationshipConstraint, } + +export type CachedRelationshipTypes = Array; diff --git a/src/core_modules/capture-core/storageControllers/index.js b/src/core_modules/capture-core/storageControllers/index.js index ac6ad66b6a..eeb9e0f53e 100644 --- a/src/core_modules/capture-core/storageControllers/index.js +++ b/src/core_modules/capture-core/storageControllers/index.js @@ -3,4 +3,5 @@ export { initAsync as initControllersAsync } from './storageControllers'; export { closeAsync as closeControllersAsync } from './storageControllers'; export { getMainController as getMainStorageController } from './storageControllers'; export { getUserController as getUserStorageController } from './storageControllers'; +export { userStores } from './stores'; export type * from './cache.types'; diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js index 92c4c08077..c8a73f7838 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js @@ -1,7 +1,6 @@ // @flow import type { UseQueryOptions } from 'react-query'; -import { userStores } from '../../storageControllers/stores'; -import { getUserStorageController } from '../../storageControllers'; +import { userStores, getUserStorageController } from '../../storageControllers'; import { useIndexedDBQuery } from '../reactQueryHelpers'; @@ -10,6 +9,7 @@ export const useProgramFromIndexedDB = (programId: ?string, QueryOptions?: UseQu const { enabled = true } = QueryOptions ?? {}; const { data, isLoading, isError } = useIndexedDBQuery( + // $FlowFixMe - only gets called when programId is defined because of enabled ['programs', programId], () => storageController.get(userStores.PROGRAMS, programId), { diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useTrackedEntityTypeFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useTrackedEntityTypeFromIndexedDB.js index f4a73308eb..effdcd50db 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useTrackedEntityTypeFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useTrackedEntityTypeFromIndexedDB.js @@ -1,7 +1,6 @@ // @flow import type { UseQueryOptions } from 'react-query'; -import { userStores } from '../../storageControllers/stores'; -import { getUserStorageController } from '../../storageControllers'; +import { userStores, getUserStorageController } from '../../storageControllers'; import { useIndexedDBQuery } from '../reactQueryHelpers'; @@ -9,6 +8,7 @@ export const useTrackedEntityTypeFromIndexedDB = (trackedEntityTypeId: ?string, const storageController = getUserStorageController(); const { data, isLoading, isError } = useIndexedDBQuery( + // $FlowFixMe - only gets called when programId is defined because of enabled ['trackedEntityType', trackedEntityTypeId], () => storageController.get(userStores.TRACKED_ENTITY_TYPES, trackedEntityTypeId), { diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js index 9d5121c8e5..0f1bca3a8f 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/index.js @@ -1,2 +1,9 @@ // @flow -export { useIndexedDBQuery } from './query'; +export { + useIndexedDBQuery, + useCustomMetadataQuery, + useApiMetadataQuery, + useApiDataQuery, +} from './query'; + +export { ReactQueryAppNamespace } from './reactQueryHelpers.const'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js index 951b7145ea..e21fb5bff8 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/index.js @@ -1,2 +1,3 @@ // @flow -export { useIndexedDBQuery } from './useMetadataQuery'; +export { useIndexedDBQuery, useCustomMetadataQuery, useApiMetadataQuery } from './useMetadataQuery'; +export { useApiDataQuery } from './useApiDataQuery'; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js new file mode 100644 index 0000000000..8505d4aaff --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js @@ -0,0 +1,27 @@ +// @flow +import { useQuery } from 'react-query'; +import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; +import type { QueryFunction, UseQueryOptions } from 'react-query'; +import type { Result } from './useMetadataQuery.types'; +import { ReactQueryAppNamespace } from '../reactQueryHelpers.const'; + +export const useApiDataQuery = ( + queryKey: Array, + queryObject: ResourceQuery, + queryOptions: UseQueryOptions, +): Result => { + const dataEngine = useDataEngine(); + const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) + .then(response => response.theQuerykey); + return useQuery( + [ReactQueryAppNamespace, ...queryKey], + queryFn, + { + ...queryOptions, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + staleTime: 2 * 60 * 1000, + cacheTime: 5 * 60 * 1000, + }); +}; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index 5079ec80ee..ee31061b8c 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -1,9 +1,11 @@ // @flow import { useQuery } from 'react-query'; import log from 'loglevel'; -import type { QueryFunction, QueryKey, UseQueryOptions } from 'react-query'; -import type { Result } from './useMetadataQuery.types'; +import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; +import type { QueryFunction, UseQueryOptions } from 'react-query'; import { IndexedDBError } from '../../../../capture-core-utils/storage/IndexedDBError/IndexedDBError'; +import type { Result } from './useMetadataQuery.types'; +import { ReactQueryAppNamespace } from '../reactQueryHelpers.const'; const throwErrorForIndexedDB = (error) => { if (error instanceof IndexedDBError) { @@ -17,16 +19,27 @@ const throwErrorForIndexedDB = (error) => { }; const useAsyncMetadata = ( - queryKey: QueryKey, + queryKey: Array, queryFn: QueryFunction, queryOptions: UseQueryOptions, -): Result => useQuery(queryKey, queryFn, { +): Result => useQuery([ReactQueryAppNamespace, ...queryKey], queryFn, { staleTime: Infinity, ...queryOptions, }); +export const useCustomMetadataQuery = ( + queryKey: Array, + queryFn: QueryFunction, + queryOptions?: UseQueryOptions, +): Result => + useAsyncMetadata(queryKey, queryFn, { + cacheTime: 5, + ...queryOptions, + }); + + export const useIndexedDBQuery = ( - queryKey: QueryKey, + queryKey: Array, queryFn: QueryFunction, queryOptions?: UseQueryOptions, ): Result => @@ -38,3 +51,18 @@ export const useIndexedDBQuery = ( throwErrorForIndexedDB(error); }, }); + +export const useApiMetadataQuery = ( + queryKey: Array, + queryObject: ResourceQuery, + queryOptions?: UseQueryOptions, +): Result => { + const dataEngine = useDataEngine(); + const queryFn: QueryFunction = () => dataEngine.query({ theQuerykey: queryObject }) + .then(response => response.theQuerykey); + return useAsyncMetadata(queryKey, queryFn, { + cacheTime: Infinity, + staleTime: Infinity, + ...queryOptions, + }); +}; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js index 3aac3d847f..04779fc25b 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.types.js @@ -1,4 +1,11 @@ // @flow -import type { UseQueryResult } from 'react-query'; +import type { UseQueryOptions, UseQueryResult } from 'react-query'; +import type { ResourceQuery } from '@dhis2/app-runtime'; + +export type ApiMetadataProps = {| + queryKey: string | Array, + QueryObject: ResourceQuery, + queryOptions?: UseQueryOptions, +|} export type Result = UseQueryResult; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js b/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js new file mode 100644 index 0000000000..2659b304ba --- /dev/null +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js @@ -0,0 +1,4 @@ + +// @flow + +export const ReactQueryAppNamespace = 'capture'; diff --git a/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js b/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js index f30dfcd8af..aa5005d8d1 100644 --- a/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js +++ b/src/core_modules/capture-core/utils/routing/buildUrlQueryString.js @@ -2,10 +2,11 @@ const LOCALE_EN = 'en'; -export const buildUrlQueryString = (queryArgs: { [id: string]: string }) => +export const buildUrlQueryString = (queryArgs: $ReadOnly<{ [id: string]: ?string }>) => Object .entries(queryArgs) - .sort((a, b) => a[0].localeCompare(b[0], LOCALE_EN)) + .filter(([, value]) => value != null) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB, LOCALE_EN)) .reduce((searchParams, [key, value]) => { // $FlowFixMe value && searchParams.append(key, value); From 30c77c4c6d74386cea1cd60170169279e86821f6 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Fri, 28 Jul 2023 12:44:56 +0200 Subject: [PATCH 81/95] chore: flow --- i18n/en.pot | 22 ++----------------- .../EnrollmentEditEventPage.component.js | 4 ---- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 211bedf701..f6ff4d53d8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-07-28T09:35:30.528Z\n" -"PO-Revision-Date: 2023-07-28T09:35:30.528Z\n" +"POT-Creation-Date: 2023-07-28T10:44:57.813Z\n" +"PO-Revision-Date: 2023-07-28T10:44:57.813Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -947,14 +947,6 @@ msgstr "Search {{uniqueAttrName}}" msgid "Search by attributes" msgstr "Search by attributes" -msgid "Fill in at least {{count}} attribute to search" -msgid_plural "Fill in at least {{count}} attribute to search" -msgstr[0] "Fill in at least {{count}} attribute to search" -msgstr[1] "Fill in at least {{count}} attributes to search" - -msgid "Could not retrieve metadata. Please try again later." -msgstr "Could not retrieve metadata. Please try again later." - msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." @@ -1259,22 +1251,12 @@ msgstr "Scheduled automatically for {{suggestedScheduleDate}}" msgid "The scheduled date matches the suggested date, but can be changed if needed." msgstr "The scheduled date matches the suggested date, but can be changed if needed." -msgid "The scheduled date is {{count}} days {{position}} the suggested date." -msgid_plural "The scheduled date is {{count}} days {{position}} the suggested date." -msgstr[0] "The scheduled date is {{count}} day {{position}} the suggested date." -msgstr[1] "The scheduled date is {{count}} days {{position}} the suggested date." - msgid "after" msgstr "after" msgid "before" msgstr "before" -msgid "There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgid_plural "There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgstr[0] "There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgstr[1] "There are {{count}} scheduled events in {{orgUnitName}} on this day." - msgid "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 7c4749af80..6876f31002 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -23,10 +23,6 @@ import { } from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import { NoticeBox } from '../../NoticeBox'; -import { - TrackedEntityRelationshipsWrapper, -} from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; -import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; const styles = ({ typography }) => ({ page: { From 14a2530424488c52f46dda49af99ff2e8da12796 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Fri, 4 Aug 2023 09:46:23 +0200 Subject: [PATCH 82/95] chore: temp --- .../EnrollmentRegistrationEntry.container.js | 3 +- .../EnrollmentRegistrationEntry.types.js | 1 + .../TeiRegistrationEntry.container.js | 12 +- .../TeiRegistrationEntry.types.js | 2 + .../EnrollmentPageDefault.component.js | 1 + .../EnrollmentEditEventPage.component.js | 1 + .../RegistrationDataEntry.component.js | 3 + ...ataEntryTrackedEntityInstance.component.js | 63 +++---- .../RegisterTei/RegisterTei.container.js | 3 +- .../DataEntryEnrollment.component.js | 42 +++++ .../DataEntryEnrollment.container.js | 23 +++ .../Enrollment/dataEntryEnrollment.types.js | 20 +++ .../Enrollment/enrollment.module.css | 11 ++ .../Enrollment/enrollment.selectors.js | 23 +++ .../RegisterTei/DataEntry/Enrollment/index.js | 2 + .../DataEntry/RegisterTeiDataEntry.actions.js | 14 ++ .../RegisterTeiDataEntry.component.js | 32 ++++ .../RegisterTeiDataEntry.container.js | 20 +++ .../DataEntryTrackedEntityInstance.js | 53 ++++++ .../dataEntryTrackedEntityInstance.types.js | 18 ++ .../DataEntry/TrackedEntityInstance/index.js | 2 + .../TrackedEntityInstance/tei.selectors.js | 18 ++ .../trackedEntityInstance.module.css | 11 ++ .../RegisterTei/RegisterTei.component.js | 133 +++++++++++++++ .../RegisterTei/RegisterTei.container.js | 35 ++++ .../RegisterTei/RegisterTei.types.js | 17 ++ .../ComposedProgramSelector.component.js | 157 ++++++++++++++++++ .../ProgramSelector.component.js | 44 +++++ .../ProgramSelector.container.js | 22 +++ .../ProgramSelector/index.js | 2 + .../ComposedRegUnitSelector.component.js | 55 ++++++ .../RegUnitSelector.component.js | 67 ++++++++ .../RegUnitSelector.container.js | 22 +++ .../RegUnitSelector/index.js | 3 + .../RegistrationSection.component.js | 43 +++++ .../SectionContents.component.js | 17 ++ .../RegisterTei/RegistrationSection/index.js | 3 + .../registrationSection.actions.js | 22 +++ .../exposedHelpers/getRelationshipNewTei.js | 141 ++++++++++++++++ .../RegisterTei/index.js | 12 ++ .../RegisterTei/registerTei.actions.js | 15 ++ .../RegisterTei/registerTei.const.js | 3 + .../RegisterTei/registerTei.selectors.js | 23 +++ ...ackedEntityRelationshipsWrapper.actions.js | 6 +- ...kedEntityRelationshipsWrapper.component.js | 30 +++- ...TrackedEntityRelationshipsWrapper.epics.js | 69 +++++++- ...TrackedEntityRelationshipsWrapper.types.js | 1 + .../index.js | 5 +- .../Breadcrumbs/Breadcrumbs.component.js | 16 +- .../NewTrackedEntityRelationship.component.js | 37 ++++- .../NewTrackedEntityRelationship.container.js | 4 + .../NewTrackedEntityRelationship.types.js | 9 + ...dgetTrackedEntityRelationship.component.js | 4 + .../WidgetTrackedEntityRelationship.types.js | 7 + src/epics/trackerCapture.epics.js | 2 + 55 files changed, 1351 insertions(+), 53 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ProgramSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ProgramSelector.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/ComposedRegUnitSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.container.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegistrationSection.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/SectionContents.component.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/registrationSection.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/exposedHelpers/getRelationshipNewTei.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/index.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/registerTei.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/registerTei.const.js create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/registerTei.selectors.js diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js index 6d624541be..703f82fe53 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js @@ -15,10 +15,11 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ id, saveButtonText, trackedEntityInstanceAttributes, + cachedOrgUnitId, ...passOnProps }) => { const orgUnitId = useCurrentOrgUnitInfo().id; - const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); + const { orgUnit, error } = useRulesEngineOrgUnit(cachedOrgUnitId ?? orgUnitId); const { teiId, ready, skipDuplicateCheck } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); const { formId, diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js index 463fbecc06..e844a094e8 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js @@ -21,6 +21,7 @@ export type OwnProps = $ReadOnly<{| skipDuplicateCheck?: ?boolean, trackedEntityInstanceAttributes?: Array, saveButtonText: (trackedEntityName: string) => string, + cachedOrgUnitId?: string, |}>; type ContainerProps = {| diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js index e35cfbb4ce..ffce8d6311 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import React, { useEffect, useMemo } from 'react'; import type { ComponentType } from 'react'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { useCurrentOrgUnitInfo } from '../../../hooks/useCurrentOrgUnitInfo'; import { Enrollment, scopeTypes } from '../../../metaData'; import { startNewTeiDataEntryInitialisation } from './TeiRegistrationEntry.actions'; import type { OwnProps } from './TeiRegistrationEntry.types'; @@ -12,10 +11,9 @@ import { useFormValuesFromSearchTerms } from './hooks/useFormValuesFromSearchTer import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm'; -const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { +const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) => { const dispatch = useDispatch(); const { scopeType, trackedEntityName } = useScopeInfo(selectedScopeId); - const { id: selectedOrgUnitId } = useCurrentOrgUnitInfo(); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const formValues = useFormValuesFromSearchTerms(); const registrationFormReady = !!formId; @@ -24,18 +22,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { if (registrationFormReady && scopeType === scopeTypes.TRACKED_ENTITY_TYPE) { dispatch( startNewTeiDataEntryInitialisation( - { selectedOrgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, + { selectedOrgUnitId: orgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, )); } }, [ scopeType, dataEntryId, selectedScopeId, - selectedOrgUnitId, registrationFormReady, formFoundation, formValues, dispatch, + orgUnitId, ]); return { @@ -44,8 +42,8 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { }; -export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, ...rest }) => { - const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id); +export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, orgUnitId, ...rest }) => { + const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id, orgUnitId); const ready = useSelector(({ dataEntries }) => (!!dataEntries[id])); const dataEntry = useSelector(({ dataEntries }) => (dataEntries[id])); const { diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js index d73d49c473..dcc02c58b4 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js @@ -7,6 +7,7 @@ import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMess export type OwnProps = $ReadOnly<{| id: string, + orgUnitId: string, selectedScopeId: string, saveButtonText: string, fieldOptions?: Object, @@ -19,6 +20,7 @@ export type OwnProps = $ReadOnly<{| |}>; type ContainerProps = {| + orgUnitId: string, teiRegistrationMetadata: RegistrationFormMetadata, ready: boolean, trackedEntityName: string, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 624ad10170..d45e854f3a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -102,6 +102,7 @@ export const EnrollmentPageDefaultPlain = ({ {}} onOpenAddRelationship={toggleVisibility} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 6876f31002..a53473f6fa 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -159,6 +159,7 @@ const EnrollmentEditEventPagePain = ({ trackedEntityTypeId={trackedEntityTypeId} teiId={teiId} programId={programId} + orgUnitId={orgUnitId} addRelationshipRenderElement={addRelationShipContainerElement} onOpenAddRelationship={toggleVisibility} onCloseAddRelationship={toggleVisibility} diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js index 7a8cf76cc6..7ba72fc2d1 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js @@ -18,6 +18,7 @@ import { EnrollmentRegistrationEntryWrapper } from '../EnrollmentRegistrationEnt import { useMetadataForRegistrationForm, } from '../../../DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { useCurrentOrgUnitInfo } from '../../../../hooks/useCurrentOrgUnitInfo'; const getStyles = ({ typography }) => ({ container: { @@ -100,6 +101,7 @@ const RegistrationDataEntryPlain = ({ const { scopeType, programName, trackedEntityName } = useScopeInfo(selectedScopeId); const titleText = useScopeTitleText(selectedScopeId); const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); + const { id: reduxOrgUnitId } = useCurrentOrgUnitInfo(); const handleRegistrationScopeSelection = (id) => { setScopeId(id); @@ -231,6 +233,7 @@ const RegistrationDataEntryPlain = ({ { - const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; - const { trackedEntityType } = teiRegistrationMetadata || {}; - const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + ({ + theme, + onSave, + teiRegistrationMetadata = {}, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { id: orgUnitId } = useCurrentOrgUnitInfo(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); - return ( - // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching - - ); - }; + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; export const RelationshipTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js index 991425a334..cf9de6695b 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js @@ -34,6 +34,7 @@ export const RegisterTei = ({ onLink, onSave, onGetUnsavedAttributeValues }: Own trackedEntityName={trackedEntityName} newRelationshipProgramId={newRelationshipProgramId} error={error} - />); + /> + ); }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js new file mode 100644 index 0000000000..1ef1030539 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -0,0 +1,42 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core/styles'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import enrollmentClasses from './enrollment.module.css'; +import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryEnrollment.types'; + +const NewEnrollmentRelationshipPlain = + ({ + theme, + onSave, + programId, + orgUnitId, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + + return ( + i18n.t('Save new {{trackedEntityTypeName}} and link', { + trackedEntityTypeName, + interpolation: { escapeValue: false }, + })} + onSave={onSave} + duplicatesReviewPageSize={duplicatesReviewPageSize} + renderDuplicatesDialogActions={renderDuplicatesDialogActions} + renderDuplicatesCardActions={renderDuplicatesCardActions} + ExistingUniqueValueDialogActions={ExistingUniqueValueDialogActions} + /> + ); + }; + +export const NewEnrollmentRelationship = withTheme()(NewEnrollmentRelationshipPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js new file mode 100644 index 0000000000..507e09e61b --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js @@ -0,0 +1,23 @@ +// @flow +import { connect } from 'react-redux'; +import { makeEnrollmentMetadataSelector } from './enrollment.selectors'; +import { NewEnrollmentRelationship } from './DataEntryEnrollment.component'; + +const makeMapStateToProps = () => { + const enrollmentMetadataSelector = makeEnrollmentMetadataSelector(); + + const mapStateToProps = (state: ReduxState) => { + const enrollmentMetadata = enrollmentMetadataSelector(state); + + return { + enrollmentMetadata, + programId: state.newRelationshipRegisterTei.programId, + orgUnitId: state.newRelationshipRegisterTei.orgUnit.id, + }; + }; + // $FlowFixMe[not-an-object] automated comment + return mapStateToProps; +}; + +// $FlowFixMe +export const DataEntryEnrollment = connect(makeMapStateToProps, () => ({}))(NewEnrollmentRelationship); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js new file mode 100644 index 0000000000..de3f6cee6f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js @@ -0,0 +1,20 @@ +// @flow +import type { Node } from 'react'; +import type { Enrollment } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + SaveForEnrollmentAndTeiRegistration, + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type Props = {| + theme: Theme, + programId: string, + orgUnitId: string, + enrollmentMetadata?: Enrollment, + onSave: SaveForEnrollmentAndTeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css new file mode 100644 index 0000000000..953ddeae59 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js new file mode 100644 index 0000000000..fd9331f6a6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js @@ -0,0 +1,23 @@ +// @flow +import { createSelector } from 'reselect'; +import type { TrackerProgram } from '../../../../../../../metaData'; +import { getProgramFromProgramIdThrowIfNotFound } from '../../../../../../../metaData'; + +const programIdSelector = state => state.newRelationshipRegisterTei.programId; + +// $FlowFixMe +export const makeEnrollmentMetadataSelector = () => createSelector( + programIdSelector, + (programId: string) => { + let program: TrackerProgram; + try { + // $FlowFixMe[incompatible-type] automated comment + program = getProgramFromProgramIdThrowIfNotFound(programId); + } catch (error) { + return null; + } + + // $FlowFixMe + return program.enrollment; + }, +); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js new file mode 100644 index 0000000000..0ecadd4fab --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryEnrollment } from './DataEntryEnrollment.container'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js new file mode 100644 index 0000000000..8cb9a9b5fa --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js @@ -0,0 +1,14 @@ +// @flow +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const actionTypes = { + DATA_ENTRY_OPEN: 'NewRelationshipRegisterTeiDataEntryOpen', + DATA_ENTRY_OPEN_CANCELLED: 'NewRelationshopRegisterTeiDataEntryOpenCancelled', + DATA_ENTRY_OPEN_FAILED: 'NewRelationshopRegisterTeiDataEntryOpenFailed', +}; + +export const openDataEntry = () => + actionCreator(actionTypes.DATA_ENTRY_OPEN)(); + +export const openDataEntryFailed = (errorMessage: string) => + actionCreator(actionTypes.DATA_ENTRY_OPEN_FAILED)({ errorMessage }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js new file mode 100644 index 0000000000..55f8c295aa --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -0,0 +1,32 @@ +// @flow +import * as React from 'react'; +import { DataEntryEnrollment } from './Enrollment'; +import { DataEntryTrackedEntityInstance } from './TrackedEntityInstance'; + +type Props = { + showDataEntry: boolean, + programId: string, +}; + +export class RegisterTeiDataEntryComponent extends React.Component { + render() { + const { showDataEntry, programId, ...passOnProps } = this.props; + + if (!showDataEntry) { + return null; + } + if (programId) { + return ( + + ); + } + + return ( + + ); + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js new file mode 100644 index 0000000000..e5e7d50e76 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js @@ -0,0 +1,20 @@ +// @flow +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { RegisterTeiDataEntryComponent } from './RegisterTeiDataEntry.component'; +import { withErrorMessageHandler } from '../../../../../../HOC/withErrorMessageHandler'; + +const mapStateToProps = (state: ReduxState) => ({ + showDataEntry: state.newRelationshipRegisterTei.orgUnit, + error: state.newRelationshipRegisterTei.dataEntryError, + programId: state.newRelationshipRegisterTei.programId, +}); + +const mapDispatchToProps = () => ({}); + +export const RegisterTeiDataEntry = + compose( + // $FlowFixMe[missing-annot] automated comment + connect(mapStateToProps, mapDispatchToProps), + withErrorMessageHandler(), + )(RegisterTeiDataEntryComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js new file mode 100644 index 0000000000..76bd3babe3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js @@ -0,0 +1,53 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import teiClasses from './trackedEntityInstance.module.css'; +import { TeiRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryTrackedEntityInstance.types'; +import { getTeiRegistrationMetadata } from './tei.selectors'; +import { useLocationQuery } from '../../../../../../../utils/routing'; + +const RelationshipTrackedEntityInstancePlain = + ({ + theme, + onSave, + trackedEntityTypeId, + // teiRegistrationMetadata = {}, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { orgUnitId } = useLocationQuery(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const teiRegistrationMetadata = getTeiRegistrationMetadata(trackedEntityTypeId); + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + + if (!teiRegistrationMetadata && !teiRegistrationMetadata?.form) { + return null; + } + + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; + +export const DataEntryTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js new file mode 100644 index 0000000000..7dc3f538e5 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -0,0 +1,18 @@ +// @flow +import type { Node } from 'react'; +import type { TeiRegistration } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + SaveForEnrollmentAndTeiRegistration, + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type Props = {| + theme: Theme, + onSave: SaveForEnrollmentAndTeiRegistration, + teiRegistrationMetadata?: TeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js new file mode 100644 index 0000000000..5bb8975389 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryTrackedEntityInstance } from './DataEntryTrackedEntityInstance'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js new file mode 100644 index 0000000000..2363e24584 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js @@ -0,0 +1,18 @@ +// @flow +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; +import type { TrackedEntityType } from '../../../../../../../metaData'; +import { getTrackedEntityTypeThrowIfNotFound } from '../../../../../../../metaData'; + +// $FlowFixMe +export const getTeiRegistrationMetadata = (TETypeId: string) => { + let TEType: TrackedEntityType; + try { + TEType = getTrackedEntityTypeThrowIfNotFound(TETypeId); + } catch (error) { + log.error(errorCreator('Could not get TrackedEntityType for id')({ TETypeId })); + return null; + } + + return TEType.teiRegistration; +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css new file mode 100644 index 0000000000..8d899a1886 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js new file mode 100644 index 0000000000..69f62d494a --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -0,0 +1,133 @@ +// @flow +import React, { type ComponentType, useContext, useCallback } from 'react'; +import { compose } from 'redux'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; +import { RegisterTeiDataEntry } from './DataEntry/RegisterTeiDataEntry.container'; +import { RegistrationSection } from './RegistrationSection'; +import { DataEntryWidgetOutput } from '../../../../DataEntryWidgetOutput/DataEntryWidgetOutput.container'; +import { ResultsPageSizeContext } from '../../../shared-contexts'; +import type { Props } from './RegisterTei.types'; +import { withErrorMessageHandler } from '../../../../../HOC'; + +const getStyles = () => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + leftContainer: { + flexGrow: 10, + flexBasis: 0, + margin: 8, + }, +}); + +const CardListButton = (({ teiId, values, handleOnClick }) => ( + +)); + +const DialogButtons = ({ onCancel, onSave, trackedEntityName }) => ( + <> + +
+ +
+ +); + +const RegisterTeiPlain = ({ + dataEntryId, + itemId, + onLink, + onSave, + onGetUnsavedAttributeValues, + trackedEntityName, + trackedEntityTypeId, + selectedProgramId, + classes, +}: Props) => { + const { resultsPageSize } = useContext(ResultsPageSizeContext); + + const renderDuplicatesCardActions = useCallback(({ item }) => ( + + ), [onLink]); + + const renderDuplicatesDialogActions = useCallback((onCancel, onSaveArgument) => ( + + ), [trackedEntityName]); + + const ExistingUniqueValueDialogActions = useCallback(({ teiId, attributeValues }) => ( + + ), [onLink]); + + const handleSave = useCallback(() => { + onSave(itemId, dataEntryId); + }, [onSave, itemId, dataEntryId]); + + return ( +
+
+ + +
+ + + } + /> +
+ ); +}; + +export const RegisterTeiComponent: ComponentType<$Diff> = + compose( + withErrorMessageHandler(), + withStyles(getStyles), + )(RegisterTeiPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js new file mode 100644 index 0000000000..c71313710f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js @@ -0,0 +1,35 @@ +// @flow +import React from 'react'; +import { useSelector } from 'react-redux'; +import { RegisterTeiComponent } from './RegisterTei.component'; +import type { OwnProps } from './RegisterTei.types'; +import { useScopeInfo } from '../../../../../hooks/useScopeInfo'; + +export const RegisterTei = ({ + onLink, + onSave, + onGetUnsavedAttributeValues, + trackedEntityTypeId, + suggestedProgramId, +}: OwnProps) => { + const dataEntryId = 'relationship'; + const itemId = useSelector(({ dataEntries }) => dataEntries[dataEntryId]?.itemId); + const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); + const newRelationshipProgramId = suggestedProgramId || trackedEntityTypeId; + const { trackedEntityName } = useScopeInfo(newRelationshipProgramId); + + return ( + + ); +}; + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js new file mode 100644 index 0000000000..957ca0637f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js @@ -0,0 +1,17 @@ +// @flow +type PropsFromRedux = {| + dataEntryId: string, + itemId: string, + trackedEntityName: ?string, + error: string, +|}; + +export type OwnProps = {| + onLink: (teiId: string, values: Object) => void, + onGetUnsavedAttributeValues?: ?Function, + onSave: (itemId: string, dataEntryId: string) => void, + suggestedProgramId: string, + trackedEntityTypeId: string, +|}; + +export type Props = {|...PropsFromRedux, ...OwnProps, ...CssClasses |} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js new file mode 100644 index 0000000000..f815b36498 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js @@ -0,0 +1,157 @@ +// @flow +import * as React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { LinkButton } from '../../../../../../Buttons/LinkButton.component'; +import { ProgramFilterer } from '../../../../../../ProgramFilterer'; +import type { Program } from '../../../../../../../metaData'; +import { TrackerProgram } from '../../../../../../../metaData'; +import { + VirtualizedSelectField, + withSelectTranslations, + withFocusSaver, + withDefaultFieldContainer, + withLabel, + withFilterProps, +} from '../../../../../../FormFields/New'; +import { NonBundledDhis2Icon } from '../../../../../../NonBundledDhis2Icon'; + +const getStyles = (theme: Theme) => ({ + iconContainer: { + display: 'flex', + alignItems: 'center', + paddingRight: 5, + }, + icon: { + width: 22, + height: 22, + borderRadius: 2, + }, + isFilteredContainer: { + fontSize: 12, + color: theme.palette.grey.dark, + paddingTop: 5, + }, + isFilteredLink: { + paddingLeft: 2, + backgroundColor: 'inherit', + }, +}); + +type Option = { + label: string, + value: string, + iconLeft?: ?React.Node, +}; + +type Props = { + orgUnitIds: ?Array, + value: string, + trackedEntityTypeId: string, + classes: Object, + onUpdateSelectedProgram: (programId: string) => void, + onClearFilter: () => void, +}; + +class ProgramSelector extends React.Component { + baseLineFilter = (program: Program) => { + const { trackedEntityTypeId } = this.props; + + const isValid = program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.write; + + return isValid; + } + + getOptionsFromPrograms = (programs: Array): Array