diff --git a/.changeset/dashboard-global-filter-field-resolution-target.md b/.changeset/dashboard-global-filter-field-resolution-target.md new file mode 100644 index 0000000000..68c61febec --- /dev/null +++ b/.changeset/dashboard-global-filter-field-resolution-target.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": patch +--- + +`Dashboard.globalFilters[].field` now describes WHERE the name resolves, instead of only that it is a field name. + +The key's `.describe()` read `Field name to filter on` — true, but silent on the one thing authors get wrong. A dashboard global filter's `field` resolves against the object behind each bound widget's dataset (`dataset.object`), never against that dataset's declared `dimensions`; `widgets[].dimensions[]` selects from that second, separate namespace by name. Readers who assumed the two were the same namespace concluded a filter needs a matching dataset dimension, which is not so. + +The rewritten description states the resolution target, scopes the claim to the authoring layer, and names the rule that already enforces it — `dashboard-filter-field-unknown`, `severity: 'error'` in `@objectstack/lint`'s widget-binding validator. It is a statement about a rule that already fires, not a suggestion. + +The mirrored TSDoc carries the half a one-line description cannot: the separation holds for the **authorable surface** only. It is NOT a claim that an object field can never serve as a dimension — the analytics query API does accept an object's own field as an ad-hoc dimension without the dataset declaring it, and `widget-dimension-unknown` (also `severity: 'error'`) is what holds that line for authored dashboards. + +**What moves for consumers.** The string is a published datum, not a comment: it is the `description` of the `field` property in the shipped JSON Schema (`json-schema/ui/GlobalFilter.json`, `json-schema/ui/Dashboard.json`, `json-schema/objectstack.json`) and the runtime `.description` on the Zod schema in `dist`, so anything that renders schema descriptions — editor hovers, generated reference pages, prompt builders — shows the new sentence. No accept set moves: the key stays `z.string()`, nothing that parses today stops parsing, and no validation behaviour changes. + +The same wording already stands on the hand-written page (`content/docs/ui/dashboards.mdx` → **Where a Filter's `field` Resolves**); the generated reference page now agrees with it rather than trailing it. diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index d78c62471b..4173838ab8 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -97,7 +97,7 @@ const result = DashboardSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable filter name (variable key); defaults to field | -| **field** | `string` | ✅ | Field name to filter on | +| **field** | `string` | ✅ | Field name to filter on — at the authoring layer it resolves against the object behind each bound widget's dataset (`dataset.object`), not against that dataset's declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error) | | **object** | `string` | optional | Object whose `fields..` translation-bundle entry resolves this filter's field label and option labels | | **label** | `string \| Record` | optional | Display label for the filter | | **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | @@ -275,7 +275,7 @@ Widget configuration — declared query keys + open renderer extras | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable filter name (variable key); defaults to field | -| **field** | `string` | ✅ | Field name to filter on | +| **field** | `string` | ✅ | Field name to filter on — at the authoring layer it resolves against the object behind each bound widget's dataset (`dataset.object`), not against that dataset's declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error) | | **object** | `string` | optional | Object whose `fields..` translation-bundle entry resolves this filter's field label and option labels | | **label** | `string \| Record` | optional | Display label for the filter | | **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index fa67cfc806..c2e7086b9e 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -800,8 +800,29 @@ export const GlobalFilterSchema = lazySchema(() => strictObject({ */ name: z.string().optional().describe('Stable filter name (variable key); defaults to field'), - /** Field name to filter on */ - field: z.string().describe('Field name to filter on'), + /** + * Field name to filter on — at the authoring layer it resolves against the + * object behind each bound widget's dataset (`dataset.object`), not against + * that dataset's declared `dimensions`; enforced by the lint rule + * `dashboard-filter-field-unknown` (severity error). + * + * The sibling `object` key does not move this: it names the object a + * translator's bundle entry is keyed by and is read for LABEL resolution + * only. `dashboard-filter-field-unknown` (exported as + * `DASHBOARD_FILTER_FIELD_UNKNOWN`) resolves against the widget's own + * dataset — `validateWidgetBindings` seeds its base with + * `const datasetObject = typeof dataset.object === 'string'` — and never + * reads a filter-level `object`. + * + * `dimensions` is a separate namespace, the one `widgets[].dimensions[]` + * selects from BY NAME (`widget-dimension-unknown`, also severity error). + * That separation is a statement about the AUTHORABLE SURFACE only — it is + * NOT a claim that an object field can never serve as a dimension: the + * analytics query API does accept an object's own field as an ad-hoc + * dimension without the dataset declaring it, and `widget-dimension-unknown` + * is what holds that line for authored dashboards. + */ + field: z.string().describe('Field name to filter on — at the authoring layer it resolves against the object behind each bound widget\'s dataset (`dataset.object`), not against that dataset\'s declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error)'), /** * Source object for i18n label resolution (#7804): when set, this filter's