@lowdefy/codemods 0.0.0-experimental-20260420141318 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/codemods",
3
- "version": "0.0.0-experimental-20260420141318",
3
+ "version": "5.1.0",
4
4
  "description": "Codemod scripts and migration prompts for Lowdefy version upgrades",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
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",
@@ -12,18 +12,18 @@ Moment.js locale shortcuts (`l`, `ll`, `lll`, `llll` and uppercase `L`, `LL`, `L
12
12
 
13
13
  **Replace these with explicit dayjs format tokens:**
14
14
 
15
- | Moment shortcut | Moment output example | Replacement |
16
- | --------------- | --------------------------------- | -------------------------- |
17
- | `l` | 3/26/2026 | `D/M/YYYY` |
18
- | `ll` | Mar 26, 2026 | `D MMM YYYY` |
19
- | `lll` | Mar 26, 2026 3:27 PM | `D MMM YYYY h:mm A` |
20
- | `llll` | Wed, Mar 26, 2026 3:27 PM | `ddd, D MMM YYYY h:mm A` |
21
- | `L` | 03/26/2026 | `DD/MM/YYYY` |
22
- | `LL` | March 26, 2026 | `D MMMM YYYY` |
23
- | `LLL` | March 26, 2026 3:27 PM | `D MMMM YYYY h:mm A` |
24
- | `LLLL` | Wednesday, March 26, 2026 3:27 PM | `dddd, D MMMM YYYY h:mm A` |
25
- | `LT` | 3:27 PM | `h:mm A` |
26
- | `LTS` | 3:27:45 PM | `h:mm:ss A` |
15
+ | Moment shortcut | Moment output example | Replacement |
16
+ | --- | --- | --- |
17
+ | `l` | 3/26/2026 | `D/M/YYYY` |
18
+ | `ll` | Mar 26, 2026 | `D MMM YYYY` |
19
+ | `lll` | Mar 26, 2026 3:27 PM | `D MMM YYYY h:mm A` |
20
+ | `llll` | Wed, Mar 26, 2026 3:27 PM | `ddd, D MMM YYYY h:mm A` |
21
+ | `L` | 03/26/2026 | `DD/MM/YYYY` |
22
+ | `LL` | March 26, 2026 | `D MMMM YYYY` |
23
+ | `LLL` | March 26, 2026 3:27 PM | `D MMMM YYYY h:mm A` |
24
+ | `LLLL` | Wednesday, March 26, 2026 3:27 PM | `dddd, D MMMM YYYY h:mm A` |
25
+ | `LT` | 3:27 PM | `h:mm A` |
26
+ | `LTS` | 3:27:45 PM | `h:mm:ss A` |
27
27
 
28
28
  **Files to search:**
29
29
 
@@ -33,25 +33,21 @@ Grep: `date\(['"]([lL]{1,4}|LTS?)['"]\)`
33
33
  **Example:**
34
34
 
35
35
  Before:
36
-
37
36
  ```yaml
38
37
  template: "{{ meeting_date | date('llll') }}"
39
38
  ```
40
39
 
41
40
  After:
42
-
43
41
  ```yaml
44
42
  template: "{{ meeting_date | date('ddd, D MMM YYYY h:mm A') }}"
45
43
  ```
46
44
 
47
45
  Before:
48
-
49
46
  ```yaml
50
47
  Last modified by {{ doc.updated.user.name | safe }} on {{ doc.updated.timestamp | date('lll')}}
51
48
  ```
52
49
 
53
50
  After:
54
-
55
51
  ```yaml
56
52
  Last modified by {{ doc.updated.user.name | safe }} on {{ doc.updated.timestamp | date('D MMM YYYY h:mm A')}}
57
53
  ```
@@ -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.