Repository: dhis2/capture-app
Pull request #4081: refactor: convert WidgetEventNote folder from Flow to TypeScript
Author: login: devin-ai-integration[bot]
Labels: name: e2e-tests
Created: 2025-05-14T09:23:11Z | Merged: 2025-05-15T08:50:58Z

# TypeScript Conversion: WidgetEventNote Folder

This PR converts the `WidgetEventNote` folder from Flow to TypeScript as part of the ongoing TypeScript migration effort.

## Changes

- Converted the following files:
  - `WidgetEventNote.types.js` → `WidgetEventNote.types.ts`
  - `WidgetEventNote.actions.js` → `WidgetEventNote.actions.ts`
  - `WidgetEventNote.epics.js` → `WidgetEventNote.epics.ts`
  - `WidgetEventNote.component.js` → `WidgetEventNote.component.tsx`
  - `index.js` → `index.ts`

- Added proper TypeScript types:
  - Defined explicit types for component props
  - Added types for Redux actions and epics
  - Created proper type definitions for notes, actions, and API utilities
  - Used TypeScript patterns consistent with reference components

## Verification

- TypeScript check (`yarn tsc:check`): ✅ Passed

## Notes

- Followed the TypeScript migration patterns established in reference components like `WidgetNote`, `WidgetFeedback`, and `OrgUnitFetcher`
- Maintained the same functionality as the original JavaScript implementation

Link to Devin run: https://app.devin.ai/sessions/c48297cfa6384e3cadf90b9e5613db4d
Requested by: Simona Domnisoru (sdomnisoru@dhis2.org)



Diff:
diff --git a/src/core_modules/capture-core-utils/types/global.ts b/src/core_modules/capture-core-utils/types/global.ts
index ec363d8a92..aadcaf783c 100644
--- a/src/core_modules/capture-core-utils/types/global.ts
+++ b/src/core_modules/capture-core-utils/types/global.ts
@@ -1,9 +1,22 @@
 import type { Observable } from 'rxjs';
 import type { Action } from 'redux';
 
+export type ReduxStore = {
+    value: {
+        dataEntries: Record<string, { eventId: string }>;
+        currentSelections: Record<string, unknown>;
+    };
+};
+
+export type ApiUtils = {
+    querySingleResource: (params: { resource: string; params?: Record<string, unknown> }) => Promise<any>;
+    fromClientDate: (date: string) => { getServerZonedISOString: () => string };
+};
+
 /**
- * Represents an Observable stream of Redux actions with a specific payload type.
+ * Represents an Observable stream of Redux actions with a specific payload type and optional meta.
  *
  * @template PayloadType The type of the action's payload.
+ * @template MetaType The type of the action's meta.
  */
-export type EpicAction<PayloadType> = Observable<Action & { payload: PayloadType }>;
+export type EpicAction<PayloadType, MetaType = Record<string, unknown>> = Observable<Action & { payload: PayloadType, meta: MetaType }>;
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts
similarity index 93%
rename from src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.js
rename to src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts
index c6a0309f4e..fbdaef2012 100644
--- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.js
+++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts
@@ -1,4 +1,3 @@
-// @flow
 import { featureAvailable, FEATURES } from 'capture-core-utils';
 import { actionCreator } from '../../actions/actions.utils';
 import { effectMethods } from '../../trackerOffline';
@@ -18,7 +17,7 @@ export const batchActionTypes = {
 export const requestAddNoteForEvent = (itemId: string, dataEntryId: string, note: string) =>
     actionCreator(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT)({ itemId, dataEntryId, note });
 
-export const startAddNoteForEvent = (eventUid: string, serverData: Object, selections: Object, context: Object) =>
+export const startAddNoteForEvent = (eventUid: string, serverData: Record<string, unknown>, selections: Record<string, unknown>, context: Record<string, unknown>) =>
     actionCreator(actionTypes.START_ADD_NOTE_FOR_EVENT)({ selections, context }, {
         offline: {
             effect: {
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx
similarity index 80%
rename from src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.js
rename to src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx
index 55c4190e12..837777659d 100644
--- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.js
+++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx
@@ -1,4 +1,3 @@
-// @flow
 import React from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 import i18n from '@dhis2/d2-i18n';
@@ -8,9 +7,10 @@ import { WidgetNote } from '../WidgetNote';
 
 export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => {
     const dispatch = useDispatch();
-    const notes = useSelector(({ dataEntriesNotes }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []);
+    const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record<string, any[]> }) =>
+        dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []);
 
-    const onAddNote = (newNoteValue) => {
+    const onAddNote = (newNoteValue: string) => {
         dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue));
     };
 
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
similarity index 75%
rename from src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js
rename to src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
index 843f59d51c..e39f34d9fa 100644
--- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js
+++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
@@ -1,11 +1,12 @@
-// @flow
 import { batchActions } from 'redux-batched-actions';
 import { ofType } from 'redux-observable';
 import { featureAvailable, FEATURES } from 'capture-core-utils';
 import { map, switchMap } from 'rxjs/operators';
 import uuid from 'd2-utilizr/lib/uuid';
 import moment from 'moment';
+import type { ReduxStore, ApiUtils, EpicAction } from '../../../capture-core-utils/types';
 import { actionTypes, batchActionTypes, startAddNoteForEvent } from './WidgetEventNote.actions';
+import type { ClientNote, FormNote, SaveContext } from './WidgetEventNote.types';
 
 import {
     addEventNote,
@@ -17,17 +18,27 @@ import {
     removeNote,
 } from '../DataEntry/actions/dataEntry.actions';
 
-const createServerData = (eventId, note, useNewEndpoint) => {
+type AddNoteActionPayload = {
+    itemId: string;
+    dataEntryId: string;
+    note: string;
+};
+
+type RemoveNoteActionMeta = {
+    context: SaveContext;
+};
+
+const createServerData = (eventId: string, note: string, useNewEndpoint: boolean): Record<string, unknown> => {
     if (useNewEndpoint) {
         return { event: eventId, value: note };
     }
     return { event: eventId, notes: [{ value: note }] };
 };
 
-export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) =>
+export const addNoteForEventEpic = (action$: EpicAction<AddNoteActionPayload>, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) =>
     action$.pipe(
         ofType(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT),
-        switchMap((action) => {
+        switchMap((action: { payload: AddNoteActionPayload }) => {
             const state = store.value;
             const payload = action.payload;
             const eventId = state.dataEntries[payload.dataEntryId].eventId;
@@ -43,7 +54,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore,
 
                 const serverData = createServerData(eventId, payload.note, useNewEndpoint);
 
-                const clientNote = {
+                const clientNote: ClientNote = {
                     value: payload.note,
                     lastUpdatedBy: {
                         firstName,
@@ -53,7 +64,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore,
                     storedBy: userName,
                     storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(),
                 };
-                const formNote = {
+                const formNote: FormNote = {
                     ...clientNote,
                     storedAt: clientNote.storedAt,
                     createdBy: {
@@ -62,7 +73,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore,
                         uid: clientId,
                     },
                 };
-                const saveContext = {
+                const saveContext: SaveContext = {
                     dataEntryId: payload.dataEntryId,
                     itemId: payload.itemId,
                     eventId,
@@ -77,10 +88,10 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore,
             });
         }));
 
-export const removeNoteForEventEpic = (action$: InputObservable) =>
+export const removeNoteForEventEpic = (action$: EpicAction<any, RemoveNoteActionMeta>) =>
     action$.pipe(
         ofType(actionTypes.ADD_NOTE_FAILED_FOR_EVENT),
-        map((action) => {
+        map((action: { meta: { context: SaveContext } }) => {
             const context = action.meta.context;
             return batchActions([
                 removeNote(context.dataEntryId, context.itemId, context.noteClientId),
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.js
deleted file mode 100644
index df1a414f68..0000000000
--- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// @flow
-
-export type Props = {|
-    dataEntryKey: string,
-    dataEntryId: string,
-    ...CssClasses
-|};
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts
new file mode 100644
index 0000000000..87957e6627
--- /dev/null
+++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts
@@ -0,0 +1,30 @@
+export type Props = {
+    dataEntryKey: string;
+    dataEntryId: string;
+};
+
+export type ClientNote = {
+    value: string;
+    lastUpdatedBy: {
+        firstName: string;
+        surname: string;
+        uid: string;
+    };
+    storedBy: string;
+    storedAt: string;
+};
+
+export type FormNote = ClientNote & {
+    createdBy: {
+        firstName: string;
+        surname: string;
+        uid: string;
+    };
+};
+
+export type SaveContext = {
+    dataEntryId: string;
+    itemId: string;
+    eventId: string;
+    noteClientId: string;
+};
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/index.js b/src/core_modules/capture-core/components/WidgetEventNote/index.js
deleted file mode 100644
index 47ae18dfcd..0000000000
--- a/src/core_modules/capture-core/components/WidgetEventNote/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { WidgetEventNote } from './WidgetEventNote.component';
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/index.ts b/src/core_modules/capture-core/components/WidgetEventNote/index.ts
new file mode 100644
index 0000000000..df498109aa
--- /dev/null
+++ b/src/core_modules/capture-core/components/WidgetEventNote/index.ts
@@ -0,0 +1,2 @@
+export { WidgetEventNote } from './WidgetEventNote.component';
+export { addNoteForEventEpic, removeNoteForEventEpic } from './WidgetEventNote.epics';


Reviews:
user: login: simonadomnisoru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:21:24Z
user: login: simonadomnisoru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:36:53Z
user: login: simonadomnisoru
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T08:24:49Z

Review comments:
diff_hunk: @@ -8,9 +7,10 @@ import { WidgetNote } from '../WidgetNote';
 
 export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => {
     const dispatch = useDispatch();
-    const notes = useSelector(({ dataEntriesNotes }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []);
+    const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record<string, any[]> }) => 
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx
user: login: simonadomnisoru
body: Delete the trailing spaces at the end of this line
created_at: 2025-05-15T06:50:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,17 +17,45 @@ import {
     removeNote,
 } from '../DataEntry/actions/dataEntry.actions';
 
-const createServerData = (eventId, note, useNewEndpoint) => {
+type ApiUtils = {
+    querySingleResource: (params: { resource: string; params?: Record<string, unknown> }) => Promise<any>;
+    fromClientDate: (date: string) => { getServerZonedISOString: () => string };
+};
+
+type ReduxStore = {
+    value: {
+        dataEntries: Record<string, { eventId: string }>;
+        currentSelections: Record<string, unknown>;
+    };
+};
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: `ReduxStore` and `ApiUtils` are global types that will be used when refactoring most of your `*.epics.ts` files. Therefore, I want you to move their definitions to `global.ts`. Then, import and use them in `WidgetEventNote.epics.ts`
created_at: 2025-05-15T06:52:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,17 +17,45 @@ import {
     removeNote,
 } from '../DataEntry/actions/dataEntry.actions';
 
-const createServerData = (eventId, note, useNewEndpoint) => {
+type ApiUtils = {
+    querySingleResource: (params: { resource: string; params?: Record<string, unknown> }) => Promise<any>;
+    fromClientDate: (date: string) => { getServerZonedISOString: () => string };
+};
+
+type ReduxStore = {
+    value: {
+        dataEntries: Record<string, { eventId: string }>;
+        currentSelections: Record<string, unknown>;
+    };
+};
+
+type ActionWithPayload = {
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: Rename `ActionWithPayload` to `AddNoteActionPayload`
created_at: 2025-05-15T06:56:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,17 +17,45 @@ import {
     removeNote,
 } from '../DataEntry/actions/dataEntry.actions';
 
-const createServerData = (eventId, note, useNewEndpoint) => {
+type ApiUtils = {
+    querySingleResource: (params: { resource: string; params?: Record<string, unknown> }) => Promise<any>;
+    fromClientDate: (date: string) => { getServerZonedISOString: () => string };
+};
+
+type ReduxStore = {
+    value: {
+        dataEntries: Record<string, { eventId: string }>;
+        currentSelections: Record<string, unknown>;
+    };
+};
+
+type ActionWithPayload = {
+    type: string;
+    payload: {
+        itemId: string;
+        dataEntryId: string;
+        note: string;
+    };
+};
+
+type ActionWithMeta = {
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: Rename `ActionWithMeta` to `RemoveNoteActionMeta`
created_at: 2025-05-15T06:57:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,17 +17,45 @@ import {
     removeNote,
 } from '../DataEntry/actions/dataEntry.actions';
 
-const createServerData = (eventId, note, useNewEndpoint) => {
+type ApiUtils = {
+    querySingleResource: (params: { resource: string; params?: Record<string, unknown> }) => Promise<any>;
+    fromClientDate: (date: string) => { getServerZonedISOString: () => string };
+};
+
+type ReduxStore = {
+    value: {
+        dataEntries: Record<string, { eventId: string }>;
+        currentSelections: Record<string, unknown>;
+    };
+};
+
+type ActionWithPayload = {
+    type: string;
+    payload: {
+        itemId: string;
+        dataEntryId: string;
+        note: string;
+    };
+};
+
+type ActionWithMeta = {
+    type: string;
+    meta: {
+        context: SaveContext;
+    };
+};
+
+const createServerData = (eventId: string, note: string, useNewEndpoint: boolean): Record<string, unknown> => {
     if (useNewEndpoint) {
         return { event: eventId, value: note };
     }
     return { event: eventId, notes: [{ value: note }] };
 };
 
-export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) =>
+export const addNoteForEventEpic = (action$: any, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) =>
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: Let's import and use the global type `EpicAction`. Have a look at how it is used in `OrgUnitFetcher.epics.ts`. The code should look like this: `EpicAction<AddNoteActionPayload>`
created_at: 2025-05-15T06:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,10 +105,10 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore,
             });
         }));
 
-export const removeNoteForEventEpic = (action$: InputObservable) =>
+export const removeNoteForEventEpic = (action$: any) =>
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: In `global.ts`, update the definition of `EpicAction` to include `meta`. The code should look like this:
`export type EpicAction<PayloadType, MetaType = Record<string, unknown>> = Observable<Action & { payload: PayloadType, meta: MetaType }>;`

Import and use the global type `EpicAction`. Have a look at how it is used in `OrgUnitFetcher.epics.ts`. The code will look like this: `action$: EpicAction<any, RemoveNoteActionMeta>`
created_at: 2025-05-15T07:11:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,12 @@
-// @flow
 import { batchActions } from 'redux-batched-actions';
 import { ofType } from 'redux-observable';
 import { featureAvailable, FEATURES } from 'capture-core-utils';
 import { map, switchMap } from 'rxjs/operators';
 import uuid from 'd2-utilizr/lib/uuid';
 import moment from 'moment';
 import { actionTypes, batchActionTypes, startAddNoteForEvent } from './WidgetEventNote.actions';
+import type { ClientNote, FormNote, SaveContext } from './WidgetEventNote.types';
+import type { ReduxStore, ApiUtils, EpicAction } from 'capture-core-utils/types';
path: src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.ts
user: login: simonadomnisoru
body: Please fix the  `Absolute imports should come before relative imports  import/first` eslint error by moving this line at the top of the file
created_at: 2025-05-15T07:35:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: devin-ai-integration[bot]
created_at: 2025-05-14T09:23:15Z
body: ### 🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:
- Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
- Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:
- [ ] Disable automatic comment and CI monitoring

author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-15T08:00:38Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=dhis2_capture-app&pullRequest=4081) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=dhis2_capture-app&pullRequest=4081&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=dhis2_capture-app&pullRequest=4081&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=dhis2_capture-app&pullRequest=4081&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=dhis2_capture-app&pullRequest=4081&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=dhis2_capture-app&pullRequest=4081&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=dhis2_capture-app&pullRequest=4081)


author_association: NONE