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
13 changes: 13 additions & 0 deletions .changeset/11880-element-datasource-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/components': patch
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
---

`element:record_picker` and `element:number` bind data through the node-level `dataSource` only (objectui#11880, part of the objectui half of objectstack#11509, ruled A-narrow).

- Neither element reads the flat `properties.object` / `filter` / `sort` / `limit` beside the binding any more: the `composed?.x ?? props.x` fallbacks are gone, and `element:number`'s filter is the binding's alone (the AND with `properties.filter` is gone). An element whose node names no `dataSource.object` issues no query. The flat keys stay published until `@objectstack/spec` retires them in v18; each input's description now says it is not read and names the binding member that is.
- The Studio page designer writes `element:number`'s object to `dataSource.object` at node level, and its measure picker reads the object from there. A stored `properties.object` stays visible in the inspector's Advanced section.
- `element.number.noObject` now names only `dataSource.object`, in all ten language packs.

Breaking for an author who wrote the flat keys on these two elements: move them into `dataSource`. `element:repeater` is unchanged.
13 changes: 10 additions & 3 deletions apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ const element = (base: { type: string; id: string; properties: Record<string, un
...base,
properties: { ...base.properties, filter },
});
// `element:number` and `element:record_picker` read their query from the
// node-level `dataSource` binding only (objectui#11880), so the filter and the
// object ride there.
const bound = (base: { type: string; id: string; properties: Record<string, unknown> }) => (filter: Filter) => ({
...base,
dataSource: { object: OBJECT, filter },
});

const INLINE = { provider: 'value', items: [{ ...ROW }] };

Expand Down Expand Up @@ -229,11 +236,11 @@ const ROWS: NodeRow[] = [
{ node: 'element:repeater', shape: 'tuple', path: 'dataSource', filterParam: '$filter',
withFilter: element({ type: 'element:repeater', id: 'rep', properties: { object: OBJECT, fields: ['name'] } }) },
{ node: 'element:number (aggregate)', shape: 'tuple', path: 'dataSource', filterParam: 'filter', aggregate: true,
withFilter: element({ type: 'element:number', id: 'num', properties: { object: OBJECT, aggregate: 'count' } }) },
withFilter: bound({ type: 'element:number', id: 'num', properties: { aggregate: 'count' } }) },
{ node: 'element:number (find fallback)', shape: 'tuple', path: 'dataSource', filterParam: '$filter', aggregate: false,
withFilter: element({ type: 'element:number', id: 'num', properties: { object: OBJECT, aggregate: 'count' } }) },
withFilter: bound({ type: 'element:number', id: 'num', properties: { aggregate: 'count' } }) },
{ node: 'element:record_picker', shape: 'rule', path: 'dataSource', filterParam: '$filter',
withFilter: element({ type: 'element:record_picker', id: 'rp', properties: { object: OBJECT } }) },
withFilter: bound({ type: 'element:record_picker', id: 'rp', properties: {} }) },
// Regression rows (objectui#10607, objectui#10666 part 1).
{ node: 'object-gallery (regression)', shape: 'tuple', path: 'dataSource', filterParam: '$filter',
withFilter: top({ type: 'object-gallery', objectName: OBJECT, gallery: { titleField: 'name' } }) },
Expand Down
10 changes: 5 additions & 5 deletions apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2772,23 +2772,23 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'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).',
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 since objectui#11880 the ONLY object (one value for the fetch guard, `aggregate` / `find` and the `useDataInvalidation` key — a bus event for a flat `properties.object` beside it does not re-read, one for the bound object does); while a named `view` is unresolved there is NO object, so nothing is aggregated; a named `view`\'s `filter` scopes the aggregate, and an unresolvable `view` REPORTS and aggregates nothing; `filter` is the binding\'s (AND-combined with its view\'s), lowered, and the flat `properties.filter` is not read — a binding filter 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 `properties` form alone shows the no-object notice and aggregates nothing (objectui#11880). ⚠️ 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).',
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. Since objectui#11880 the predicate is the binding\'s `dataSource.filter` (lowered to the ObjectQL AST); this flat key stays published, NOT READ, until the spec\'s v18 retirement. `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).',
},
'element:record_picker.dataSource': {
file: 'packages/components/src/__tests__/record-picker-element-data-source.test.tsx',
pins: 'The per-element binding\'s own members, read through the REAL renderer and asserted at the query it fires: `view` resolves a saved view whose `filter`/`sort`/row cap become the picker\'s baseline, an authored binding key WINS over the same key from the view, the binding\'s own `filter` arrives AND-combined with the view\'s rather than replacing it, and an unresolvable `view` reports instead of falling back to every record of the object — the quieter failure mode objectstack#6953 fixed, one class below the throw objectstack#5576 fixed on `list-view`. A `view`-less binding leaves every one of the four flat keys (`object`/`filter`/`sort`/`limit`) behaving exactly as it did before a `dataSource` was authored at all, which is the control that keeps the override rows from reading as a coincidence. `columns` is deliberately absent from this pin: unlike `record:related_list.dataSource` (objectui#8071 slice 2), this renderer never reads `composed.columns` at all — a select dropdown has no column list to narrow. Pre-existing file, promoted to a pin here after being read end to end (objectui#8071).',
pins: 'The per-element binding\'s own members, read through the REAL renderer and asserted at the query it fires: `view` resolves a saved view whose `filter`/`sort`/row cap become the picker\'s baseline, an authored binding key WINS over the same key from the view, the binding\'s own `filter` arrives AND-combined with the view\'s rather than replacing it, and an unresolvable `view` reports instead of falling back to every record of the object — the quieter failure mode objectstack#6953 fixed, one class below the throw objectstack#5576 fixed on `list-view`. A `view`-less binding passes its own keys through as authored, which is the control that keeps the override rows from reading as a coincidence; and since objectui#11880 the four flat keys (`object`/`filter`/`sort`/`limit`) are read by nothing — a flat `properties.object` alone issues no query, and beside a binding the flat `filter`/`sort`/`limit` change nothing. `columns` is deliberately absent from this pin: unlike `record:related_list.dataSource` (objectui#8071 slice 2), this renderer never reads `composed.columns` at all — a select dropdown has no column list to narrow. Pre-existing file, promoted to a pin here after being read end to end (objectui#8071).',
},
'element:record_picker.emptyText': {
file: 'apps/console/src/__tests__/component-input-union-specimens.test.ts',
pins: 'The `object` arm is the inline translation map, asserted at the render site that actually resolves one, with the non-matching controls (`42`, `["No records"]`) still reported (objectui#3832).',
},
'element:record_picker.filter': {
file: 'packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts',
pins: 'The `array` arm is `ViewFilterRuleSchema[]` — the one filter orthography this map\'s array-declared `filter` doors share — with the MongoDB-style RECORD spelling (`{ status }`, `{ $and }`) refused by kind at `filter` and a bare tuple refused at `filter.0`, and the description pinned to the renderer\'s own precedence read (`composed?.filter ?? props.filter`). objectui#3830 declared the key; objectstack#14406 converged it off the record form and objectui#7663 moved this side onto the array arm.',
pins: 'The `array` arm is `ViewFilterRuleSchema[]` — the one filter orthography this map\'s array-declared `filter` doors share — with the MongoDB-style RECORD spelling (`{ status }`, `{ $and }`) refused by kind at `filter` and a bare tuple refused at `filter.0`, and the description pinned to say the key is NOT READ and to name `dataSource.filter`, the one filter the renderer reads since objectui#11880 (`composed?.filter`). objectui#3830 declared the key; objectstack#14406 converged it off the record form and objectui#7663 moved this side onto the array arm; it stays published until the spec\'s v18 retirement (objectstack#11509).',
},
'element:record_picker.label': {
file: 'packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx',
Expand All @@ -2800,7 +2800,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'element:record_picker.sort': {
file: 'packages/components/src/__tests__/record-picker-sort-members.test.tsx',
pins: 'An IDENTITY pin, unlike the sibling block\'s: `record-picker.tsx` assigns `query.$orderby = sort` with no `normalizeSortSpec` in between, so a member missing `field` is NOT dropped the way `record:related_list.sort` (objectui#8071 slice 2) drops one — pinned as the current, undocumented divergence between two blocks whose registrations both say only "array of objects". Order is preserved for a multi-member array, no `$orderby` is sent when the key is absent, and PRECEDENCE against `dataSource` is pinned in both directions: a binding that declares its own `sort` replaces the flat key outright, and the flat key still applies when the binding carries none — `composed?.sort ?? props.sort`, identical to `filter`\'s read (objectui#8071).',
pins: 'An IDENTITY pin, unlike the sibling block\'s: `record-picker.tsx` assigns `query.$orderby = sort` with no `normalizeSortSpec` in between, so a member missing `field` is NOT dropped the way `record:related_list.sort` (objectui#8071 slice 2) drops one — pinned as the current, undocumented divergence between two blocks whose registrations both say only "array of objects". Order is preserved for a multi-member array and no `$orderby` is sent when the key is absent. Since objectui#11880 the read is `composed?.sort` alone, so these rows drive the binding\'s `dataSource.sort`, and the flat `sort` — still published, its description saying NOT READ — is pinned unread both beside a binding that declares its own `sort` and beside one that carries none (objectui#8071, objectui#11880).',
},
'element:text.content': {
file: 'apps/console/src/__tests__/component-input-union-specimens.test.ts',
Expand Down
27 changes: 15 additions & 12 deletions content/docs/guide/data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,20 +303,23 @@ rows, a form's inline fields — `customFields`, or `sections` whose every field
inline — a chart's `dataset`, a `bind` path, or a metric's `fallbackValue`) draws
from that source and shows no hint.

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`,
The two `element:*` rows keep their display configuration in the node's
`properties` bag and take their query from the binding **only**: every query
key either one reads comes from `dataSource` and from nowhere else. The flat
`properties.object`, `properties.filter`, `properties.sort` and
`properties.limit` that the spec still declares on these two elements are not
read, and `@objectstack/spec` retires them in v18 (objectstack#11509). The
binding's filter is AND-combined with its view's filter, as on every block
above. 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. An `element:number`
that sets `aggregate` but names no object in either place (no `properties.object`,
no `dataSource.object`) shows a short "no object named" notice instead of a count.
has no ordering and a capped count would be a wrong number, and a filter the
converter refuses shows the configuration-error panel instead of a count. An
element whose node names no `dataSource.object` issues no query: an
`element:number` that sets `aggregate` shows a short "No object named: set
dataSource.object." notice instead of a count, and an `element:record_picker`
offers no records. The Studio page designer writes the metric's object into
`dataSource`.

#### Scoping a filter to the record in view: `{record_id}`

Expand Down
Loading
Loading