Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/10909-element-number-datasource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@object-ui/components': minor
---

`element:number` reads its object from the node-level `dataSource` binding, as the spec declares it (objectui#10909).

`PageComponentSchema.dataSource` is the spec's per-element data binding, and the spec lint gate waives a missing `properties.object` when `dataSource.object` names one. `ElementNumberRenderer` read its object only from `properties.object`, so a metric written `{ type: 'element:number', dataSource: { object: 'contact' }, properties: { aggregate: 'count' } }` — which the spec lint gate accepts, as `objectui validate` will once objectui#10908 arms the type — issued no query and painted the empty dash, with nothing to tell the author why.

The renderer now resolves the binding the way `element:record_picker` does:

- `object` is `dataSource.object ?? properties.object`, resolved once and used for the fetch guard, the `aggregate` / `find` call and the data-invalidation bus key. The binding wins when both are set.
- A named `view` is honoured: its filter scopes the aggregate. A view that cannot be resolved renders the shared "data source could not be resolved" panel and aggregates nothing, rather than counting every record of the object.
- `properties.filter` is AND-combined with the binding's `filter` and with its view's, the rule every block behind `ElementDataSourceGate` applies: neither is dropped, so a validated `properties.filter` can never be discarded and widen the count. A filter the converter refuses while combining them renders the same configuration-error panel, naming the refused rule, and aggregates nothing.
- The binding's `sort` and `limit` are not read: an aggregate has no ordering, and a capped count would be a wrong number.

A metric with no `dataSource` behaves exactly as before.

**The registration's declared inputs move.** `element:number` is now registered through `elementDataSourceBlock`, so its registration and the published manifest declare the `dataSource` input (the one injected declaration every reader of the binding carries), and the html tier no longer reports `has no prop "dataSource"` on the node. `object` is no longer `required`, because the binding can supply it; its description, and a new `filter` description, say how each combines with the binding.

**Clause-②: yes (widening)** — no export moves, but the published `element:number` entry in `sdui.manifest.json` widens: it declares `dataSource`, and `object` is no longer `required`. The html tier, and the objectstack CLI's JSX gate that reads this manifest, therefore accept a node bound through `dataSource` alone — and also one that names neither `object` nor a binding, which no longer draws `missing-required-prop` and paints the empty dash.
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,10 @@ const MEMBER_PINS: Record<string, MemberPin> = {
file: 'apps/console/src/__tests__/component-input-union-specimens.test.ts',
pins: 'The `object` arm is the inline translation map `{ en, "zh-CN" }` and nothing else — driven through the real `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the save gate use, each positive paired with a value matching NEITHER arm that must still be reported (objectui#4970).',
},
'element:number.dataSource': {
file: 'packages/components/src/renderers/basic/__tests__/elementNumber.dataSourceBinding-10909.test.tsx',
pins: 'The per-element binding\'s members as this METRIC reads them, through the REAL renderer and asserted at the call it fires, with the whole options bag compared so a member forwarded by accident is red. Read: `object` is what gets aggregated and OUTRANKS `properties.object` (`dataSource.object ?? properties.object`, one value for the fetch guard, `aggregate` / `find` and the `useDataInvalidation` key — a bus event for the outranked flat object does not re-read, one for the bound object does), and while a named `view` is unresolved there is NO object, so a flat `properties.object` beside an unresolvable view aggregates nothing; a named `view`\'s `filter` scopes the aggregate, and an unresolvable `view` REPORTS and aggregates nothing; `filter` is AND-combined with `properties.filter` — the `ElementDataSourceGate` rule, lowered and merged the same way — so a binding filter and the flat one both reach the aggregate, a view filter and the flat one both reach it, and a merge the converter refuses REPORTS on the error panel and aggregates nothing. Not read: `sort` and `limit`, from the binding or from the view, never reach `aggregate()` or the `find()` fallback, because an aggregate has no ordering and a capped count is a wrong number. A binding naming no object supplies nothing and the flat object stands, and the `properties` form with no binding is the control. ⚠️ The key is INJECTED by `Registry.register` (`ELEMENT_DATA_SOURCE_INPUT`), not written by the block: the declaration lists the binding\'s five members generically (`{ object, view, filter, sort, limit }`) and does not say which of them THIS block reads — that per-block half is what this pin carries (objectui#10909).',
},
'element:number.filter': {
file: 'packages/components/src/renderers/basic/__tests__/elementNumberFilterMembers-8071.test.tsx',
pins: 'The TWO wire spellings one authored predicate takes, chosen by an adapter capability the author cannot see, plus the re-query rule — asserted through the real renderer on a stubbed adapter. `aggregate()` receives it FLAT under its own name and beside the members that make the call an aggregate (`field`, `function`, `groupBy: \'_all\'`), asserted as the whole options bag rather than the one key; the `find()` fallback receives it WRAPPED as `$filter`; and an unfiltered metric sends `undefined` rather than an empty envelope some adapters read as "match nothing". Collapsing the two spellings into one drops the predicate silently and the metric paints a confidently wrong number over every row, with no diagnostic and no empty state. The third half is that the key is read BY VALUE, not by identity (`filterKey` is a `JSON.stringify` memo): a deep-equal filter rebuilt by a re-rendering parent must NOT re-probe, while a changed comparand MUST and carries the new predicate — each arm the other\'s control, so a dependency array "simplified" to the raw object (a per-render fetch storm) is red. New file (objectui#8071 slice 7).',
Expand Down
14 changes: 14 additions & 0 deletions content/docs/guide/data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `list-view` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:number` | ✅ | filter | ✅ | — single value | — single value |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
Expand All @@ -279,6 +280,19 @@ of the binding and stays the author's — it has to name a field on the bound ch
object, so rebinding `object` without updating it is an authoring error the panel
cannot paper over.

The two `element:*` rows keep their configuration in the node's `properties` bag,
so the binding does not land on a schema key there: each reads it directly, and
`dataSource.object` wins over `properties.object`. They differ on `filter`.
`element:record_picker` takes the binding's (or its view's) filter in place of
`properties.filter`, which applies only when neither supplies one.
`element:number` AND-combines `properties.filter` with the binding's filter and
its view's — the rule the gate-wrapped blocks above follow — so neither is
dropped, and a filter refused while combining them shows the configuration-error
panel instead of a count. On `element:number`,
`{ "dataSource": { "object": "contact" }, "properties": { "aggregate": "count" } }`
is a complete metric; its `sort` and `limit` are not read, because an aggregate
has no ordering and a capped count would be a wrong number.

On `record:related_list` and `record:line_items` the composed filter is
AND-combined with the parent relationship condition, never substituted for it: a
child panel is always scoped to the record it appears on, and an *additional*
Expand Down
Loading
Loading