|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/lint": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec,lint): declare the chart segment drill — `ChartDrillDownSchema`, on the react tier where it is actually read (#5022) |
| 7 | + |
| 8 | +`drillDown` has driven a real capability since long before this release: click a |
| 9 | +bar or a slice on an `<ObjectChart>` and objectui opens the underlying records, |
| 10 | +filtered by the clicked category, in a drawer. The protocol declared it |
| 11 | +**nowhere**. objectui read it as `(schema as any).drillDown`, so every key inside |
| 12 | +it — right, wrong, or misspelled — reached the renderer unchecked, and a typo was |
| 13 | +simply ignored at click time. This is Prime Directive #10 inverted: not declared |
| 14 | +without being delivered, but delivered without ever being declared. |
| 15 | + |
| 16 | +It is declared now, as `ChartDrillDownSchema`, and it is **additive** — nothing |
| 17 | +that parsed before stops parsing. |
| 18 | + |
| 19 | +## What you can write |
| 20 | + |
| 21 | +`drillDown` is a prop on the react-tier `<ObjectChart>` block: |
| 22 | + |
| 23 | +```jsx |
| 24 | +<ObjectChart objectName="opportunity" |
| 25 | + aggregate={{ function: 'sum', field: 'amount', groupBy: 'stage' }} |
| 26 | + drillDown={{ columns: ['name', 'amount'], maxRows: 50 }} /> |
| 27 | +``` |
| 28 | + |
| 29 | +| key | type | meaning | |
| 30 | +|---|---|---| |
| 31 | +| `enabled` | `boolean` | Only needed to force the drill OFF — the block being present already means on, so `drillDown={{}}` enables it | |
| 32 | +| `filter` | `Record<string, unknown>` | Filter for the drilled list; values support `${event.*}`. Omit it and the filter is derived from `aggregate.groupBy` equal to the clicked category | |
| 33 | +| `title` | `string` | Drawer/dialog heading; supports `${event.*}` | |
| 34 | +| `target` | `'drawer' \| 'dialog'` | In-place side sheet (default), or a centered modal when the chart is already inside a drawer | |
| 35 | +| `columns` | `string[]` | Column whitelist for the drilled list | |
| 36 | +| `maxRows` | `number` | Rows per page in the drilled list | |
| 37 | + |
| 38 | +Every one of those six is a key objectui's `ObjectChart` was measured to read. |
| 39 | +The renderer's own drill type is wider — it is shared with the table / pivot / |
| 40 | +metric widgets — and the extra keys are **deliberately not declared**, because a |
| 41 | +chart reads none of them: |
| 42 | + |
| 43 | +- **`mode`** (`'filter'`/`'record'`) is a table/pivot/metric key. A chart segment |
| 44 | + is always an aggregate, so there is nothing to discriminate. |
| 45 | +- **`report`** (drill into a report instead of a record list) is a metric/pivot |
| 46 | + capability. |
| 47 | +- **`view`** and **`sort`** are read by *no* renderer at all (objectui#3354). |
| 48 | +- **`target: 'navigate'`** is implemented for the other widgets but not for a |
| 49 | + chart, which falls back to the drawer. |
| 50 | + |
| 51 | +Writing any of them is now a loud rejection that says which surface owns it, |
| 52 | +rather than a value that silently does nothing. |
| 53 | + |
| 54 | +## Where it is NOT declared, and why that is deliberate |
| 55 | + |
| 56 | +**Not on `ChartConfigSchema`, and not a dashboard widget key.** A dashboard |
| 57 | +widget has no per-widget drill configuration, by design: an ADR-0021 |
| 58 | +dataset-bound widget drills through the semantic layer, deriving the target |
| 59 | +object and filter from the dataset row that was clicked. That is what |
| 60 | +`content/docs/ui/dashboards.mdx` has said all along, and it is what the renderer |
| 61 | +does — `DashboardRenderer` never reads `chartConfig`, and `DatasetWidget` |
| 62 | +forwards exactly one key out of it (`showLegend`). Declaring the drill there |
| 63 | +would have produced authorable metadata that parses clean and never reaches a |
| 64 | +renderer — the failure this campaign removes elsewhere. |
| 65 | + |
| 66 | +So the three places an author might reach for it now answer instead of shrugging: |
| 67 | + |
| 68 | +- `widget.chartConfig.drillDown` → rejected, pointing at the react-tier prop. |
| 69 | +- `widget.drillDown` / `widget.drilldown` → rejected, explaining that dashboard |
| 70 | + drill-through is **automatic**, and naming both configurable drills. |
| 71 | +- `report.drillDown` → rejected, pointing back at the chart prop. |
| 72 | + |
| 73 | +## `drillDown` is not `drilldown` |
| 74 | + |
| 75 | +Two capabilities, one letter apart, and they are now disambiguated in both |
| 76 | +directions at the schema gate: |
| 77 | + |
| 78 | +| | `drillDown` | `drilldown` | |
| 79 | +|---|---|---| |
| 80 | +| spelling | camelCase | all lowercase | |
| 81 | +| type | configuration object | boolean | |
| 82 | +| surface | react `<ObjectChart>` prop | `ReportSchema` key (ADR-0021 D2, on by default) | |
| 83 | + |
| 84 | +Edit distance alone gets this wrong — the two spellings are a distance of 1, so a |
| 85 | +plain "did you mean" would happily send an author writing `drillDown` on a report |
| 86 | +to `drilldown`, where their config object then fails a second time as a boolean. |
| 87 | +Both gates name the **type** difference, not just the spelling. |
| 88 | + |
| 89 | +## Enforced, not just declared |
| 90 | + |
| 91 | +`@objectstack/lint`'s react-page publish gate now **parses** the schema |
| 92 | +(`react-chart-drilldown-invalid`) against a static `drillDown={{…}}` literal, |
| 93 | +rather than re-deriving the rules. Unknown keys, the wrong `target`, and the |
| 94 | +near-key spelling all fail the build with the schema's own prescription. A value |
| 95 | +assembled from React state is skipped, unchanged: an unresolvable binding is not |
| 96 | +a wrong one (ADR-0072 D1). |
0 commit comments