|
| 1 | +--- |
| 2 | +"@objectstack/lint": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(lint): resolve an interface page's whitelisted visualizations at validate/build (#14073) |
| 6 | + |
| 7 | +Accept-set narrowing, `minor` under the family precedent (#14107, #14105, #14148). |
| 8 | + |
| 9 | +An interface `list` page whitelists renderers with |
| 10 | +`interfaceConfig.appearance.allowedVisualizations`, and `InterfacePageConfigSchema` |
| 11 | +is a closed shape with **no per-visualization binding key at all** — no |
| 12 | +`calendar:`, no `kanban:`, no `map:`. So #13817's parse-time refinement, which |
| 13 | +demands a `calendar:` block on a list VIEW that whitelists `calendar`, was |
| 14 | +correctly not extended to this door: a requirement the page surface cannot |
| 15 | +satisfy would be unauthorable. |
| 16 | + |
| 17 | +That left the page door with no check of any kind. Measured on objectui |
| 18 | +`f0f774b0` (after objectui#7029 removed the invented `due_date` default), the |
| 19 | +renderer derives each binding from the source object's fields |
| 20 | +(`InterfaceListPage.tsx`, `view.<viz> ?? deriveFromObject(objectDef)`), and when |
| 21 | +nothing derives there are exactly two outcomes, neither of which reaches the |
| 22 | +author: |
| 23 | + |
| 24 | +- the entry LEADS the whitelist — it becomes the page's forced view type, is |
| 25 | + force-pushed into the switcher's resolvable set, and every visitor lands on |
| 26 | + the renderer's "Calendar configuration required" refusal screen; |
| 27 | +- the entry is anywhere else — it is filtered out of the switcher **silently**, |
| 28 | + while the switcher chrome still appears (it is shown on whitelist length). |
| 29 | + |
| 30 | +The new rule `page/visualization-without-binding` |
| 31 | +(`validatePageVisualizationBindings`, a member of the reference-integrity suite, |
| 32 | +so it runs on `validate` / `lint` / `compile`) asks the renderer's own question |
| 33 | +at authoring time. For every `list` page, each whitelisted visualization must be |
| 34 | +either derivable from the source object's declared fields — using the SAME |
| 35 | +predicates the renderer applies, the field TYPE first and then the NAME regex |
| 36 | +fallback (kanban: select-like type or status-like name; calendar and timeline: a |
| 37 | +date-typed non-hidden non-system field, else a date-like name; gallery: |
| 38 | +image-typed, no name leg; gantt: two distinct date fields; map: location-typed or |
| 39 | +a geo-like name) — or bound by the block of the list view the page references |
| 40 | +through `sourceView` (a `calendar:` block also binds a `timeline`, which is what |
| 41 | +`resolveTimelineDateBinding` accepts). `grid` always passes. |
| 42 | + |
| 43 | +Severity tracks what the visitor sees: **`error`** when the unbound entry is |
| 44 | +`allowedVisualizations[0]`, **`warning`** otherwise. Every message names |
| 45 | +`sourceView` as the remedy, because on this door it is the one schema-legal |
| 46 | +channel for a per-visualization binding — exactly how the shipped showcase map |
| 47 | +page binds its `locationField`. |
| 48 | + |
| 49 | +Deliberately NOT stricter than the renderer: mirroring both predicates rather |
| 50 | +than the type half alone means a page whose only date is a text field called |
| 51 | +`due_date` still passes, because it still renders. The mirrored table is exported |
| 52 | +(`OBJECTUI_DERIVATION_PREDICATES`) and pinned verbatim by a fixture test, and the |
| 53 | +seven shipped `showcase_task` interface pages are pinned as a live regression |
| 54 | +corpus — the two halves catch drift on either side of a mirror no build edge |
| 55 | +connects. `chart` and `tree` get no verdict: the renderer derives no binding for |
| 56 | +them on this seam, so the rule says nothing rather than guessing. |
| 57 | + |
| 58 | +**Migration.** A page reported by the new rule whitelists a visualization that |
| 59 | +renders nothing: point the page at a list view that declares the block |
| 60 | +(`interfaceConfig.sourceView`), give the source object a field the derivation can |
| 61 | +find, or drop the entry from `appearance.allowedVisualizations`. Four skips keep |
| 62 | +it quiet where it cannot know: a page that is not `type: 'list'`, an object this |
| 63 | +stack does not define, an object with no readable field map (ADR-0015 |
| 64 | +`external`), and a `sourceView` naming a view this stack does not declare (the |
| 65 | +runtime hydrates stored view bodies over the network). |
| 66 | + |
| 67 | +No `packages/spec` change — the page surface stays closed, which is what ADR-0047 |
| 68 | +§7 open question 3 asks for. |
0 commit comments