|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/lint": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec,lint)!: give `bulkActionDefs` a shape, and lint the aggregate name it references (#4457) |
| 7 | + |
| 8 | +A selection-bar bulk action was declared as |
| 9 | +`z.array(z.record(z.string(), z.any()))` — **no shape at all**. The real |
| 10 | +contract lived in objectui's `BulkActionDef` interface and in the executor that |
| 11 | +reads it, so every authoring mistake landed as a silent runtime downgrade: |
| 12 | +`opeartion` parsed and the executor hit `Unknown operation: undefined` per row; |
| 13 | +`excution: 'aggregate'` parsed and the def stayed per-record, so the endpoint |
| 14 | +written for ONE `_selectedIds` call got N calls instead — the exact defect |
| 15 | +objectui#3139 was filed to make expressible. That is ADR-0018's "second |
| 16 | +vocabulary" smell (an action surface sharing none of `ActionSchema`'s checks) |
| 17 | +crossed with ADR-0078's silently-inert metadata. |
| 18 | + |
| 19 | +`ui/bulk-action.zod.ts` types it, with the same treatment `ActionParamSchema` |
| 20 | +got in #3746/#4001: a **strict** def whose unknown-key error names the offending |
| 21 | +key and the canonical spelling. Beyond spelling, it refuses the combinations the |
| 22 | +executor never reads — `patch` outside an `update`, `execution` outside a |
| 23 | +`custom`, `params` on a `delete`, `batchSize` on an aggregate — and refuses a |
| 24 | +hand-written `actionDef`, which is attached by the renderer when it resolves the |
| 25 | +def's `name` and which authored by hand would smuggle an action definition past |
| 26 | +the action registry. |
| 27 | + |
| 28 | +**One shape that parsed before is now rejected**: `operation: 'custom'` without |
| 29 | +`execution: 'aggregate'`. `resolveBulkActions` attaches a dispatcher for exactly |
| 30 | +one authored shape (the aggregate one); every other custom def falls to |
| 31 | +`Promise.resolve()` per row — a button that reports success for every selected |
| 32 | +record and does nothing. The error names both legal forms: `bulkActions: |
| 33 | +['<name>']` for per-record (promoted with the action's own label, params and |
| 34 | +`visible`), `execution: 'aggregate'` for one call over the whole selection. |
| 35 | + |
| 36 | +Two things are deliberately left open: |
| 37 | + |
| 38 | +- **`params[]` is `.passthrough()`.** objectui's `BulkActionParam` declares a |
| 39 | + `[key: string]: unknown` catch-all — widget config (min/max/step/format) |
| 40 | + forwarded to the field renderer as-is. Locking it down would reject valid |
| 41 | + config, so declared keys are typed and the rest rides through, the same call |
| 42 | + `dashboard.zod.ts` makes for a widget's `config`. |
| 43 | +- **The bulk-param / action-param spelling divergence** (`help`/`helpText`, |
| 44 | + `default`/`defaultValue`, `object`/`reference`, plus `labelField`, which |
| 45 | + `ActionParamSchema` has no counterpart for). objectui already owns a converter |
| 46 | + for the promoted direction; converging the authored direction is a cross-repo |
| 47 | + change with its own migration. Typing them as they are is what makes the |
| 48 | + divergence visible rather than undocumented — the prerequisite for closing it. |
| 49 | + |
| 50 | +`label` and the param/option labels are `z.string()`, not `I18nLabelSchema`: |
| 51 | +an authored def reaches the grid verbatim (nothing resolves an `{ en, zh }` map |
| 52 | +on this path) and the bar renders `def.label` as a React child, so blessing the |
| 53 | +map form would trade a parse error for a blank screen. Localize by declaring a |
| 54 | +real action and naming it in `bulkActions` — that path runs through the i18n |
| 55 | +resolver. |
| 56 | + |
| 57 | +**Lint**: `validate-action-name-refs` now covers `bulkActionDefs`. Only an |
| 58 | +`execution: 'aggregate'` entry is a name reference (it is what |
| 59 | +`resolveBulkActions` looks up); an `update`/`delete` def's `name` is a button id |
| 60 | +and resolving it would be nonsense. The walk also reaches an **object's own |
| 61 | +`listViews`** for the first time — an object has no top-level `list`, so that |
| 62 | +tier had simply never been visited while the view-level ones were covered. And |
| 63 | +the hint no longer tells a bulk-surface author to add a `locations` entry: the |
| 64 | +selection bar is the one surface that does not filter on it, so naming the |
| 65 | +action there is the whole placement. |
| 66 | + |
| 67 | +Verified zero new findings against `app-showcase` / `app-crm` / `app-todo`. |
0 commit comments