Skip to content

Commit c5f6f87

Browse files
fix: resolve CI failures in TypeScript migration (#47)
- Fix prettier formatting for all migrated .tsx/.ts files - Fix ESLint errors: remove unused eslint-disable directives, replace any types - Fix dropdown-button test: update modal import from .js to .tsx - Fix simple-single-select-field: restore missing labelId prop on Field - Fix table-header-cell/table-data-cell tests: expect Number() for colSpan/rowSpan - Fix organisation-unit-tree tests: replace obsolete prop-types tests with TS-compatible tests - Fix broken story imports: update .js/.tsx references to match renamed files - Fix scripts/ts-check.js formatting Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Eirik <eirik.haugstulen@gmail.com>
1 parent d65da51 commit c5f6f87

211 files changed

Lines changed: 1329 additions & 507 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ const config = {
4949
ecmaFeatures: { jsx: true },
5050
},
5151
plugins: ['@typescript-eslint'],
52-
extends: [
53-
'plugin:@typescript-eslint/recommended',
54-
],
52+
extends: ['plugin:@typescript-eslint/recommended'],
5553
settings: {
5654
'import/resolver': {
5755
typescript: {
@@ -63,9 +61,15 @@ const config = {
6361
// Disable JS-only rules that conflict with TS
6462
'react/prop-types': 'off',
6563
'no-unused-vars': 'off',
66-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
64+
'@typescript-eslint/no-unused-vars': [
65+
'error',
66+
{ argsIgnorePattern: '^_' },
67+
],
6768
// Keep consistent with existing code style
68-
'react/no-unknown-property': ['error', { ignore: ['jsx', 'global'] }],
69+
'react/no-unknown-property': [
70+
'error',
71+
{ ignore: ['jsx', 'global'] },
72+
],
6973
// Allow .js extension imports in TS files (Babel resolves .tsx -> .js)
7074
'import/extensions': 'off',
7175
},

MIGRATION_GUIDE.md

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ This document describes the approach for incrementally migrating `@dhis2/ui` com
66

77
## Key Findings
88

9-
- **Build system**: `d2-app-scripts build` uses Babel with `@babel/preset-typescript` already included — it can compile `.ts`/`.tsx` out of the box.
10-
- **styled-jsx**: Ships TypeScript definitions that augment React's `StyleHTMLAttributes` with `jsx` and `global` props. Works in `.tsx` files with the `"types": ["styled-jsx"]` tsconfig option.
11-
- **Existing types**: Each component has hand-written `.d.ts` files in a `types/` directory. After migration, these can be auto-generated from source via `tsc --declaration`.
12-
- **ESLint**: The project uses `@dhis2/cli-style` (ESLint 7.x). TypeScript linting works via `@typescript-eslint/parser` v6 + `@typescript-eslint/eslint-plugin` v6.
13-
- **One quirk**: `d2-app-scripts build` doesn't rename `.tsx``.js` in build output. A `post-build-rename.js` script handles this.
9+
- **Build system**: `d2-app-scripts build` uses Babel with `@babel/preset-typescript` already included — it can compile `.ts`/`.tsx` out of the box.
10+
- **styled-jsx**: Ships TypeScript definitions that augment React's `StyleHTMLAttributes` with `jsx` and `global` props. Works in `.tsx` files with the `"types": ["styled-jsx"]` tsconfig option.
11+
- **Existing types**: Each component has hand-written `.d.ts` files in a `types/` directory. After migration, these can be auto-generated from source via `tsc --declaration`.
12+
- **ESLint**: The project uses `@dhis2/cli-style` (ESLint 7.x). TypeScript linting works via `@typescript-eslint/parser` v6 + `@typescript-eslint/eslint-plugin` v6.
13+
- **One quirk**: `d2-app-scripts build` doesn't rename `.tsx``.js` in build output. A `post-build-rename.js` script handles this.
1414

1515
## How to Migrate a Component
1616

@@ -23,26 +23,32 @@ This document describes the approach for incrementally migrating `@dhis2/ui` com
2323
"rootDir": "./src"
2424
},
2525
"include": ["src/**/*.ts", "src/**/*.tsx"],
26-
"exclude": ["node_modules", "build", "**/*.stories.*", "**/*.test.*", "**/*.e2e.*"]
26+
"exclude": [
27+
"node_modules",
28+
"build",
29+
"**/*.stories.*",
30+
"**/*.test.*",
31+
"**/*.e2e.*"
32+
]
2733
}
2834
```
2935

3036
### 2. Convert source files from `.js``.tsx` (or `.ts`)
3137

32-
- Replace `PropTypes` with TypeScript interfaces
33-
- Remove `prop-types` and `@dhis2/prop-types` imports
34-
- Add explicit types for props, state, and function parameters
35-
- Keep `styled-jsx` usage as-is (it works in `.tsx`)
36-
- Keep import paths using `.js` extensions (Babel + Node resolve these to `.tsx`)
37-
- Leave `.stories.js` and `.feature` test files as JavaScript
38+
- Replace `PropTypes` with TypeScript interfaces
39+
- Remove `prop-types` and `@dhis2/prop-types` imports
40+
- Add explicit types for props, state, and function parameters
41+
- Keep `styled-jsx` usage as-is (it works in `.tsx`)
42+
- Keep import paths using `.js` extensions (Babel + Node resolve these to `.tsx`)
43+
- Leave `.stories.js` and `.feature` test files as JavaScript
3844

3945
### 3. Update `d2.config.js` entry point
4046

4147
```js
4248
module.exports = {
4349
type: 'lib',
4450
entryPoints: {
45-
lib: 'src/index.ts', // was src/index.js
51+
lib: 'src/index.ts', // was src/index.js
4652
},
4753
}
4854
```
@@ -73,20 +79,20 @@ cd components/<name> && yarn build
7379

7480
## Files Added/Modified
7581

76-
| File | Purpose |
77-
|------|---------|
78-
| `tsconfig.json` (root) | Base TypeScript config for the whole repo |
79-
| `components/<name>/tsconfig.json` | Per-component TS config extending root |
80-
| `.eslintrc.js` | Added TypeScript override block for `.ts`/`.tsx` files |
81-
| `scripts/ts-check.js` | Unified feedback pipeline (tsc + eslint + prettier) |
82-
| `scripts/post-build-rename.js` | Renames `.tsx`/`.ts``.js` in build output |
82+
| File | Purpose |
83+
| --------------------------------- | ------------------------------------------------------ |
84+
| `tsconfig.json` (root) | Base TypeScript config for the whole repo |
85+
| `components/<name>/tsconfig.json` | Per-component TS config extending root |
86+
| `.eslintrc.js` | Added TypeScript override block for `.ts`/`.tsx` files |
87+
| `scripts/ts-check.js` | Unified feedback pipeline (tsc + eslint + prettier) |
88+
| `scripts/post-build-rename.js` | Renames `.tsx`/`.ts``.js` in build output |
8389

8490
## Dev Dependencies Added
8591

86-
- `typescript` ~5.4.5
87-
- `@typescript-eslint/parser` ^6
88-
- `@typescript-eslint/eslint-plugin` ^6
89-
- `eslint-import-resolver-typescript` ^3
92+
- `typescript` ~5.4.5
93+
- `@typescript-eslint/parser` ^6
94+
- `@typescript-eslint/eslint-plugin` ^6
95+
- `eslint-import-resolver-typescript` ^3
9096

9197
## Migration Order Recommendation
9298

@@ -103,6 +109,6 @@ Start with leaf components (no internal deps) and work up:
103109

104110
## Notes
105111

106-
- Stories and E2E feature files can stay as `.js` — they don't need to be migrated immediately.
107-
- The `types/index.d.ts` hand-written files can eventually be replaced by auto-generated declarations from `tsc --declaration`.
108-
- The `import/extensions` ESLint rule is disabled for `.ts`/`.tsx` files because the codebase convention is to import with `.js` extensions (which Babel resolves to the actual `.tsx` files).
112+
- Stories and E2E feature files can stay as `.js` — they don't need to be migrated immediately.
113+
- The `types/index.d.ts` hand-written files can eventually be replaced by auto-generated declarations from `tsc --declaration`.
114+
- The `import/extensions` ESLint rule is disabled for `.ts`/`.tsx` files because the codebase convention is to import with `.js` extensions (which Babel resolves to the actual `.tsx` files).

collections/forms/i18n/en.pot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ msgstr ""
55
"Content-Type: text/plain; charset=utf-8\n"
66
"Content-Transfer-Encoding: 8bit\n"
77
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
8-
"POT-Creation-Date: 2026-01-12T11:22:32.560Z\n"
9-
"PO-Revision-Date: 2026-01-12T11:22:32.562Z\n"
8+
"POT-Creation-Date: 2026-04-13T21:27:29.445Z\n"
9+
"PO-Revision-Date: 2026-04-13T21:27:29.445Z\n"
1010

1111
msgid "Upload file"
1212
msgstr "Upload file"

components/alert/src/alert-bar/alert-bar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ const AlertBar = ({
9898
const pauseDisplayTimeout = () => {
9999
if (shouldAutoHide) {
100100
clearAllTimeouts()
101-
const elapsedTime = Date.now() - (displayStartTime.current as number)
101+
const elapsedTime =
102+
Date.now() - (displayStartTime.current as number)
102103
displayTimeRemaining.current =
103104
(displayTimeRemaining.current as number) - elapsedTime
104105
}

components/alert/src/alert-bar/icon.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ interface IconProps {
4747
warning?: boolean
4848
}
4949

50-
const Icon = ({ icon, success, warning, critical, info, dataTest }: IconProps) => {
50+
const Icon = ({
51+
icon,
52+
success,
53+
warning,
54+
critical,
55+
info,
56+
dataTest,
57+
}: IconProps) => {
5158
if (icon === false) {
5259
return null
5360
}

components/alert/tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"rootDir": "./src"
55
},
66
"include": ["src/**/*.ts", "src/**/*.tsx"],
7-
"exclude": ["node_modules", "build", "**/*.stories.*", "**/*.test.*", "**/*.e2e.*"]
7+
"exclude": [
8+
"node_modules",
9+
"build",
10+
"**/*.stories.*",
11+
"**/*.test.*",
12+
"**/*.e2e.*"
13+
]
814
}

components/box/tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"rootDir": "./src"
55
},
66
"include": ["src/**/*.ts", "src/**/*.tsx"],
7-
"exclude": ["node_modules", "build", "**/*.stories.*", "**/*.test.*", "**/*.e2e.*"]
7+
"exclude": [
8+
"node_modules",
9+
"build",
10+
"**/*.stories.*",
11+
"**/*.test.*",
12+
"**/*.e2e.*"
13+
]
814
}

components/button/src/button/button.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,28 @@ export interface ButtonProps {
6262
* Callback to trigger on de-focus (blur).
6363
* Called with same args as `onClick`
6464
*/
65-
onBlur?: (payload: ButtonCallbackPayload, event: React.FocusEvent<HTMLButtonElement>) => void
65+
onBlur?: (
66+
payload: ButtonCallbackPayload,
67+
event: React.FocusEvent<HTMLButtonElement>
68+
) => void
6669
/**
6770
* Callback to trigger on click.
6871
* Called with args `({ value, name }, event)`
6972
*/
70-
onClick?: (payload: ButtonCallbackPayload, event: React.MouseEvent<HTMLButtonElement>) => void
73+
onClick?: (
74+
payload: ButtonCallbackPayload,
75+
event: React.MouseEvent<HTMLButtonElement>
76+
) => void
7177
/** Callback to trigger on focus. Called with same args as `onClick` */
72-
onFocus?: (payload: ButtonCallbackPayload, event: React.FocusEvent<HTMLButtonElement>) => void
78+
onFocus?: (
79+
payload: ButtonCallbackPayload,
80+
event: React.FocusEvent<HTMLButtonElement>
81+
) => void
7382
/** Callback to trigger on key-down. Called with same args as `onClick` */
74-
onKeyDown?: (payload: ButtonCallbackPayload, event: React.KeyboardEvent<HTMLButtonElement>) => void
83+
onKeyDown?: (
84+
payload: ButtonCallbackPayload,
85+
event: React.KeyboardEvent<HTMLButtonElement>
86+
) => void
7587
[key: string]: unknown
7688
}
7789

@@ -107,7 +119,10 @@ export const Button = ({
107119
}
108120
}, [initialFocus, ref.current])
109121

110-
const { 'aria-label': ariaLabel, title } = otherProps as Record<string, unknown>
122+
const { 'aria-label': ariaLabel, title } = otherProps as Record<
123+
string,
124+
unknown
125+
>
111126

112127
if (!children && !title && !ariaLabel) {
113128
console.debug(

components/button/src/dropdown-button/__tests__/dropdown-button.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react'
44
import { mount } from 'enzyme'
55
import React from 'react'
66
import { act } from 'react-dom/test-utils'
7-
import { Modal } from '../../../../modal/src/modal/modal.js'
7+
import { Modal } from '../../../../modal/src/modal/modal.tsx'
88
import { Button } from '../../index.ts'
99
import { DropdownButton } from '../dropdown-button.tsx'
1010

components/button/src/dropdown-button/dropdown-button.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,20 @@ export interface DropdownButtonProps {
103103
* Called with signature `({ name: string, value: string, open: bool }, event)`
104104
* Is required when using the `open` prop to override the internal state.
105105
*/
106-
onClick?: (payload: DropdownButtonCallbackPayload, event: React.MouseEvent<HTMLButtonElement> | React.SyntheticEvent) => void
106+
onClick?: (
107+
payload: DropdownButtonCallbackPayload,
108+
event: React.MouseEvent<HTMLButtonElement> | React.SyntheticEvent
109+
) => void
107110
}
108111

109112
interface DropdownButtonState {
110113
open: boolean
111114
}
112115

113-
class DropdownButton extends Component<DropdownButtonProps, DropdownButtonState> {
116+
class DropdownButton extends Component<
117+
DropdownButtonProps,
118+
DropdownButtonState
119+
> {
114120
state: DropdownButtonState = {
115121
open: false,
116122
}
@@ -137,7 +143,10 @@ class DropdownButton extends Component<DropdownButtonProps, DropdownButtonState>
137143
}
138144
}
139145

140-
onClickHandler = ({ name, value }: { name?: string; value?: string }, event: React.MouseEvent<HTMLButtonElement> | React.SyntheticEvent) => {
146+
onClickHandler = (
147+
{ name, value }: { name?: string; value?: string },
148+
event: React.MouseEvent<HTMLButtonElement> | React.SyntheticEvent
149+
) => {
141150
const handleClick = (open: boolean) => {
142151
if (this.props.onClick) {
143152
this.props.onClick(
@@ -210,7 +219,14 @@ class DropdownButton extends Component<DropdownButtonProps, DropdownButtonState>
210219
</Button>
211220

212221
{open && (
213-
<Layer onBackdropClick={(_payload, event) => this.onClickHandler({}, event as unknown as React.SyntheticEvent)}>
222+
<Layer
223+
onBackdropClick={(_payload, event) =>
224+
this.onClickHandler(
225+
{},
226+
event as unknown as React.SyntheticEvent
227+
)
228+
}
229+
>
214230
<Popper
215231
dataTest={`${dataTest}-popper`}
216232
placement="bottom-start"

0 commit comments

Comments
 (0)