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
10 changes: 10 additions & 0 deletions .changeset/8071-object-metric-query-member-pins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---

Pin the four `object-metric` members that shape the aggregate query behind the
number — `dataSource`, `aggregate`, `filter`, `compareTo` (objectui#8071, slice
8). `MEMBER_PIN_EXEMPTIONS` drops 37 → 33 and `MEMBER_PIN_EXEMPTION_CEILING`
follows in the same commit; `trend` and `drillDown`, which shape what is drawn
around the number rather than the number, stay exempt.

Test only; no package is released by this change.
102 changes: 96 additions & 6 deletions apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,22 @@ const MEMBER_PINS: Record<string, MemberPin> = {
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',
pins: 'ONE nested position and no more: `schema.grouping?.fields?.[0]?.field` is the FALLBACK source of `swimlaneField`, and that is the entire member contract this board carries for the key. Three rows make it a reading rather than a claim — the swimlane layout appears keyed by `fields[0].field` where without the key there is none; an explicit `swimlaneField` WINS over it; and a second `fields` entry changes nothing, which is what pins the read at `[0]` rather than at "the fields list". The declared description says the rest is inert precisely so the declaration does not recommend a write the renderer cannot honour — this file is what keeps that sentence true. The spec row is `z.unknown()`, so the read site is the whole member contract (objectui#8313).',
},
'object-metric.aggregate': {
file: 'packages/plugin-dashboard/src/__tests__/objectMetricQueryMembers-8071.test.tsx',
pins: 'A three-member options bag whose members are read TWICE — once into the adapter call, once back out of the response. `field`/`function`/`groupBy` become `ds.aggregate(object, { field, function, groupBy, filter })` with `groupBy` OPTIONAL and defaulting to `\'_all\'` (one bucket) and an authored value outranking it; the bag is asserted AS A WHOLE so a member added to or dropped from the call is red in either direction. The readback is the half that has no second chance: `function: \'count\'` sums `<field>_count` across EVERY returned row while any other function reads the FIRST row\'s `<field>_<function>`, and the two arms run on the SAME two-row response so only a readback reading both members can answer 120 on one and 7 on the other — a `field`/`function` that reached the query but not the readback paints 0 over a response that carried the right number. `<field>` unsuffixed is pinned as the chain\'s second limb. The key\'s ABSENCE is a member semantic in its own right and is the file\'s non-vacuity floor: with no `aggregate` the query VERB changes to `find()` and the value becomes the row count. Driven through the registered block, not the bare widget. New file (objectui#8071 slice 8).',
},
'object-metric.compareTo': {
file: 'packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.test.tsx',
pins: 'The `{ kind, dimension? }` member set under objectstack#5011\'s converged shape: `kind` alone chooses BOTH the comparison window and the trend label, pinned on a QUARTER-scoped filter so the two admitted values disagree about each (`previousYear` = the same quarter one year back and "vs last year"; `previousPeriod` = the quarter before and "vs last quarter") — a year-scoped fixture would have let a `previousYear` that fell through to the other branch pass. `dimension` is CARRIED but inert on this inline path (it addresses a dataset time dimension the executor resolves), asserted by the query\'s filter keys being unchanged by it. An absent `compareTo` runs a single pass and paints no trend, which is the control that keeps the other rows from reading as a coincidence. Pre-existing file (objectui#3337), promoted here after being read end to end; objectui#8071 slice 8 added the row that mounts `type: \'object-metric\'` through `SchemaRenderer`, so the claim that the block\'s gate forwards this key untouched is asserted rather than assumed.',
},
'object-metric.dataSource': {
file: 'packages/plugin-dashboard/src/ObjectMetric.elementDataSource.test.tsx',
pins: 'The per-element binding\'s members as a WHITELIST in both directions. Acting: `object` is what gets aggregated, and a named `view`\'s own `filter` becomes the metric\'s scope — with an unresolvable `view` REPORTING instead of aggregating the whole object, which for a metric is the quiet failure (one number, no rows, nothing to notice). Not acting: the same view fixture declares `columns`, `sort` and `pagination`, and the aggregate options bag is asserted whole to keep all three OUT — `OBJECT_METRIC_DATA_SOURCE` names only `filter`, because a metric is one aggregated number with no projection, ordering or page for the rest to act on. A metric with NO binding behaving exactly as before is the control. Pre-existing file (objectstack#6953), promoted here after being read end to end; objectui#8071 slice 8 added the whole-bag row, without which the pin would have been satisfied by a mapping that forwarded everything.',
},
'object-metric.filter': {
file: 'packages/plugin-dashboard/src/__tests__/objectMetricQueryMembers-8071.test.tsx',
pins: 'No named member set — the renderer never inspects the predicate — so the member shape is the SPELLING it arrives under, and there are two, chosen by an adapter capability the author cannot see: FLAT under its own name inside the aggregate options bag, and WRAPPED as `$filter` on the no-`aggregate()` `find()` fallback. Collapsing them into one drops the predicate on whichever path lost and the tile counts every row — the same defect `element:number.filter` was pinned for (slice 7) on a different renderer. Two further halves: placeholders are resolved BEFORE the query (an authored `{current_quarter_start}` reaches the adapter as a real date, and a macro surviving onto the wire is a literal nobody matches), and the key is read BY VALUE rather than by identity (`JSON.stringify` memo) — a deep-equal rebuild 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 against a dependency "simplified" to the raw object. New file (objectui#8071 slice 8).',
},
'page:accordion.items': {
file: 'packages/components/src/__tests__/pageAccordionItemMembers-8071.test.tsx',
pins: 'Which member of one panel definition becomes which part of the rendered accordion — the four `PageAccordionItem` members (`label`, `icon`, `collapsed`, `children`) asserted as a SET through the real renderer, which nothing did before: `label` becomes the trigger\'s accessible name and `children` the panel BODY, asserted against each other so a renderer painting the wrong one cannot pass. `collapsed` is the reading with real semantics to get wrong and it is strictly `=== false`: `collapsed: false` OPENS a panel while `collapsed: true` AND an omitted `collapsed` both leave it shut — the omitted-key arm is the control an "obvious" edit to `!it.collapsed` breaks, and a no-opener fixture keeps the two shut rows from passing on a renderer that opens nothing. The single/multiple split is pinned on the SAME items so only `allowMultiple` varies: single mode takes `defaultOpen[0]` and drops later openers, multiple mode opens them all without opening panels that never asked. `icon` is covered narrowly on purpose — `page-accordion-icon.test.tsx` (objectui#4721) is its pin and is not re-litigated. New file (objectui#8071 slice 7).',
Expand Down Expand Up @@ -2508,12 +2524,11 @@ const MEMBER_PIN_EXEMPTIONS: Record<string, string> = {
'object-master-detail-form.initialValues': AWAITING_A_PIN,
'object-master-detail-form.sections': AWAITING_A_PIN,

// object-metric
'object-metric.aggregate': AWAITING_A_PIN,
'object-metric.compareTo': AWAITING_A_PIN,
'object-metric.dataSource': AWAITING_A_PIN,
// object-metric — objectui#8071 slice 8 pinned the four members that shape
// the aggregate query behind the NUMBER (`dataSource`, `aggregate`, `filter`,
// `compareTo`). The two left shape what is drawn AROUND the number once it
// exists, and neither reaches `fetchMetric`.
'object-metric.drillDown': AWAITING_A_PIN,
'object-metric.filter': AWAITING_A_PIN,
'object-metric.trend': AWAITING_A_PIN,

// page:accordion — objectui#8071 slice 7 pinned `items`, the block's one
Expand Down Expand Up @@ -2811,11 +2826,86 @@ const NEWLY_JUDGED_UNPINNED_MEMBERS = [
* behaviour in the new file, which authors `bodyShape` and asserts it is
* dropped.
*
* ## 37 -> 33, the eighth slice, and the FIRST partial bite of a multi-key block
*
* objectui#8071's eighth slice is the first that could not close a block by
* taking its last key — slice 7 exhausted that shape — so it takes a PART of
* one, and the part is chosen by a rule rather than by convenience:
*
* **every `object-metric` member that shapes the aggregate query behind the
* NUMBER the tile paints.**
*
* Four qualify — `dataSource`, `aggregate`, `filter` and `compareTo` — so the
* ceiling follows to 33 in the same commit. The rule is mechanical, not
* editorial: those four are exactly the members that reach
* `ObjectMetricWidget`'s `fetchMetric` / `computeOne` (`dataSource` gates the
* effect; `aggregateKey`, `resolvedFilterKey` and `compareToKey` are its other
* dependencies), and the two left — `trend` and `drillDown` — reach neither.
* They shape what is drawn AROUND the number once it exists: a STATIC badge
* beside it, and a drawer opened by clicking it. Neither can change the number.
*
* ⇒ That is what the next slice inherits, and it is cheap to state: the two
* PRESENTATION members of `object-metric`, `trend` and `drillDown`. They were
* left together on purpose rather than one being swept in — `drillDown` does
* issue a query of its own (the drawer's record list, off the same resolved
* filter), so "reaches an adapter" would have been the wrong cut and is stated
* here so the next reader does not have to re-derive why it was not used.
*
* TWO of the four pins PROMOTE pre-existing files, each read end to end before
* being credited and each given the one row it was missing:
*
* - `ObjectMetric.elementDataSource.test.tsx` (objectstack#6953) already drove
* the binding's `object` and `view` members through the real renderer, with
* an unresolvable `view` reporting rather than aggregating the whole object
* and a binding-less metric unchanged. What it did not have was the
* NEGATIVE direction, so this slice adds it: the view fixture's `columns`,
* `sort` and `pagination` are asserted absent from the aggregate call.
* Without that the pin would have been satisfied by a mapping that forwarded
* everything, which here would mean a metric silently ordered and paged by a
* list view's presentation settings.
* - `ObjectMetricWidget.compareTo.test.tsx` (objectui#3337) already asserted
* the `{ kind, dimension? }` member set — `kind` choosing both window and
* label on a quarter-scoped fixture where the two values disagree about
* each, `dimension` carried but inert, and the no-`compareTo` single-pass
* control. It drove `ObjectMetricWidget` directly, so this slice adds the
* row that mounts `type: 'object-metric'` through `SchemaRenderer`: the
* block's `ElementDataSourceGate` shell re-binds only `objectName` and
* `filter` and forwards the rest untouched, and that is now asserted rather
* than assumed. ⚠️ It is also why the file names the block at all — the
* locator requires it, and a docblock mention alone would have been the
* locator satisfied by prose rather than by behaviour.
*
* `aggregate` and `filter` share ONE new file,
* `objectMetricQueryMembers-8071.test.tsx` — the shape slice 3 used for
* `record-picker-label-placeholder-i18n.test.tsx` and slice 6 for the
* chatter/discussion pair — because the two keys meet inside a single adapter
* call and the assertions have to be written against each other to stay
* independent: the `aggregate` rows author NO filter (so the bag comparison is
* blind to the filter spelling), and the `filter` rows are the ones that pin it.
*
* Every other candidate was read end to end and rejected with a reason, not
* dismissed on its greps: `public-block-binding-reach.test.tsx` states its own
* narrowness — one question per block, "did any call carry the object name" —
* and lists `aggregate` only as a plausible sample value;
* `widget-dom-leak-sweep.test.tsx` is the DOM-attribute canary, where
* `aggregate` is a stub returning `[]`; `ObjectMetricWidget.i18nLabel.test.tsx`
* authors `aggregate`, `trend` and `drillDown` purely to make the drill-down
* reachable, and its subject is `I18nLabel` resolution — crediting it for
* `trend` or `drillDown` would have been slice 7's
* `action-bodyShape-forward.test.tsx` mistake in a new place. The remaining
* seven files that name `object-metric` do so in prose, in a membership list,
* or in a designer-inspector fixture.
*
* ⚠️ Unchanged by this slice: `NEWLY_JUDGED_UNPINNED_MEMBERS` (no block it names
* was touched) and `record:related_list.actions`, whose `NO_READ_SITE_TO_PIN`
* reading was not re-measured here because this slice took a different block —
* slice 7's measurement stands as the last one taken.
*
* ⇒ The rule for every future slice of objectui#8071: delete the entry, register
* the pin, and set this constant to the new count. Not to the new count plus
* room.
*/
const MEMBER_PIN_EXEMPTION_CEILING = 37;
const MEMBER_PIN_EXEMPTION_CEILING = 33;

/**
* Every test file a member pin can live in, as LAZY `?raw` loaders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@
* `object` and `filter` are the only mapped keys: a metric is one aggregated
* number, so there is no projection, no ordering and no page for the binding's
* remaining keys to act on.
*
* ## Promoted to the member pin for `object-metric.dataSource` (objectui#8071)
*
* objectui#8071's eighth slice registers this file as the pin for that key. The
* member set it asserts is a WHITELIST in both directions: `object` and `view`
* are the members that act — the bound object is what gets aggregated and the
* named view's own `filter` becomes the metric's scope — while the view's
* `columns`, `sort` and `pagination` are carried in the fixture and asserted
* ABSENT from the call, because `OBJECT_METRIC_DATA_SOURCE` names only
* `filter`. Without that second direction the pin would be satisfied by a
* mapping that forwarded everything, which for this block would mean a metric
* silently ordered and paged by a list view's presentation settings.
*/

import { describe, it, expect, vi } from 'vitest';
Expand Down Expand Up @@ -99,6 +111,32 @@ describe('object-metric — dataSource: { object, view } (objectstack#6953)', ()
expect(adapter.aggregate).not.toHaveBeenCalled();
});

it('maps `object` and the view’s `filter` and NOTHING else the view carries', async () => {
// The whitelist direction. `HOT_VIEW` declares `columns`, `sort` and
// `pagination`; none of the three is a member this block reads, so none may
// appear in the aggregate call. Asserted on the WHOLE options bag rather
// than key by key, so a mapping widened to forward the rest is red here.
const adapter = makeAdapter();
renderBlock(
{
type: 'object-metric',
label: 'Pipeline',
aggregate: AGGREGATE,
dataSource: { object: 'account', view: 'hot' },
},
adapter,
);

await waitFor(() => expect(adapter.aggregate).toHaveBeenCalled());
const [, params] = adapter.aggregate.mock.calls[0] as [string, any];
expect(params).toEqual({
field: 'amount',
function: 'sum',
groupBy: '_all',
filter: [['rating', '=', 'hot']],
});
});

it('leaves a metric with NO dataSource exactly as it was', async () => {
const adapter = makeAdapter();
renderBlock(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,30 @@
* before) and the label (`vs last year` / `vs last quarter`), so each case can
* only pass by reading `.kind`. A year-scoped filter would have let a
* `previousYear` that fell through to the previousPeriod branch pass anyway.
*
* ## Promoted to the member pin for `object-metric.compareTo` (objectui#8071)
*
* objectui#8071's eighth slice registers this file as the pin for that key: the
* cases below already assert the member SET the renderer reads — `kind` selects
* both the comparison window and the label, `dimension` is carried but inert on
* this inline path, and an absent `compareTo` leaves the tile at one pass with
* no trend — which is what a member-shape claim has to say.
*
* The four cases above drive `ObjectMetricWidget` directly. The registered
* block `object-metric` is the same reader: its `ElementDataSourceGate` shell
* re-binds only `objectName` and `filter` and forwards every other authored key
* untouched, so `compareTo` arrives at this component exactly as authored. That
* is asserted rather than assumed by the last case in this file, which mounts
* `type: 'object-metric'` through `SchemaRenderer`.
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup, waitFor } from '@testing-library/react';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
import { ObjectMetricWidget } from '../ObjectMetricWidget';
// Registers `object-metric` at MODULE scope, never inside a hook
// (object-ui/no-dynamic-import-in-test-hook).
import '../index';

afterEach(cleanup);

Expand Down Expand Up @@ -101,4 +120,30 @@ describe('ObjectMetricWidget — compareTo under { kind }', () => {
await waitFor(() => expect(src.aggregate).toHaveBeenCalledTimes(1));
expect(screen.queryByText(/vs last/i)).not.toBeInTheDocument();
});

it('arrives unchanged through the registered `object-metric` block', async () => {
// What makes the four cases above a pin on the BLOCK's key rather than on
// one component's prop: authored on the schema, `compareTo` passes the
// `ElementDataSourceGate` shell untouched and shifts the window exactly as
// it does when handed straight to the widget.
const src = makeSource();
render(
<SchemaRendererProvider dataSource={src as never}>
<SchemaRenderer
schema={{
type: 'object-metric',
objectName: 'deal',
label: 'Revenue',
aggregate: { field: 'amount', function: 'sum' },
filter: { close_date: { $gte: '{current_quarter_start}', $lte: '{current_quarter_end}' } },
compareTo: { kind: 'previousYear' },
} as never}
/>
</SchemaRendererProvider>,
);

await waitFor(() => expect(src.aggregate).toHaveBeenCalledTimes(2));
expect(comparisonFilterOf(src).$gte).toBe(PREVIOUS_YEAR_FROM);
expect(await screen.findByText(/vs last year/i)).toBeInTheDocument();
});
});
Loading
Loading