Repository: dhis2/capture-app
Pull request #4071: refactor: convert WidgetFeedback folder from Flow to TypeScript
Author: login: devin-ai-integration[bot]
Created: 2025-05-05T11:43:34Z | Merged: 2025-05-05T13:04:36Z

# TypeScript Refactoring: WidgetFeedback Folder

This PR refactors the `src/core_modules/capture-core/components/WidgetFeedback` folder from Flow to TypeScript as part of the ongoing TypeScript migration effort.

## Changes
- Converted `WidgetFeedback.types.js` to TypeScript
- Converted `WidgetFeedback.component.js` to TypeScript
- Converted `WidgetFeedbackContent/WidgetFeedbackContent.js` to TypeScript
- Updated `index.js` to TypeScript
- Removed original JavaScript files

## Testing
- Verified with `yarn tsc:check` - TypeScript compilation passes
- Verified with `yarn linter:check` - Linting passes

## Notes
- No logic changes were made, only type annotations were added
- Followed the patterns established in the QuickActionButton and OrgUnitFetcher components
- Used 'in' operator for type-safe property checking in WidgetFeedbackContent

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



Diff:
diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.tsx
similarity index 98%
rename from src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js
rename to src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.tsx
index 4e0790a5cb..6d2f0b4bad 100644
--- a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js
+++ b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.tsx
@@ -1,9 +1,8 @@
-// @flow
 import React, { useState } from 'react';
 import i18n from '@dhis2/d2-i18n';
 import { Widget } from '../Widget';
-import type { Props } from './WidgetFeedback.types';
 import { WidgetFeedbackContent } from './WidgetFeedbackContent/WidgetFeedbackContent';
+import type { Props } from './WidgetFeedback.types';
 
 export const WidgetFeedback = ({ feedback, emptyText }: Props) => {
     const [openStatus, setOpenStatus] = useState(true);
@@ -26,4 +25,3 @@ export const WidgetFeedback = ({ feedback, emptyText }: Props) => {
         </div>
     );
 };
-
diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js
deleted file mode 100644
index c56c9e33e5..0000000000
--- a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// @flow
-
-export type FilteredText = {|
-    id: string,
-    message: string,
-    color?: ?string,
-|}
-
-export type FilteredKeyValue = {|
-    id: string,
-    key: string,
-    value: string,
-    color?: ?string,
-|}
-
-export type WidgetData = string | FilteredText | FilteredKeyValue;
-
-export type ContentType = {|
-    widgetData?: ?Array<WidgetData>,
-    emptyText: string,
-    ...CssClasses
-|}
-
-export type InputFeedbackProps = {|
-    widgetEffects?: Object,
-    feedbackEmptyText: string,
-|}
-
-export type Props = {|
-    feedback?: ?Array<string | FilteredText | FilteredKeyValue>,
-    emptyText: string,
-|}
-
-export type IndicatorProps = {|
-    indicators?: ?Array<string | FilteredText | FilteredKeyValue>,
-    emptyText: string,
-|}
-
-export type InputIndicatorProps = {|
-    widgetEffects?: Object,
-    indicatorEmptyText: string,
-|}
diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.ts b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.ts
new file mode 100644
index 0000000000..80a6188b23
--- /dev/null
+++ b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.ts
@@ -0,0 +1,39 @@
+export type FilteredText = {
+    id: string;
+    message: string;
+    color?: string;
+}
+
+export type FilteredKeyValue = {
+    id: string;
+    key: string;
+    value: string;
+    color?: string;
+}
+
+export type WidgetData = string | FilteredText | FilteredKeyValue;
+
+export type ContentType = {
+    widgetData?: Array<WidgetData>;
+    emptyText: string;
+};
+
+export type InputFeedbackProps = {
+    widgetEffects?: Record<string, unknown>;
+    feedbackEmptyText: string;
+}
+
+export type Props = {
+    feedback?: Array<string | FilteredText | FilteredKeyValue>;
+    emptyText: string;
+}
+
+export type IndicatorProps = {
+    indicators?: Array<string | FilteredText | FilteredKeyValue>;
+    emptyText: string;
+}
+
+export type InputIndicatorProps = {
+    widgetEffects?: Record<string, unknown>;
+    indicatorEmptyText: string;
+}
diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.js b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
similarity index 80%
rename from src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.js
rename to src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
index 935070cb18..c588c8531f 100644
--- a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.js
+++ b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
@@ -1,8 +1,6 @@
-// @flow
-
 import React, { type ComponentType } from 'react';
 import { spacersNum, colors } from '@dhis2/ui';
-import { withStyles } from '@material-ui/core';
+import { withStyles, type WithStyles } from '@material-ui/core';
 import type { FilteredKeyValue, FilteredText, ContentType, WidgetData } from '../WidgetFeedback.types';
 
 const styles = {
@@ -30,8 +28,9 @@ const styles = {
     },
 };
 
+type Props = ContentType & WithStyles<typeof styles>;
 
-const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: ContentType) => {
+const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Props) => {
     if (!widgetData?.length) {
         return (
             <div
@@ -78,10 +77,10 @@ const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Cont
             <ul className={classes.unorderedList}>
                 {widgetData.map((rule: WidgetData, index: number) => {
                     if (typeof rule === 'object') {
-                        if (rule.key || rule.value) {
-                            return renderKeyValue(rule);
-                        } else if (rule.message) {
-                            return renderTextObject(rule);
+                        if ('key' in rule || 'value' in rule) {
+                            return renderKeyValue(rule as FilteredKeyValue);
+                        } else if ('message' in rule) {
+                            return renderTextObject(rule as FilteredText);
                         }
                     } else if (typeof rule === 'string') {
                         return renderString(rule, index);
@@ -93,4 +92,5 @@ const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Cont
     );
 };
 
-export const WidgetFeedbackContent: ComponentType<$Diff<ContentType, CssClasses>> = withStyles(styles)(WidgetFeedbackContentComponent);
+export const WidgetFeedbackContent =
+    withStyles(styles)(WidgetFeedbackContentComponent) as ComponentType<ContentType>;
diff --git a/src/core_modules/capture-core/components/WidgetFeedback/index.js b/src/core_modules/capture-core/components/WidgetFeedback/index.ts
similarity index 93%
rename from src/core_modules/capture-core/components/WidgetFeedback/index.js
rename to src/core_modules/capture-core/components/WidgetFeedback/index.ts
index 8f608c3a41..af4145477b 100644
--- a/src/core_modules/capture-core/components/WidgetFeedback/index.js
+++ b/src/core_modules/capture-core/components/WidgetFeedback/index.ts
@@ -1,4 +1,2 @@
-// @flow
-
 export { WidgetFeedback } from './WidgetFeedback.component';
 export type { FilteredText, FilteredKeyValue } from './WidgetFeedback.types';


Reviews:
user: login: simonadomnisoru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:50:56Z
user: login: simonadomnisoru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:03:05Z
user: login: simonadomnisoru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:40:28Z
user: login: simonadomnisoru
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:04:05Z

Review comments:
diff_hunk: @@ -93,4 +92,6 @@ const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Cont
     );
 };
 
-export const WidgetFeedbackContent: ComponentType<$Diff<ContentType, CssClasses>> = withStyles(styles)(WidgetFeedbackContentComponent);
+type PropsWithoutClasses = Omit<ContentType, 'classes'>;
+
+export const WidgetFeedbackContent = withStyles(styles)(WidgetFeedbackContentComponent) as ComponentType<PropsWithoutClasses>;
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
user: login: simonadomnisoru
body: use this patern: export const WidgetFeedbackContent =
    withStyles(styles)(WidgetFeedbackContentComponent) as ComponentType<ContentType>;
created_at: 2025-05-05T11:46:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,7 +60,7 @@ const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Cont
         </li>
     );
 
-    const renderString = (item: string, index: number) => (
+    const renderString = (item: string, index: number): React.ReactElement => (
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
user: login: simonadomnisoru
body: Clean React.ReactElement from this file
created_at: 2025-05-05T11:46:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+export type FilteredText = {
+    id: string;
+    message: string;
+    color?: string | undefined;
+}
+
+export type FilteredKeyValue = {
+    id: string;
+    key: string;
+    value: string;
+    color?: string | undefined;
+}
+
+export type WidgetData = string | FilteredText | FilteredKeyValue;
+
+export type CssClasses = {
+    classes: {
+        [key: string]: string;
+    };
+}
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.ts
user: login: simonadomnisoru
body: Remove these lines
created_at: 2025-05-05T11:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,8 +28,9 @@ const styles = {
     },
 };
 
+type Props = ContentType;
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
user: login: simonadomnisoru
body: use `type Props = ContentType & WithStyles<typeof styles>;`
created_at: 2025-05-05T11:48:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,12 +1,11 @@
-// @flow
 import React, { useState } from 'react';
 import i18n from '@dhis2/d2-i18n';
 import { Widget } from '../Widget';
-import type { Props } from './WidgetFeedback.types';
 import { WidgetFeedbackContent } from './WidgetFeedbackContent/WidgetFeedbackContent';
+import type { Props } from './WidgetFeedback.types';
 
-export const WidgetFeedback = ({ feedback, emptyText }: Props) => {
-    const [openStatus, setOpenStatus] = useState(true);
+export const WidgetFeedback = ({ feedback, emptyText }: Props): React.ReactElement => {
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.tsx
user: login: simonadomnisoru
body: Remove React.ReactElement
created_at: 2025-05-05T11:50:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,4 +92,4 @@ const WidgetFeedbackContentComponent = ({ widgetData, emptyText, classes }: Cont
     );
 };
 
-export const WidgetFeedbackContent: ComponentType<$Diff<ContentType, CssClasses>> = withStyles(styles)(WidgetFeedbackContentComponent);
+export const WidgetFeedbackContent = withStyles(styles)(WidgetFeedbackContentComponent) as ComponentType<Omit<ContentType, 'classes'>>;
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
user: login: simonadomnisoru
body: Don't use Omit. Use the pattern from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx something like: `
export const WidgetFeedbackContent =
withStyles(styles)(WidgetFeedbackContentComponent) as ComponentType;
`
created_at: 2025-05-05T11:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,6 @@
-// @flow
-
 import React, { type ComponentType } from 'react';
 import { spacersNum, colors } from '@dhis2/ui';
-import { withStyles } from '@material-ui/core';
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedbackContent/WidgetFeedbackContent.tsx
user: login: simonadomnisoru
body: use the pattern` import { withStyles, type WithStyles } from '@material-ui/core';` from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx
created_at: 2025-05-05T12:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+export type FilteredText = {
+    id: string;
+    message: string;
+    color?: string | undefined;
path: src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.ts
user: login: simonadomnisoru
body: Remove the "undefined" type because using "?" is sufficient. Change it everywhere in this file.
created_at: 2025-05-05T12:40:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: devin-ai-integration[bot]
created_at: 2025-05-05T11:43:37Z
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-05T12:45:12Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=dhis2_capture-app&pullRequest=4071) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=dhis2_capture-app&pullRequest=4071&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New Code Smells](https://sonarcloud.io/dashboard?id=dhis2_capture-app&pullRequest=4071) (required ≤ 0)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=dhis2_capture-app&pullRequest=4071)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


author_association: NONE