@lowdefy/codemods 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "5.1.0",
|
|
5
|
+
"from": ">=5.0.0 <5.1.0",
|
|
6
|
+
"description": "AgGrid built-in cell renderer types (tag, avatar, link, date, boolean, progress, number) + ellipsis + align",
|
|
7
|
+
"codemods": [
|
|
8
|
+
{
|
|
9
|
+
"id": "aggrid-cell-types-migration",
|
|
10
|
+
"description": "Report-only: detect cellRenderer HTML patterns that could migrate to cell.type. Each column requires human review and author sign-off.",
|
|
11
|
+
"path": "v5-1-0/01-aggrid-cell-types-migration.md"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
3
15
|
{
|
|
4
16
|
"version": "5.0.0",
|
|
5
17
|
"from": ">=4.0.0 <5.0.0",
|
|
@@ -124,6 +136,11 @@
|
|
|
124
136
|
"id": "page-layout-theme-removal",
|
|
125
137
|
"description": "Remove header.theme, sider.theme, menu.theme from page layout blocks and migrate nested style props",
|
|
126
138
|
"path": "v5-0-0/24-page-layout-theme-removal.md"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "plugin-peer-dependencies",
|
|
142
|
+
"description": "Move react, react-dom, and antd from dependencies to peerDependencies in custom block plugins",
|
|
143
|
+
"path": "v5-0-0/25-plugin-peer-dependencies.md"
|
|
127
144
|
}
|
|
128
145
|
]
|
|
129
146
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Migration: Plugin `react`, `react-dom`, and `antd` to Peer Dependencies
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
In a pnpm monorepo, if a custom block plugin declares `react` or `react-dom` as direct dependencies (even with a range like `^18` or `^19`), pnpm may install a **second React instance** in the store. The Lowdefy dev server pins its own React version, and when plugin components resolve to a different copy, **two React context trees** exist at runtime. This breaks:
|
|
6
|
+
|
|
7
|
+
- **antd theme inheritance** — dark mode, compact algorithm, and design tokens from `ConfigProvider` are invisible to plugin blocks
|
|
8
|
+
- **React hooks** — hooks called in the plugin's React copy throw "Invalid hook call" or silently malfunction
|
|
9
|
+
- **React context** — any context (including antd's internal contexts) shared between the host app and plugin blocks is split across instances
|
|
10
|
+
|
|
11
|
+
The fix is to declare `react`, `react-dom`, and `antd` as **peer dependencies** in plugin packages, and optionally use `pnpm.overrides` in the root `package.json` to force a single version across the workspace.
|
|
12
|
+
|
|
13
|
+
This migration targets `package.json` files in custom block plugin directories, not YAML configs.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
`plugins` — scan `package.json` files in local block plugin directories.
|
|
18
|
+
|
|
19
|
+
## What to Do
|
|
20
|
+
|
|
21
|
+
### 1. Move `react` and `react-dom` from `dependencies` to `peerDependencies`
|
|
22
|
+
|
|
23
|
+
For each custom block plugin `package.json`, remove `react` and `react-dom` from `dependencies` and add them to `peerDependencies` with an open range:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18",
|
|
29
|
+
"react-dom": ">=18"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Do **not** pin a specific version (e.g., `"18.2.0"` or `"^19.0.0"`). The range `>=18` lets the plugin work with whatever React version the Lowdefy dev server provides.
|
|
35
|
+
|
|
36
|
+
### 2. Move `antd` from `dependencies` to `peerDependencies` (if present)
|
|
37
|
+
|
|
38
|
+
If the plugin imports from `antd` directly (e.g., `import { Tag } from 'antd'`), it should declare `antd` as a peer dependency:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"react-dom": ">=18",
|
|
45
|
+
"antd": ">=5"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the plugin only imports from `@lowdefy/blocks-antd` or other `@lowdefy/*` packages (not `antd` itself), antd does not need to be listed.
|
|
51
|
+
|
|
52
|
+
**Check which plugins import antd directly:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
grep -rn "from 'antd'" plugins/*/src/ --include='*.js'
|
|
56
|
+
grep -rn "from \"antd\"" plugins/*/src/ --include='*.js'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Add `pnpm.overrides` to the root `package.json`
|
|
60
|
+
|
|
61
|
+
In the monorepo root `package.json`, add `pnpm.overrides` to force a single React version across all workspace packages:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"pnpm": {
|
|
66
|
+
"overrides": {
|
|
67
|
+
"react": "18.2.0",
|
|
68
|
+
"react-dom": "18.2.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This ensures that even if transitive dependencies pull in a different React version, pnpm collapses them all to one copy.
|
|
75
|
+
|
|
76
|
+
**Choose the version that the Lowdefy dev server uses.** As of Lowdefy v5, this is `18.2.0`.
|
|
77
|
+
|
|
78
|
+
### 4. Run `pnpm install`
|
|
79
|
+
|
|
80
|
+
Regenerate the lockfile to collapse duplicate installations:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 5. Verify single React instance
|
|
87
|
+
|
|
88
|
+
Check that all plugin symlinks resolve to the same React copy:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
readlink plugins/*/node_modules/react
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
All paths should point to the same `react@18.2.0` store entry.
|
|
95
|
+
|
|
96
|
+
## Files to Check
|
|
97
|
+
|
|
98
|
+
Glob: `plugins/*/package.json`
|
|
99
|
+
|
|
100
|
+
Grep patterns:
|
|
101
|
+
|
|
102
|
+
- `"react":` in `dependencies` — should only appear in `peerDependencies` for plugin packages
|
|
103
|
+
- `"react-dom":` in `dependencies` — same
|
|
104
|
+
- `"antd":` in `dependencies` — move to `peerDependencies` if plugin imports antd directly
|
|
105
|
+
|
|
106
|
+
Also check non-plugin packages that might pull in a competing React version:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
grep -rn '"react":' */package.json --include='package.json'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If other non-plugin packages in the workspace have React as a direct dependency, consider aligning those versions too, or rely on `pnpm.overrides` to force alignment.
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
### Before — plugin `package.json`
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"name": "@my-org/plugin-blocks",
|
|
121
|
+
"dependencies": {
|
|
122
|
+
"@lowdefy/block-utils": "5.0.0",
|
|
123
|
+
"@lowdefy/helpers": "5.0.0",
|
|
124
|
+
"antd": "4.22.5",
|
|
125
|
+
"react": "18.2.0",
|
|
126
|
+
"react-dom": "18.2.0"
|
|
127
|
+
},
|
|
128
|
+
"devDependencies": {
|
|
129
|
+
"@swc/cli": "0.1.57",
|
|
130
|
+
"@swc/core": "1.2.194"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### After — plugin `package.json`
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"name": "@my-org/plugin-blocks",
|
|
140
|
+
"dependencies": {
|
|
141
|
+
"@lowdefy/block-utils": "5.0.0",
|
|
142
|
+
"@lowdefy/helpers": "5.0.0"
|
|
143
|
+
},
|
|
144
|
+
"peerDependencies": {
|
|
145
|
+
"react": ">=18",
|
|
146
|
+
"react-dom": ">=18",
|
|
147
|
+
"antd": ">=5"
|
|
148
|
+
},
|
|
149
|
+
"devDependencies": {
|
|
150
|
+
"@swc/cli": "0.1.57",
|
|
151
|
+
"@swc/core": "1.2.194"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Before — root `package.json` (no overrides)
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"name": "my-lowdefy-app",
|
|
161
|
+
"private": true,
|
|
162
|
+
"packageManager": "pnpm@8.15.4"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### After — root `package.json` (with overrides)
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"name": "my-lowdefy-app",
|
|
171
|
+
"private": true,
|
|
172
|
+
"packageManager": "pnpm@8.15.4",
|
|
173
|
+
"pnpm": {
|
|
174
|
+
"overrides": {
|
|
175
|
+
"react": "18.2.0",
|
|
176
|
+
"react-dom": "18.2.0"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Edge Cases
|
|
183
|
+
|
|
184
|
+
- **Non-plugin packages** (APIs, lambdas, Next.js apps): These are standalone apps that bundle their own React. They should keep `react` and `react-dom` as direct dependencies. The `pnpm.overrides` in the root ensures they still use the same version.
|
|
185
|
+
- **Plugins without blocks** (auth adapters, operators, connections): If a plugin doesn't render React components, it doesn't need `react` or `react-dom` at all — neither as dependencies nor peer dependencies. Only block plugins need this migration.
|
|
186
|
+
- **`@monaco-editor/react` and similar**: Libraries that have `react` as a peer dependency themselves work correctly when the plugin also uses peer dependencies — pnpm resolves them all to the same overridden version.
|
|
187
|
+
- **Transitive React dependencies**: If another workspace package depends on a library that has its own React peer dependency (e.g., an email rendering library, a rich text editor), the `pnpm.overrides` will force alignment. Verify the library accepts the pinned version before upgrading.
|
|
188
|
+
- **Future React 19 upgrade**: When Lowdefy upgrades to React 19, update the `pnpm.overrides` version. The plugin peer dep ranges (`>=18`) will continue to work without changes.
|
|
189
|
+
- **`antd` version**: In Lowdefy v5, antd is v6.x. Plugins that import antd directly should use `"antd": ">=5"` as the peer dep range (not `>=4`) to prevent accidentally resolving to antd v4 which has incompatible APIs.
|
|
190
|
+
|
|
191
|
+
## Verification
|
|
192
|
+
|
|
193
|
+
1. No plugin should have `react` or `react-dom` in `dependencies`:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Should return no matches in plugin package.json files
|
|
197
|
+
grep -A1 '"dependencies"' plugins/*/package.json | grep '"react"'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
2. All plugins with block exports should have `react` and `react-dom` in `peerDependencies`:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
grep -A3 '"peerDependencies"' plugins/*/package.json | grep '"react"'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
3. Root `package.json` should have `pnpm.overrides`:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
grep -A4 '"overrides"' package.json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
4. After `pnpm install`, all plugins resolve to the same React version:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
readlink plugins/*/node_modules/react
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
5. Build each plugin and start the dev server — blocks should inherit the antd theme (dark mode toggle, compact algorithm, design tokens)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Migration: Convert AgGrid cellRenderers to built-in `cell.type`
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
`@lowdefy/blocks-aggrid@5.1` introduces first-class column cell renderers that cover the patterns apps hand-build today with `_function` + `__nunjucks` + HTML strings:
|
|
6
|
+
|
|
7
|
+
| `cell.type` | Replaces |
|
|
8
|
+
| ----------- | ----------------------------------------------------------------- |
|
|
9
|
+
| `tag` | Coloured pill HTML via `color-mix`, antd Tag-style badges |
|
|
10
|
+
| `avatar` | Circle image/initials + name in a flex row |
|
|
11
|
+
| `link` | Hand-built `<a href="/pageId?…">` anchors |
|
|
12
|
+
| `date` | `__dayjs.format` / `__moment.format` inside `cellRenderer` |
|
|
13
|
+
| `boolean` | Conditional coloured "Yes/No" / "Verified/Unverified" text |
|
|
14
|
+
| `progress` | Conditional-colour percent pill (5-tier scale) |
|
|
15
|
+
| `number` | `valueFormatter` + `__string.concat` for currency/percent/compact |
|
|
16
|
+
|
|
17
|
+
Plus two column-level helpers:
|
|
18
|
+
|
|
19
|
+
- `ellipsis: N` — replaces `.ellipsis-N` class + manual `wrapText` / `autoHeight`.
|
|
20
|
+
- `cell.align: left | center | right` — replaces manual `cellStyle.justifyContent` + `headerClass`.
|
|
21
|
+
|
|
22
|
+
## Critical: this is a **report-only** codemod
|
|
23
|
+
|
|
24
|
+
**Do not auto-rewrite any column.** Every match below is a _candidate_ for conversion. Each candidate must be:
|
|
25
|
+
|
|
26
|
+
1. **Reviewed against its data shape.** The replacement reads from a specific field (or row-data path); the original cellRenderer may have computed, fallen-back, or reshaped data inline. If the aggregation/request doesn't already deliver the exact shape the built-in renderer expects, the column either needs a projection change in the request/aggregation, or the original `_function` cellRenderer should stay.
|
|
27
|
+
2. **Reviewed against its enum / colour lookup.** Many tag cells read colour from `__get: { key: __args: 0.data.x, from: __global: enums.y }` — this can migrate to `cell.colorFrom: x.color` **only if** the aggregation already hydrates `data.x.color`. If the colour lookup happens inside the cellRenderer template, the request has to start returning the resolved colour, or the tag's `colorMap` has to be baked into the column config.
|
|
28
|
+
3. **Confirmed with the column author.** Post each proposed change to the author of the table (or the nearest code owner) and get explicit sign-off before applying. Do not batch multiple columns into a single review.
|
|
29
|
+
|
|
30
|
+
The codemod runner should produce a **report with one entry per column matched**, including:
|
|
31
|
+
|
|
32
|
+
- File path + line number of the column defintion.
|
|
33
|
+
- The full existing cellRenderer snippet (for context).
|
|
34
|
+
- The proposed `cell.type` replacement.
|
|
35
|
+
- The **data contract** the replacement requires (fields read, shape expected).
|
|
36
|
+
- A checkbox / flag for the reviewer to approve or reject the individual column.
|
|
37
|
+
|
|
38
|
+
## Files to Search
|
|
39
|
+
|
|
40
|
+
Glob: `**/*.{yaml,yml,yaml.njk}`
|
|
41
|
+
|
|
42
|
+
Narrow to files containing AgGrid blocks first:
|
|
43
|
+
|
|
44
|
+
Grep: `type:\s*AgGrid(Alpine|Balham|Material|InputAlpine|InputBalham|InputMaterial)`
|
|
45
|
+
|
|
46
|
+
Then inside each matched file, look for `columnDefs:` entries.
|
|
47
|
+
|
|
48
|
+
## Detection Patterns
|
|
49
|
+
|
|
50
|
+
Each pattern below lists:
|
|
51
|
+
|
|
52
|
+
- A recognisable HTML / operator signature.
|
|
53
|
+
- The proposed `cell.type` replacement.
|
|
54
|
+
- The **questions the reviewer must answer** before approving.
|
|
55
|
+
|
|
56
|
+
### 1. Tag / badge pill
|
|
57
|
+
|
|
58
|
+
**Signature — the 12% / 30% `color-mix` convention:**
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
px-1.5 py-0.5 rounded text-\[10px\] font-semibold.*color-mix\(in srgb,\{\{\s*color\s*\}\}\s+12%
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Also matches variants:
|
|
65
|
+
|
|
66
|
+
- `color-mix(in srgb, {{ status.color }} 12%, transparent)`
|
|
67
|
+
- Inline `<span style="background:...;border:1px solid ...;color:...;">`
|
|
68
|
+
- Literal hex colours inside the style attribute.
|
|
69
|
+
|
|
70
|
+
**Proposed replacement:**
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
cell:
|
|
74
|
+
type: tag
|
|
75
|
+
colorFrom: <row.data path to colour> # when colour is on the row
|
|
76
|
+
# OR
|
|
77
|
+
colorMap: # when colour is per-enum-value
|
|
78
|
+
<value_1>: <colour>
|
|
79
|
+
<value_2>: <colour>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Reviewer must answer:**
|
|
83
|
+
|
|
84
|
+
- Where does the colour come from in the original — a global enum lookup, or a field on the row?
|
|
85
|
+
- Is the coloured value the cell's raw value, or a nested `title` / `label` field?
|
|
86
|
+
- Are there multiple fallback colours (e.g. `default` when enum lookup fails)? Map each to `colorMap.default` or the renderer's own default.
|
|
87
|
+
- Does the rendered text come straight from the cell value, or is it `value.title` / `data.xxx.label`? If not direct, the `field` on the column must point at the displayed text.
|
|
88
|
+
|
|
89
|
+
### 2. Avatar + name
|
|
90
|
+
|
|
91
|
+
**Signature:**
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
display:\s*inline-flex.*border-radius:\s*50%.*<img.*width="3[02]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Also matches:
|
|
98
|
+
|
|
99
|
+
- Gray circle fallback with unicode 👤 or similar.
|
|
100
|
+
- `flex-shrink: 0`, `margin-right: 8px`.
|
|
101
|
+
- An `<a>` wrapping the image or name.
|
|
102
|
+
|
|
103
|
+
**Proposed replacement:**
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
cell:
|
|
107
|
+
type: avatar
|
|
108
|
+
nameField: <path> # row-data path for the visible name
|
|
109
|
+
srcField: <path> # row-data path for picture src (optional)
|
|
110
|
+
idField: <path> # seeds initials colour
|
|
111
|
+
link: # only if the original wrapped a link
|
|
112
|
+
pageId: <target>
|
|
113
|
+
urlQuery:
|
|
114
|
+
<key>: <row-data-path>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Reviewer must answer:**
|
|
118
|
+
|
|
119
|
+
- Where does the name string live? (`profile.name`, `assignee.full_name`, …)
|
|
120
|
+
- Where does the picture URL live, and is it nullable? (The built-in renders initials on null.)
|
|
121
|
+
- If wrapped in a link — what `pageId` and what `urlQuery` keys?
|
|
122
|
+
- Is there a distinct `id` for initials-colour seeding, or should it fall back to the name?
|
|
123
|
+
- If the initials fallback colour scheme in the original is brand-specific, it **doesn't** match the built-in scheme — skip conversion unless OK to change.
|
|
124
|
+
|
|
125
|
+
### 3. Link-styled cell
|
|
126
|
+
|
|
127
|
+
**Signature:**
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
cellRenderer:\s*\n\s*_function:\s*\n\s*__nunjucks:\s*\n.*<a href
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Also matches:
|
|
134
|
+
|
|
135
|
+
- `__string.concat: ['<a href="/', __args: 0.data._id, ...]`
|
|
136
|
+
- Links using `_module.pageId` inside the template.
|
|
137
|
+
|
|
138
|
+
**Proposed replacement:**
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
cell:
|
|
142
|
+
type: link
|
|
143
|
+
pageId: <target>
|
|
144
|
+
urlQuery:
|
|
145
|
+
<key>: <row-data-path>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
And at the table level:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
events:
|
|
152
|
+
onCellLink:
|
|
153
|
+
- id: go
|
|
154
|
+
type: Link
|
|
155
|
+
params:
|
|
156
|
+
_event: link
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Reviewer must answer:**
|
|
160
|
+
|
|
161
|
+
- Does the link target a `pageId` or a full `href` (external URL)?
|
|
162
|
+
- What are the `urlQuery` keys and their row-data paths?
|
|
163
|
+
- Does the original open in a new tab (`target="_blank"`)? Use `newTab: true`.
|
|
164
|
+
- Is the link only shown conditionally (inside a `{% if %}`)? The built-in always renders; the conditional needs to move to the data layer (null value → null placeholder).
|
|
165
|
+
|
|
166
|
+
### 4. Date / timestamp
|
|
167
|
+
|
|
168
|
+
**Signature:**
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
cellRenderer:\s*\n\s*_function:\s*\n\s*__dayjs\.format:
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or its moment equivalent `__moment.format`. Also matches `__string.concat` pipes that include `| date(...)` Nunjucks filter.
|
|
175
|
+
|
|
176
|
+
**Proposed replacement:**
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
cell:
|
|
180
|
+
type: date
|
|
181
|
+
format: <format string> # defaults to 'YYYY-MM-DD HH:mm' — omit if using default
|
|
182
|
+
# relative: true # if the original used fromNow()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Reviewer must answer:**
|
|
186
|
+
|
|
187
|
+
- Is the value an ISO string, a `Date`, or a millisecond number? The built-in calls `dayjs(value)` — anything dayjs can parse is fine.
|
|
188
|
+
- Is the format the default `YYYY-MM-DD HH:mm`? If so, drop the explicit `format` key.
|
|
189
|
+
- Is the original using `fromNow()` / `humanize()` / a custom relative output? Use `relative: true`.
|
|
190
|
+
- Timezone handling — the built-in uses local time; if the original forced UTC, preserve it or convert before passing.
|
|
191
|
+
|
|
192
|
+
### 5. Boolean / Yes-No
|
|
193
|
+
|
|
194
|
+
**Signature:**
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
cellRenderer:\s*\n\s*_function:\s*\n\s*__nunjucks:.*\{%\s*if\s+\w+\s*%\}.*<span.*color:\s*#(52c41a|[0-9a-f]+).*Yes.*<span.*No
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Also matches patterns using `app_config.colors.success` / `var(--ant-color-success)`.
|
|
201
|
+
|
|
202
|
+
**Proposed replacement:**
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
cell:
|
|
206
|
+
type: boolean
|
|
207
|
+
# trueLabel / falseLabel / trueColor / falseColor — override only if the original deviates
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Reviewer must answer:**
|
|
211
|
+
|
|
212
|
+
- Are the labels literally "Yes" / "No", or something domain-specific (`Verified`/`Unverified`, `Active`/`Inactive`)?
|
|
213
|
+
- What are the colours? The built-in defaults to `--ant-color-success` / `--ant-color-text-quaternary`. If the original uses brand-specific tokens, set `trueColor` / `falseColor`.
|
|
214
|
+
- Does the original render `—` or something else for null? The built-in renders em-dash `—` — confirm that matches.
|
|
215
|
+
- Does the rendered text include an icon? (✓ ✗ 👍 etc.) — **skip conversion** unless the column owner approves text-only.
|
|
216
|
+
|
|
217
|
+
### 6. Progress / percentage
|
|
218
|
+
|
|
219
|
+
**Signature — 5-tier coloured pill:**
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
\{%\s*if\s+\w+\s*<\s*20.*#dc2f02.*%\}.*\{%\s*elseif.*<\s*40.*#e85d04.*%\}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Also matches apps using `app_config.colors.progress_low` etc.
|
|
226
|
+
|
|
227
|
+
**Proposed replacement:**
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
cell:
|
|
231
|
+
type: progress
|
|
232
|
+
# thresholds: [20, 40, 60, 80] # defaults
|
|
233
|
+
# colors: [<five colour tokens>] # defaults to antd error → success scale
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Reviewer must answer:**
|
|
237
|
+
|
|
238
|
+
- Are the thresholds exactly `[20, 40, 60, 80]`? If not, set `thresholds`.
|
|
239
|
+
- Are the colours the same antd error → warning → gold → info → success? If the original used brand colours, set `colors` array (length = thresholds.length + 1).
|
|
240
|
+
- Is the value already a 0–100 number, or a 0–1 fraction? If fraction, either multiply in the aggregation or use `cell.type: number` with `format: percent` instead.
|
|
241
|
+
- Is there a null / unstarted label other than "None"? Use `nullLabel`.
|
|
242
|
+
|
|
243
|
+
### 7. Number formatting
|
|
244
|
+
|
|
245
|
+
**Signature:**
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
valueFormatter:\s*\n\s*_function:\s*\n.*__string\.concat:
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Or manual `Intl.NumberFormat` / `toLocaleString` usage, or hand-built percent with `%` suffix.
|
|
252
|
+
|
|
253
|
+
**Proposed replacement — pick by shape:**
|
|
254
|
+
|
|
255
|
+
```yaml
|
|
256
|
+
# Currency
|
|
257
|
+
cell:
|
|
258
|
+
type: number
|
|
259
|
+
format: currency
|
|
260
|
+
currency: USD
|
|
261
|
+
decimals: 0
|
|
262
|
+
|
|
263
|
+
# Accounting (negatives in parens, green/red)
|
|
264
|
+
cell:
|
|
265
|
+
type: number
|
|
266
|
+
format: currency
|
|
267
|
+
currency: USD
|
|
268
|
+
decimals: 2
|
|
269
|
+
negative: parentheses
|
|
270
|
+
signColor: true
|
|
271
|
+
|
|
272
|
+
# Percent
|
|
273
|
+
cell:
|
|
274
|
+
type: number
|
|
275
|
+
format: percent
|
|
276
|
+
decimals: 1
|
|
277
|
+
|
|
278
|
+
# Compact (K/M/B)
|
|
279
|
+
cell:
|
|
280
|
+
type: number
|
|
281
|
+
format: compact
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Reviewer must answer:**
|
|
285
|
+
|
|
286
|
+
- Is the raw value the final number, or a string (e.g. `'12,345.67'`)? `cell.type: number` expects a number — coerce in the aggregation if needed.
|
|
287
|
+
- For percent: is the field a fraction (0.184) or a 0-100 integer (18.4)? `Intl.NumberFormat` with `style: 'percent'` **multiplies by 100** — adjust the field or use `format: number` + `suffix: '%'`.
|
|
288
|
+
- For currency: which currency, locale, and decimal precision? Make sure the currency code is ISO 4217.
|
|
289
|
+
- Does the original right-align? The built-in auto-right-aligns numbers — confirm this matches the column's visual intent. Override via `align: left` if needed.
|
|
290
|
+
- Does the original colour negatives red / positives green? Set `signColor: true`.
|
|
291
|
+
|
|
292
|
+
### 8. Ellipsis / truncated text
|
|
293
|
+
|
|
294
|
+
**Signature:**
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
cellRenderer:\s*\n\s*_function:\s*\n\s*__string\.concat:\s*\n.*ellipsis-\d
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Also matches bare `class="ellipsis-4"` / `class="ellipsis-2"` patterns inside any cellRenderer.
|
|
301
|
+
|
|
302
|
+
**Proposed replacement:**
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
ellipsis: <N> # column-level; omit cellRenderer entirely
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
(Automatically sets `wrapText: true` + `autoHeight: true` + applies the line-clamp.)
|
|
309
|
+
|
|
310
|
+
**Reviewer must answer:**
|
|
311
|
+
|
|
312
|
+
- Is the content plain text, or HTML? The built-in ellipsis renderer treats the value as text — if the original included formatting, this path doesn't fit. Keep the custom renderer.
|
|
313
|
+
- Does the original wrap a **tag** or **link** inside the clamp? `ellipsis: N` only wraps plain values — for rich-cell clamping, skip this conversion.
|
|
314
|
+
- Line count: 1, 2, 3, 4, 5, or 6? Only those counts are supported.
|
|
315
|
+
|
|
316
|
+
### 9. Header & cell alignment
|
|
317
|
+
|
|
318
|
+
**Signature:**
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
headerClass:\s*ag-right-aligned-header
|
|
322
|
+
cellStyle:\s*\n.*justifyContent:\s*flex-end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Proposed replacement:**
|
|
326
|
+
|
|
327
|
+
```yaml
|
|
328
|
+
cell:
|
|
329
|
+
type: <existing type or number>
|
|
330
|
+
align: right
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Remove the explicit `headerClass` and `cellStyle.justifyContent`.
|
|
334
|
+
|
|
335
|
+
**Reviewer must answer:**
|
|
336
|
+
|
|
337
|
+
- Did the original set both cell and header alignment consistently? If only one side was aligned, the unified `align:` key changes visible behaviour — confirm.
|
|
338
|
+
|
|
339
|
+
## Column-level guardrails the codemod MUST emit alongside every proposal
|
|
340
|
+
|
|
341
|
+
For every column flagged as a conversion candidate, the report entry must include:
|
|
342
|
+
|
|
343
|
+
1. **"Field contract" block** — list every row-data path the built-in renderer will read, with a note whether the current aggregation/request delivers that shape. If unknown, mark as "needs aggregation inspection".
|
|
344
|
+
2. **"Visual parity" warning** — remind the reviewer to compare a few rows before/after to confirm padding, colours, row height, and null handling are acceptable.
|
|
345
|
+
3. **"Dark-mode check"** — new renderers use antd tokens; if the old hand-built HTML used hard-coded hex, dark mode may look different (usually better, occasionally surprising).
|
|
346
|
+
4. **"Sort / filter / CSV export"** — `cellRenderer` does not affect `valueGetter` / `valueFormatter` used by sort / filter / export. The built-in renderers _display_ only. If the original used a `cellRenderer` that also transformed the value, check that sort/filter/export still behave correctly.
|
|
347
|
+
5. **"Events"** — if the column participated in `onCellClick` with behaviour branching on column, migrating to `cell.type: link` + `onCellLink` is usually cleaner but changes the event shape. Audit event handlers.
|
|
348
|
+
|
|
349
|
+
## Apply workflow (per column)
|
|
350
|
+
|
|
351
|
+
1. Run the codemod in report mode. Output goes to a reviewable file (Markdown or JSON), one section per matched column.
|
|
352
|
+
2. For each section, the reviewer answers the questions above and either:
|
|
353
|
+
- **Approves** → manually apply the proposed `cell:` block in the column def.
|
|
354
|
+
- **Defers** → leave the column's custom cellRenderer untouched.
|
|
355
|
+
- **Requires data change** → log a ticket to adjust the aggregation / request to hydrate the field shape the built-in needs, then revisit.
|
|
356
|
+
3. After conversion, remove the now-unused `_function` / `__nunjucks` / `__dayjs.format` blocks and any `valueFormatter` that only formatted for display.
|
|
357
|
+
4. Test the page in the browser — confirm visual parity and that click/sort/filter/export behave identically.
|
|
358
|
+
5. Commit per-column (small, reviewable diffs) so a regression can be isolated quickly.
|
|
359
|
+
|
|
360
|
+
## What to NOT convert
|
|
361
|
+
|
|
362
|
+
- **Cells with conditional visual branching** (showing a completely different shape when `status == 'error'` vs default). The built-ins are single-shape.
|
|
363
|
+
- **Cells that compose multiple widgets** (avatar + status dot + text + link + icon). Split into separate columns, or keep the custom renderer.
|
|
364
|
+
- **Cells driven by a request that isn't yet aggregated** (e.g. the colour / icon is computed in Nunjucks from a `_global` that's not on the row). Either pre-compute in the aggregation or keep.
|
|
365
|
+
- **Cells where the `cellRenderer` also mutates `params` for ag-grid's internal sort/filter**. The built-ins only render — they don't touch value semantics.
|
|
366
|
+
- **Cells inside an `AgGridInput*`** where the cell is editable. The built-ins render display-only output; for editable cells, keep your custom `cellRenderer` / `cellEditor`.
|
|
367
|
+
|
|
368
|
+
## Verification
|
|
369
|
+
|
|
370
|
+
For each converted column:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
pnpm --filter=@lowdefy/blocks-aggrid build
|
|
374
|
+
pnpm ldf:b
|
|
375
|
+
pnpm ldf:d
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Navigate to the page, verify the column visually matches the prior output, click-test, sort-test, filter-test, export-test. Capture a before/after screenshot for the PR.
|
|
379
|
+
|
|
380
|
+
## Non-goals of this codemod
|
|
381
|
+
|
|
382
|
+
- No auto-apply. Ever. Even the "safe" conversions (date, ellipsis) require author confirmation because the data contract may differ.
|
|
383
|
+
- No bulk commit. One PR = one column, one reviewer sign-off.
|
|
384
|
+
- No cross-file refactors. If multiple tables share a module that currently returns HTML, migrate the module call-sites first, then the module itself in a follow-up.
|