Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
0aba77e
feat: add typescript migration infrastructure and convert tag component
devin-ai-integration[bot] Apr 13, 2026
430d2c0
feat: migrate required component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
fb15030
feat: migrate help component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
36ddce8
feat: migrate box component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
7d65cb9
feat: migrate divider component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
e996479
feat: migrate cover component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
0223f30
fix: handle missing index files in storybook webpack config for TS mi…
devin-ai-integration[bot] Apr 13, 2026
a4818e4
feat: migrate portal component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
1fb1c6e
feat: migrate center component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
5a02743
feat: migrate logo component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
ceab4b1
feat: migrate chip component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
e2a50d9
feat: migrate user-avatar component to TypeScript (#9)
devin-ai-integration[bot] Apr 13, 2026
2286a8d
fix: update test file imports after user-avatar TypeScript migration …
devin-ai-integration[bot] Apr 13, 2026
f0fef09
feat: migrate intersection-detector component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
1972451
feat: migrate css component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
16b726d
feat: migrate notice-box component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
fc32584
feat: migrate radio component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
31e7f5d
fix: update test file imports after radio TypeScript migration
devin-ai-integration[bot] Apr 13, 2026
b209667
fix: update test file imports after notice-box TypeScript migration
devin-ai-integration[bot] Apr 13, 2026
74388a4
feat: migrate popper component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
e37c8ea
feat: migrate status-icon component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
8cabab3
feat: migrate segmented-control component to TypeScript (#17)
devin-ai-integration[bot] Apr 13, 2026
d90b42b
feat: migrate label component to TypeScript (#18)
devin-ai-integration[bot] Apr 13, 2026
b63109b
feat: migrate card component to TypeScript (#20)
devin-ai-integration[bot] Apr 13, 2026
28038df
feat: migrate legend component to TypeScript (#21)
devin-ai-integration[bot] Apr 13, 2026
3ff3f1a
feat: migrate pagination component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
fbbe531
feat: migrate layer component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
b8e422f
feat: migrate loader component to TypeScript (#22)
devin-ai-integration[bot] Apr 13, 2026
802ada6
feat: migrate field component to TypeScript (#26)
devin-ai-integration[bot] Apr 13, 2026
c86ede5
feat: migrate popover component to TypeScript (#24)
devin-ai-integration[bot] Apr 13, 2026
5a8de32
feat: migrate node component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
212b856
feat: migrate text-area component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
d8f9f96
feat: migrate menu component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
b1c8700
feat: migrate switch component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
2d94c60
feat: migrate modal component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
5f42d45
feat: migrate header-bar component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
0ee47fd
fix: use proper runtime Number() conversion for tabIndex instead of u…
devin-ai-integration[bot] Apr 13, 2026
1628c66
feat: migrate input component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
bab5f84
fix: restore empty array initializer for openSubMenus state
devin-ai-integration[bot] Apr 13, 2026
e5caa13
feat: migrate calendar component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
069de11
feat: migrate transfer component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
b33a071
feat: migrate alert component to TypeScript (#28)
devin-ai-integration[bot] Apr 13, 2026
51918f5
feat: migrate table component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
d23fa1e
feat: migrate file-input component to TypeScript (#36)
devin-ai-integration[bot] Apr 13, 2026
a920955
fix: keep tabIndex as string type with Number() conversion at DOM bou…
devin-ai-integration[bot] Apr 13, 2026
1e0d3d9
fix: align onClose type with LayerBackdropClickHandler and restore di…
devin-ai-integration[bot] Apr 13, 2026
63a7af8
feat: migrate sharing-dialog component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
b766022
feat: migrate tooltip component to TypeScript (#37)
devin-ai-integration[bot] Apr 13, 2026
be1f128
fix: restore elevations.popover constant instead of hardcoded box-shadow
devin-ai-integration[bot] Apr 13, 2026
edc8ad6
feat: migrate button component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
d1078b8
feat: migrate organisation-unit-tree component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
9a55c8c
feat: migrate selector-bar component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
86452a1
fix: update jest.mock paths to use .ts extensions
devin-ai-integration[bot] Apr 13, 2026
4e50120
feat: migrate checkbox component to TypeScript (#32)
devin-ai-integration[bot] Apr 13, 2026
8c56696
fix: make onClose event param optional and remove unsafe KeyboardEven…
devin-ai-integration[bot] Apr 13, 2026
c43c209
fix: preserve original column fallback behavior in StackedTableCell
devin-ai-integration[bot] Apr 13, 2026
7729fde
feat: migrate select component to TypeScript
devin-ai-integration[bot] Apr 13, 2026
783be81
fix: remove id placeholder from mutation to preserve API URL
devin-ai-integration[bot] Apr 13, 2026
d4d88cc
fix: remove typescript from production dependencies
devin-ai-integration[bot] Apr 13, 2026
1e099c2
fix: update jest.mock path in simple-single-select-field test
devin-ai-integration[bot] Apr 13, 2026
cf97ead
feat: migrate tab component to TypeScript (#33)
devin-ai-integration[bot] Apr 13, 2026
d65da51
fix: correct e2e story imports from index.tsx to index.ts
devin-ai-integration[bot] Apr 13, 2026
c5f6f87
fix: resolve CI failures in TypeScript migration (#47)
devin-ai-integration[bot] Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35 changes: 35 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ const config = {
'import/no-extraneous-dependencies': 'error',
},
},
// TypeScript overrides for migrated components
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
settings: {
'import/resolver': {
typescript: {
project: './tsconfig.json',
},
},
},
rules: {
// Disable JS-only rules that conflict with TS
'react/prop-types': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
// Keep consistent with existing code style
'react/no-unknown-property': [
'error',
{ ignore: ['jsx', 'global'] },
],
// Allow .js extension imports in TS files (Babel resolves .tsx -> .js)
'import/extensions': 'off',
},
},
],
}

Expand Down
114 changes: 114 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# dhis2/ui — Incremental TypeScript Migration Strategy

## Overview

This document describes the approach for incrementally migrating `@dhis2/ui` components from JavaScript to TypeScript, one component at a time, without breaking the rest of the library.

## Key Findings

- **Build system**: `d2-app-scripts build` uses Babel with `@babel/preset-typescript` already included — it can compile `.ts`/`.tsx` out of the box.
- **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.
- **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`.
- **ESLint**: The project uses `@dhis2/cli-style` (ESLint 7.x). TypeScript linting works via `@typescript-eslint/parser` v6 + `@typescript-eslint/eslint-plugin` v6.
- **One quirk**: `d2-app-scripts build` doesn't rename `.tsx` → `.js` in build output. A `post-build-rename.js` script handles this.

## How to Migrate a Component

### 1. Add a `tsconfig.json` to the component

```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"node_modules",
"build",
"**/*.stories.*",
"**/*.test.*",
"**/*.e2e.*"
]
}
```

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

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

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

```js
module.exports = {
type: 'lib',
entryPoints: {
lib: 'src/index.ts', // was src/index.js
},
}
```

### 4. Update `package.json` build script

```json
"build": "d2-app-scripts build && node ../../scripts/post-build-rename.js"
```

### 5. Delete old `.js` source files

Remove the original `.js` files that were converted (keep stories/features as JS).

### 6. Run the feedback pipeline

```bash
node scripts/ts-check.js <component-name> # check one component
node scripts/ts-check.js --all # check all migrated components
node scripts/ts-check.js <component-name> --fix # auto-fix lint + format
```

### 7. Verify the build

```bash
cd components/<name> && yarn build
```

## Files Added/Modified

| File | Purpose |
| --------------------------------- | ------------------------------------------------------ |
| `tsconfig.json` (root) | Base TypeScript config for the whole repo |
| `components/<name>/tsconfig.json` | Per-component TS config extending root |
| `.eslintrc.js` | Added TypeScript override block for `.ts`/`.tsx` files |
| `scripts/ts-check.js` | Unified feedback pipeline (tsc + eslint + prettier) |
| `scripts/post-build-rename.js` | Renames `.tsx`/`.ts` → `.js` in build output |

## Dev Dependencies Added

- `typescript` ~5.4.5
- `@typescript-eslint/parser` ^6
- `@typescript-eslint/eslint-plugin` ^6
- `eslint-import-resolver-typescript` ^3

## Migration Order Recommendation

Start with leaf components (no internal deps) and work up:

1. **Simple leaf**: `divider`, `cover`, `center`, `required`, `help`, `legend`, `label`
2. **Small with sub-components**: `tag`, `chip`, `card`, `box`, `status-icon`, `user-avatar`, `logo`
3. **Medium**: `loader`, `notice-box`, `alert`, `tooltip`, `popover`, `popper`
4. **Complex**: `button`, `checkbox`, `radio`, `switch`, `input`, `text-area`, `file-input`
5. **Field wrappers**: `field`
6. **Composite**: `select`, `menu`, `tab`, `table`, `pagination`, `modal`, `calendar`
7. **Large/complex**: `header-bar`, `organisation-unit-tree`, `transfer`, `sharing-dialog`, `selector-bar`
8. **Collections**: `collections/ui`, `collections/forms`

## Notes

- Stories and E2E feature files can stay as `.js` — they don't need to be migrated immediately.
- The `types/index.d.ts` hand-written files can eventually be replaced by auto-generated declarations from `tsc --declaration`.
- 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).
4 changes: 2 additions & 2 deletions collections/forms/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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: 2026-01-12T11:22:32.560Z\n"
"PO-Revision-Date: 2026-01-12T11:22:32.562Z\n"
"POT-Creation-Date: 2026-04-13T21:27:29.445Z\n"
"PO-Revision-Date: 2026-04-13T21:27:29.445Z\n"

msgid "Upload file"
msgstr "Upload file"
Expand Down
2 changes: 1 addition & 1 deletion components/alert/d2.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
type: 'lib',
entryPoints: {
lib: 'src/index.js',
lib: 'src/index.ts',
},
}
3 changes: 2 additions & 1 deletion components/alert/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
},
"scripts": {
"start": "storybook dev -c ../../storybook/config --port 5000",
"build": "d2-app-scripts build",
"build": "d2-app-scripts build && node ../../scripts/post-build-rename.js",
"typecheck": "tsc --noEmit",
"test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js"
},
"peerDependencies": {
Expand Down
37 changes: 0 additions & 37 deletions components/alert/src/alert-bar/action.js

This file was deleted.

36 changes: 36 additions & 0 deletions components/alert/src/alert-bar/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { spacers } from '@dhis2/ui-constants'
import React, { Component } from 'react'

export interface ActionProps {
dataTest: string
hide: (event: React.MouseEvent<HTMLSpanElement>) => void
label: string
onClick: (event: React.MouseEvent<HTMLSpanElement>) => void
}

class Action extends Component<ActionProps> {
onClick = (event: React.MouseEvent<HTMLSpanElement>) => {
this.props.onClick(event)
this.props.hide(event)
}

render() {
return (
<span onClick={this.onClick} data-test={this.props.dataTest}>
{this.props.label}
<style jsx>{`
span {
margin-inline-end: ${spacers.dp12};
text-decoration: underline;
white-space: nowrap;
}
span:hover {
cursor: pointer;
}
`}</style>
</span>
)
}
}

export { Action }
47 changes: 0 additions & 47 deletions components/alert/src/alert-bar/actions.js

This file was deleted.

41 changes: 41 additions & 0 deletions components/alert/src/alert-bar/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { spacers } from '@dhis2/ui-constants'
import React from 'react'
import { Action } from './action.tsx'

export interface AlertBarAction {
label: string
onClick: (event: React.MouseEvent<HTMLSpanElement>) => void
}

interface ActionsProps {
dataTest: string
hide: (event: React.MouseEvent<HTMLSpanElement>) => void
actions?: AlertBarAction[]
}

const Actions = ({ actions, hide, dataTest }: ActionsProps) => {
if (!actions) {
return null
}

return (
<div>
{actions.map((action) => (
<Action
key={action.label}
{...action}
hide={hide}
dataTest={`${dataTest}-action`}
/>
))}
<style jsx>{`
div {
margin-inline-start: ${spacers.dp48};
margin-inline-end: -${spacers.dp12};
}
`}</style>
</div>
)
}

export { Actions }
2 changes: 1 addition & 1 deletion components/alert/src/alert-bar/alert-bar.e2e.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { AlertBar } from './index.js'
import { AlertBar } from './index.ts'

window.onHidden = window.Cypress && window.Cypress.cy.stub()

Expand Down
Loading
Loading