Skip to content

Commit dd0d78f

Browse files
fix(components): element:number reads its object from dataSource.object, as the spec binding declares (objectui#10909) (#10944)
Fixes #10909 Clause-②: yes (widening) Implemented on branch `claude/issue-10909-element-number-datasource` by the `os-dev` run dispatched from the `domain:ui` seat 2 session `session_014mXUNuFomfj24w7s1pZzhN` (claim comment 5866532141 on the card). ## What changed - **`ElementNumberRenderer`** (`packages/components/src/renderers/basic/elements.tsx`) now reads the spec's per-element binding (`PageComponentSchema.dataSource`) through `useElementDataSource`, the way its element twin `element:record_picker` does. - The object resolves ONCE as `dataSource.object ?? properties.object`, and that one value is the fetch guard, the `adapter.aggregate` / `adapter.find` target and the `useDataInvalidation` key. `dataSource.object` wins when both are set. - While a named view is unresolved or unresolvable, there is no object, even beside a flat `properties.object`. - **Filter (seat ruling B):** with a binding present, `properties.filter` is AND-combined with the binding's filter and with its view's. The lowering and merge are the ones `ElementDataSourceGate` uses (`toFilterNodeSafely` + `mergeFilterNodes`), so neither filter is dropped. A merge the converter refuses shows the configuration-error panel and aggregates nothing. With no binding, `properties.filter` reaches the adapter exactly as authored. - **Registration:** `element:number` is registered through `elementDataSourceBlock(...)` (from `@object-ui/core`), so `Registry.register` injects the one shared `dataSource` declaration (`ELEMENT_DATA_SOURCE_INPUT`, `binding: 'object'`). - `object` is no longer `required: true`. - The `object` input's description states its precedence. - The `filter` input's description is exactly: "Criteria the aggregate is scoped by. When a node-level `dataSource` binding also supplies a filter (its own, or the saved view its `view` names), the two are AND-combined: neither is dropped." - **Pins:** new `packages/components/src/renderers/basic/__tests__/elementNumber.dataSourceBinding-10909.test.tsx` (18 rows). It is registered as the `element:number.dataSource` member pin in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, which the parity census requires once the block declares an object-armed input. - **Docs:** `content/docs/guide/data-source.md` gains the `element:number` row in the per-element binding table. A paragraph says how the two `element:*` rows read the binding, and that they differ on `filter`: `record_picker` substitutes, `element:number` AND-combines. - **Changeset:** `.changeset/10909-element-number-datasource.md`, `'@object-ui/components': minor`, with `Clause-②: yes (widening)`. ## H1: reproduced on `main` (733fd5a) The probe used the real `SchemaRenderer`, the real registry, and an `AdapterCtx` adapter with `aggregate` / `find` spies. The probe file was temporary and not committed. ```text H1 dataSource-form :: aggregate calls=[] find calls=[] painted="—" H1 properties-form :: aggregate calls=["contact"] find calls=[] painted="7" ``` The same probe on this branch: ```text H1 dataSource-form :: aggregate calls=["contact"] find calls=[] painted="7" H1 properties-form :: aggregate calls=["contact"] find calls=[] painted="7" ``` ## `ElementDataSourceSchema` members: which ones this renderer reads | member | read? | how | |---|---|---| | `object` | yes | `dataSource.object ?? properties.object`. There is no object at all while a named view is unresolved or unresolvable. A binding naming no object (empty or not a string) supplies nothing, and the flat object stands. | | `view` | yes | Through `useElementDataSource`: the saved view's `filter` scopes the aggregate. While the view resolves, the shared loading panel shows and nothing is read. If it cannot be resolved, the shared error panel (`element-number-datasource-error`) shows and nothing is aggregated. | | `filter` | yes | `properties.filter` AND (view filter AND binding filter), lowered and merged as `ElementDataSourceGate` does. Neither is dropped; a refused merge reports and aggregates nothing. Context tokens are handled by the existing `useResolvedFilter` on the merged value. There is no record-context binding (objectui#7297 stays on hold), and the filter-shape note (objectui#8945) is untouched. | | `sort` | no | An aggregate has no ordering. It never reaches `aggregate()` or `find()`, and the whole options bag is asserted. | | `limit` | no | A capped count would be a wrong number. Neither the binding's nor a view's `pagination.pageSize` reaches `aggregate()` or the `find()` fallback. | | `columns` / `viewType` (view-only composed keys) | no | A metric has no projection and no view kind. | No member is declared per member. The only declaration is the injected `dataSource` input (`type: 'object'`, `binding: 'object'`), whose description lists the binding's five members generically. Which of them this block reads is pinned at the read site. ## The registration change: what `elementDataSourceBlock` changes, exactly `elementDataSourceBlock` is a marker. It adds the renderer to a `WeakSet` and returns it unchanged, so it adds no gate, no loading state and no fetch. The declaration is emitted by `withElementDataSourceInput` inside `Registry.register`. Measured on the live registry: - the registered inputs on head are `object,aggregate!,field,filter,format,prefix,suffix,dataSource` (`!` = required). On base, `object!` carried the required mark and there was no `dataSource`; - `validateTree` on `{ type: 'element:number', dataSource: { object: 'contact' }, aggregate: 'count' }` returns no diagnostic. On base it returned `is missing required prop "object"` and `has no prop "dataSource"`. The render-path changes come from `useElementDataSource` and from the filter merge, and they reach only a node that carries a binding: - a named `view` reads saved views and shows the shared loading panel while it resolves; - an unresolvable `view`, or a filter refused in either merge, shows the shared error panel; - a node with no `dataSource` resolves `absent` synchronously, so the `properties` form issues no new read, shows no new panel, and sends its filter unlowered. This is pinned: the control row asserts `getObjectSchema` is never called. ## Pins: red before, green on head File: `elementNumber.dataSourceBinding-10909.test.tsx`, 18 rows. The file was byte-identical in all three runs, and each base run used a temporary detached worktree that was removed afterwards. | run | result | |---|---| | head 37bc45f | `Tests 18 passed (18)` | | base 733fd5a | `Tests 16 failed \| 2 passed (18)` — the 2 green rows are the `properties`-form control and the empty-`object` binding row | | previous head 48d755d (rule A) | `Tests 6 failed \| 12 passed (18)` | The 6 rows red on the previous head: - the two AND rows (binding + properties filter, view + properties filter) and the refused-merge row are red on semantics; - "the binding filter scopes the aggregate", "a named saved view's filter scopes the aggregate" and the sort/limit whole-bag row are red on wire shape only. With a binding present, every filter source is now lowered to the ObjectQL AST before merging, as the gate does. Rows: 1. the dataSource form calls aggregate for the bound object and paints the value; 2. dataSource.object wins over properties.object when both are set; 3. control: the properties form is unchanged; 4. the find() fallback reads the bound object when the adapter cannot aggregate; 5. a binding that names no object supplies nothing, and the flat object stands; 6. one bus event re-reads it, keyed on the resolved object; 7. the key is the binding, not the flat object it outranks; 8. filter: the binding filter scopes the aggregate; 9. filter: properties.filter is AND-combined with the binding filter — both reach the aggregate, neither is dropped; 10. filter: a named view's filter and properties.filter are both applied, AND-combined; 11. filter: a merge the converter refuses reports on the error panel and aggregates nothing; 12. view: a named saved view's filter scopes the aggregate; 13. view: one that cannot be resolved reports and aggregates nothing; 14. view: an unresolvable one does not fall back to properties.object — no object while it is unresolved; 15. sort and limit are not read: the options bag stays whole with a binding and a view carrying both; 16. limit is not read on the find() fallback either: a count is never capped; 17. the registration declares dataSource as an object binding, and object is no longer required; 18. the html tier accepts the dataSource form without a diagnostic (control in the same row: `element:text` still reports `has no prop "dataSource"`). **Ablations** at 37bc45f, through `objectstack/scripts/ablation-replace.mjs` in wrap mode with its trap-armed restore. Each was run over the pin file plus the three pre-existing `element:number` files (`elementNumberFilterMembers-8071`, `elementReaders.invalidationRefetch-10623`, `element-number.contractEnvelope-6726`), 33 rows in all. Each restore printed `ok restored: blob == HEAD (3ab0d9a) and git diff HEAD is empty`. | # | ablation | red | green | |---|---|---|---| | 1 | resolution line: `(composed?.object ?? props.object)` becomes `(/* ablation-10909 */ props.object)`; blob `3ab0d9a745a4` to `875d8dfa0e4a` | `11 failed \| 22 passed (33)`: rows 1, 2, 4, 6–10, 12, 15 and 16, the dataSource-form rows that reach the resolution line | both controls, the refused-merge and unresolvable-view rows, the two registration rows, and all 15 rows of the three pre-existing files | | 2 | AND merge: `mergeFilterNodes(own.node, bound.node)` becomes `(/* ablation-10909-and */ bound.node ?? own.node)`, the rule A semantics; blob `3ab0d9a745a4` to `2a9bf06af87c` | `2 failed \| 31 passed (33)`: exactly the two AND rows, 9 and 10 | everything else | | 3 | guard: `unresolved \|\| filterRefusal ? undefined : ` becomes `/* ablation-10909-guard */ `; blob `3ab0d9a745a4` to `c249c85fa79b` | `2 failed \| 31 passed (33)`: exactly row 11 (refused merge) and row 14 (unresolved view beside a flat object) | everything else | ## Census: element renderers and the `dataSource` binding Every `element:*` registration is in `@object-ui/components`, and no other package registers one. The census probe rendered each registered element with `dataSource: { object: 'contact' }` and no flat object: | block | object-bound? | reads `dataSource`? | html tier on `dataSource` | |---|---|---|---| | `element:number` | yes (`object`) | **yes** (this PR): `aggregate(contact)` | accepted | | `element:record_picker` | yes (`object`) | yes: `find(contact)` | accepted | | `element:repeater` | yes (`object`, required) | no: no read | reports `has no prop "dataSource"` and the missing `object` | | `element:metadata_viewer` | names a METADATA subject (a state machine's object), not a record query | no | reports `has no prop "dataSource"` | | `element:text`, `element:divider`, `element:image`, `element:button`, `element:text_input`, `element:definition-list` | no | no | reports `has no prop "dataSource"` | `element:form` and `element:filter` have `ComponentPropsMap` rows but no objectui renderer. `element:repeater` is not a spec block: it has no `PageComponentType` member and no `ComponentPropsMap` row. Its declaration and its runtime agree, and the html tier reports the key, so this is an observation, not a finding. ## Consumer sweep The file set is the `git grep` hits for `element:number`, `ElementNumberRenderer`, `elementDataSourceBlock`, `withElementDataSourceInput` and `ELEMENT_DATA_SOURCE_INPUT` in tests, plus: - the html-tier manifest and html-tier `dataSource` declaration suites; - the registry-inputs parity suite and the `element-data-source-input-injection` render probe; - the `record_picker` data-source and malformed-filter pins; - the suites that render every element renderer generically. Fix round 1 adds the gate-family filter-merge tests this change reuses: `ElementDataSourceGate.malformedFilter-10789`, `ElementDataSourceGate.test`, core `filter-source-merge.test`, and `ObjectMetric.elementDataSource.test`. At 37bc45f, 34 files, run under the shared verify lock: `Test Files 34 passed (34)` · `Tests 983 passed (983)`. The full `@object-ui/components` suite ran at 75f027e (before this round) in three directory runs: 326 collected files, all green. This round changes only `elements.tsx` and its pin file inside that package. Both are covered above by the directories' own `element:number` files and by the sweep. ## Gates At 37bc45f. Each verdict is the gate's own line. - `pnpm --filter @object-ui/components type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`), after `pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build` (8 of 47 projects): exit 0. - `pnpm check:element-data-source-declaration`: `OK — 14 gate-consuming file(s) checked, 3 definition file(s) excluded; all reach the seam and take it from core.` - `node scripts/check-changeset-presence.mjs`: `3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s): .changeset/10909-element-number-datasource.md.` - `node scripts/check-changeset-no-major.mjs`: `No changeset declares a major bump.` The frontmatter is `minor`. - `pnpm check:changeset-claims`: report-only. The same three pending changesets name touched files (`6726-find-envelope-records-arms`, `6783-readprops-degenerate-config-bag`, `8067-component-input-member-kind`); each paragraph is still true. - `pnpm check:control-bytes`: `OK`. - `pnpm check:new-line-citations`: `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `node scripts/check-doc-links.mjs`: `Links are valid across 17 scan roots.` - **eslint, a proven narrowing** (`--no-inline-config --format json` over the three changed source and test files): 1. **Population** from eslint's own config: `ESLint.isPathIgnored` answers `false` for all three. 2. **Count** from the JSON: 3 files, 0 errors. `elements.tsx` has 24 warnings, the same per-rule split as base; the other two files have 0. 3. **Invariance**: type-aware linting is off (no `parserOptions.project` or `projectService`), and the one custom rule reads no file from disk, so untouched files cannot move. - Carried from round 0, and not affected by this round's diff: `check:handler-key-reads`, `check:designer-field-key-parity`, `check:component-surface-parity`, `check:unreferenced-sources`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:registry-bare-names`, `check:i18n-keys`, `check:doc-types`, `check:doc-fences`, `check:doc-example-ids` and `check:spec-symbols` were green at 75f027e. - **NOT MEASURED: `apps/console` type-check.** Reason: its program needs every plugin's built declarations, and that closure build is beyond the foreground cap. The CI `Type Check` job owns it. The console edit is one `MEMBER_PINS` entry with two string fields. - **NOT MEASURED: `check:sdui-registration-pins`.** Reason: PREREQUISITE, no console bundle (exit 2). The `performance-budget` workflow runs it. - **NOT MEASURED: `check:doc-snippets` and `check:doc-examples`.** Reason: PREREQUISITE, unbuilt packages (exit 2). The guide edit adds no fenced snippet. ## Serial - **Open PRs**, read through REST before opening: 19 PRs, all file lists (1,867 rows). None touches `packages/components/src/renderers/basic/`, the parity suite or `content/docs/guide/data-source.md`. Positive control: PR objectui#10908's `packages/types/` files match the same grep. - `packages/core` is unchanged; this change only imports its existing `toFilterNodeSafely` and `mergeFilterNodes`. - `git merge-tree --write-tree` against a fresh `origin/main` 5c94589, fetched into a private ref: exit 0, tree `4c51202bf5`. Main moved 20 commits since the base, and none of them touches the renderer, the parity suite, the guide, `useElementDataSource`, `useResolvedFilter`, the gate, the core data-source module, `filter-converter`, `Registry.ts` or `validate.ts`. - `git merge-tree --write-tree` against PR objectui#10908's head fe8f6a6: exit 0, tree `4bb57ac81c`. ## Fix round 1 This responds to the contract review FAIL at 48d755d, on text and one seat ruling. Object resolution, the unchanged properties form, the registration, the census and the pins held. - **Seat ruling B, filter AND-combine.** Implemented in the renderer with the gate's `toFilterNodeSafely` + `mergeFilterNodes`. - The precedence row flipped to assert that both filters reach the aggregate. - New rows: a view filter plus `properties.filter`; a refused merge taking the error panel with no aggregate call. - The registered `filter` description is the ruling's text verbatim. The guide paragraph and the changeset bullet say AND-combined. - `record_picker` is untouched. - **Changeset:** - the frontmatter becomes `minor`; - the Clause paragraph is replaced with the review's `Clause-②: yes (widening)` text verbatim; - "accepted by both validators" becomes "which the spec lint gate accepts, as `objectui validate` will once objectui#10908 arms the type"; - the filter bullet now states AND-combine and the loud refusal. - **PR body:** line 2 is `Clause-②: yes (widening)`. - **Unresolved-guard pin:** `dataSource: { object: 'contact', view: 'no_such_view' }` beside `properties.object: 'contact'` asserts no aggregate call and the error panel. The guard ablation reddens exactly that row and the refused-merge row. - **`MEMBER_PINS` prose:** rewritten for rule B. It no longer says the declaration carries "nothing about members"; it says the injected description lists the five members generically and the pin carries which of them this block reads. ## Acceptance notes - **This unblocks PR objectui#10908.** It is the contract review's option (c) sequencing. With this landed, both validators, the html tier and the renderer tell one story about `{ dataSource: { object } }`, and PR objectui#10908's arm (which accepts `properties.filter` beside a binding filter or view) now matches a renderer that honours both. - **Clause-②: yes (widening).** The published `element:number` manifest entry declares `dataSource`, and `object` is no longer `required`. The html tier, and the objectstack CLI's JSX gate that reads the manifest, now also accept a node that names neither `object` nor a binding, which paints the empty dash. That case is still reported on the JSON door by the spec lint gate (advisory), and after PR objectui#10908 by its arm when a bag is present. A runtime "no object named" notice, or a one-of check at the html tier, would be a follow-up card. Precedent: objectui#10508 made the same trade on calendar and gantt `objectName`. - **Wire shape with a binding.** When a binding is present, every filter source is lowered to the ObjectQL AST before it reaches `aggregate()` / `find()`, as `ElementDataSourceGate` does for `object-metric`. Without a binding, `properties.filter` is sent as authored, as before. - **Studio is unchanged.** Studio's block config for `element:number` stays properties-only and offers no `dataSource` editor. - **The error-panel title** "could not be resolved" is imprecise for a refused filter; the panel's message names the refused rule. `record_picker` shares the wording. This is a review nit, and the text is unchanged here. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c94589 commit dd0d78f

5 files changed

Lines changed: 517 additions & 14 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@object-ui/components': minor
3+
---
4+
5+
`element:number` reads its object from the node-level `dataSource` binding, as the spec declares it (objectui#10909).
6+
7+
`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.
8+
9+
The renderer now resolves the binding the way `element:record_picker` does:
10+
11+
- `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.
12+
- 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.
13+
- `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.
14+
- The binding's `sort` and `limit` are not read: an aggregate has no ordering, and a capped count would be a wrong number.
15+
16+
A metric with no `dataSource` behaves exactly as before.
17+
18+
**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.
19+
20+
**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.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,10 @@ const MEMBER_PINS: Record<string, MemberPin> = {
23012301
file: 'apps/console/src/__tests__/component-input-union-specimens.test.ts',
23022302
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).',
23032303
},
2304+
'element:number.dataSource': {
2305+
file: 'packages/components/src/renderers/basic/__tests__/elementNumber.dataSourceBinding-10909.test.tsx',
2306+
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).',
2307+
},
23042308
'element:number.filter': {
23052309
file: 'packages/components/src/renderers/basic/__tests__/elementNumberFilterMembers-8071.test.tsx',
23062310
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).',

‎content/docs/guide/data-source.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ ignores would be accepted and dropped, which is the defect this binding removes.
253253
| `list-view` | ✅ | ✅ | ✅ | ✅ | ✅ |
254254
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
255255
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
256+
| `element:number` | ✅ | filter | ✅ | — single value | — single value |
256257
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
257258
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
258259
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
@@ -279,6 +280,19 @@ of the binding and stays the author's — it has to name a field on the bound ch
279280
object, so rebinding `object` without updating it is an authoring error the panel
280281
cannot paper over.
281282

283+
The two `element:*` rows keep their configuration in the node's `properties` bag,
284+
so the binding does not land on a schema key there: each reads it directly, and
285+
`dataSource.object` wins over `properties.object`. They differ on `filter`.
286+
`element:record_picker` takes the binding's (or its view's) filter in place of
287+
`properties.filter`, which applies only when neither supplies one.
288+
`element:number` AND-combines `properties.filter` with the binding's filter and
289+
its view's — the rule the gate-wrapped blocks above follow — so neither is
290+
dropped, and a filter refused while combining them shows the configuration-error
291+
panel instead of a count. On `element:number`,
292+
`{ "dataSource": { "object": "contact" }, "properties": { "aggregate": "count" } }`
293+
is a complete metric; its `sort` and `limit` are not read, because an aggregate
294+
has no ordering and a capped count would be a wrong number.
295+
282296
On `record:related_list` and `record:line_items` the composed filter is
283297
AND-combined with the parent relationship condition, never substituted for it: a
284298
child panel is always scoped to the record it appears on, and an *additional*

0 commit comments

Comments
 (0)