= {}) {
+ return render(
+
+
+ ,
+ );
+}
+
+describe('DashboardFilterBar — inline per-locale filter labels (#4032 / #4163)', () => {
+ it('(c) resolves a map-valued filter label in the trigger text and the aria-label', () => {
+ const { container } = renderIn('zh', [
+ {
+ name: 'owner',
+ field: 'owner',
+ type: 'select',
+ label: { en: 'Owner', 'zh-CN': '负责人' },
+ options: [{ value: 'alice', label: 'Alice' }],
+ } as unknown as DashboardFilterDef,
+ ]);
+
+ expect(container.innerHTML).not.toContain('[object Object]');
+ const trigger = screen.getByTestId('dashboard-filter-owner');
+ expect(trigger.getAttribute('aria-label')).toBe('负责人');
+ expect(trigger.textContent).toContain('负责人');
+ });
+
+ it('(c2) a label map that resolves to nothing falls through to the filter name', () => {
+ // The truthiness half — an object is always truthy, so `def.label ||
+ // def.name` never reached `def.name` for ANY object, empty or not.
+ const { container } = renderIn('zh', [
+ { name: 'region', field: 'region', type: 'select', label: {}, options: [] } as unknown as DashboardFilterDef,
+ ]);
+
+ expect(container.innerHTML).not.toContain('[object Object]');
+ expect(screen.getByTestId('dashboard-filter-region').getAttribute('aria-label')).toBe('region');
+ });
+
+ it('(c3) resolves a map label on the text filter — placeholder AND aria-label', () => {
+ renderIn('zh', [
+ { name: 'note', field: 'note', type: 'text', label: { en: 'Note', 'zh-CN': '备注' } } as unknown as DashboardFilterDef,
+ ]);
+
+ const input = screen.getByTestId('dashboard-filter-note');
+ expect(input.getAttribute('placeholder')).toBe('备注');
+ expect(input.getAttribute('aria-label')).toBe('备注');
+ });
+
+ it('(c4) resolves a map label on the date-range filter aria-label', () => {
+ renderIn('zh', [
+ { name: 'dateRange', field: 'created_at', type: 'dateRange', label: { en: 'Period', 'zh-CN': '期间' } } as unknown as DashboardFilterDef,
+ ]);
+
+ expect(screen.getByLabelText('期间')).toBeTruthy();
+ });
+
+ it('(d) keeps a map-valued OPTION label and resolves it per locale', () => {
+ // Through the real normalizer, because the discard happens there — a def
+ // hand-written in the object form would not exercise the reported defect.
+ const defs = resolveDashboardFilterDefs({
+ globalFilters: [
+ {
+ name: 'channel',
+ field: 'sales_channel',
+ type: 'select',
+ options: [{ value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } }],
+ },
+ ],
+ } as never);
+
+ renderIn('zh', defs, { channel: 'domestic' });
+ expect(screen.getByTestId('dashboard-filter-channel').textContent).toContain('国内');
+ });
+
+ it('(d2) an `en` UI keeps the AUTHORED english option text, not the stored value', () => {
+ // The half that makes this data loss rather than a translation gap: before
+ // the change this rendered `domestic`, the raw stored value, in en too.
+ const defs = resolveDashboardFilterDefs({
+ globalFilters: [
+ {
+ name: 'channel',
+ field: 'sales_channel',
+ type: 'select',
+ options: [{ value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } }],
+ },
+ ],
+ } as never);
+
+ renderIn('en', defs, { channel: 'domestic' });
+ expect(screen.getByTestId('dashboard-filter-channel').textContent).toContain('Domestic');
+ });
+
+ it('(e0) an UNLABELLED date filter keeps its translated fallback, not the reserved name', () => {
+ // Regression pin for the shape of the fix, not for the reported defect.
+ // The three controls do NOT share a fallback — `dateRange` falls back to a
+ // translated "Date range", the others to `def.name` — so a resolver that
+ // helpfully applied `|| def.name` itself would have made this control read
+ // the reserved string `dateRange`. Caught while writing the helper; pinned
+ // so the next simplification cannot re-collapse them.
+ renderIn('en', [
+ { name: 'dateRange', field: 'created_at', type: 'dateRange' } as unknown as DashboardFilterDef,
+ ]);
+
+ expect(screen.getByLabelText('Date range')).toBeTruthy();
+ expect(screen.queryByLabelText('dateRange')).toBeNull();
+ });
+
+ it('(e) leaves plain-string labels and options exactly as authored', () => {
+ // Non-vacuity + acceptance boundary for all of the above.
+ const defs = resolveDashboardFilterDefs({
+ globalFilters: [
+ {
+ name: 'stage',
+ field: 'stage',
+ type: 'select',
+ label: 'Stage',
+ options: [{ value: 'won', label: 'Won' }, 'lost'],
+ },
+ ],
+ } as never);
+
+ expect(defs[0].options).toEqual([
+ { value: 'won', label: 'Won' },
+ { value: 'lost', label: 'lost' },
+ ]);
+
+ renderIn('zh', defs, { stage: 'won' });
+ const trigger = screen.getByTestId('dashboard-filter-stage');
+ expect(trigger.getAttribute('aria-label')).toBe('Stage');
+ expect(trigger.textContent).toContain('Won');
+ });
+});
diff --git a/packages/plugin-dashboard/src/__tests__/DashboardRenderer.metricI18n.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardRenderer.metricI18n.test.tsx
new file mode 100644
index 000000000..490213b82
--- /dev/null
+++ b/packages/plugin-dashboard/src/__tests__/DashboardRenderer.metricI18n.test.tsx
@@ -0,0 +1,190 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#4032 (source thread objectstack#5428) — a `type: 'metric'` KPI card
+ * drops the translated widget title while every OTHER widget type on the same
+ * dashboard renders it.
+ *
+ * TWO INDEPENDENT CHANNELS reach a widget title, and the metric path was
+ * disconnected from both:
+ *
+ * 1. **The convention-key channel** — `{ns}.dashboards.{dash}.widgets.{id}.title`,
+ * resolved by `tWidgetTitle`. `DashboardRenderer` computes `resolvedTitle`
+ * for every widget, but the self-contained metric branch renders no Card
+ * header, so the value was computed and thrown away; the metric dispatch
+ * built its own label from the raw authored `widget.title` instead.
+ * 2. **The inline per-locale map** — `{ en: 'Revenue', 'zh-CN': '收入' }`, which
+ * `@objectstack/spec` 17.0.0-rc.6 admits on `DashboardWidget.title`
+ * (`I18nLabel`). The private `resolveLabel` copy this file's subject used
+ * read the RETIRED `{ key, defaultValue }` form (objectstack#5055) and
+ * answered `undefined` for a map — see the direction note below.
+ *
+ * DIRECTIONS, written before the reverse verification was run:
+ *
+ * - **(a) bundle-translated title/description → RED before the change.** The
+ * metric branch never consulted `tWidgetTitle`, so the card rendered the
+ * authored English in a `zh` UI.
+ * - **(b) inline map title → RED before the change, and NOT as `[object
+ * Object]`.** This is the prediction worth writing down, because it is not
+ * the failure the sibling surface had. `resolveLabel` ended
+ * `return label.defaultValue || label.key`, and a locale map has neither —
+ * so it returned `undefined`, and the caller's `|| widgetType` fallback took
+ * over. The card therefore rendered the literal widget TYPE, the string
+ * `"metric"`. A map that stringifies is at least visible; this one silently
+ * substitutes a plausible-looking word.
+ * - **(f) controls → GREEN on both sides.** A plain-string title must stay
+ * byte-identical, and a non-metric widget's title path is untouched. These
+ * are the acceptance boundary, not decoration: the fix must not move a
+ * dashboard that has no translations at all.
+ */
+
+import * as React from 'react';
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import { I18nProvider } from '@object-ui/i18n';
+import type { DashboardComponentSchema } from '@object-ui/types';
+// The dashboard renders each widget through `SchemaRenderer`, which resolves
+// `metric` / `chart` from the registry — populated as a side effect of the
+// package barrel. Imported at MODULE scope (never in a hook) per AGENTS.md's
+// flaky-test rule: the cost lands in the import phase, unbounded by any
+// test/hook timeout.
+import { DashboardRenderer } from '../index';
+
+afterEach(cleanup);
+
+/**
+ * The app bundle. `crm` is discovered as an app namespace because it carries a
+ * `dashboards` sub-key — the same discovery `useObjectLabel` performs for every
+ * other convention lookup.
+ */
+const ZH_BUNDLE = {
+ zh: {
+ crm: {
+ dashboards: {
+ sales: {
+ widgets: {
+ revenue: { title: '总收入', description: '本季度已赢单' },
+ pipeline: { title: '销售漏斗' },
+ },
+ },
+ },
+ },
+ },
+};
+
+function dashboard(widget: Record
): DashboardComponentSchema {
+ return {
+ type: 'dashboard',
+ name: 'sales',
+ widgets: [widget],
+ } as unknown as DashboardComponentSchema;
+}
+
+/** The KPI card's heading row — the slot every assertion below is about. */
+function cardHeading(): HTMLElement {
+ const el = document.querySelector('.tracking-tight.text-sm.font-medium');
+ if (!el) throw new Error('metric card heading not rendered');
+ return el as HTMLElement;
+}
+
+function renderIn(language: string, schema: DashboardComponentSchema) {
+ return render(
+
+
+ ,
+ );
+}
+
+describe('DashboardRenderer — KPI cards translate from the widget convention keys (#4032)', () => {
+ it('(a) renders the bundle title on a self-contained metric card', () => {
+ renderIn('zh', dashboard({
+ id: 'revenue',
+ type: 'metric',
+ title: 'Total Revenue',
+ options: { value: 1930000 },
+ }));
+
+ expect(screen.getByText('总收入')).toBeTruthy();
+ // The authored English must not survive beside the translation.
+ expect(screen.queryByText('Total Revenue')).toBeNull();
+ });
+
+ it('(b) resolves an inline per-locale title map, and never leaks the widget type', () => {
+ renderIn('zh', dashboard({
+ id: 'unkeyed',
+ type: 'metric',
+ title: { en: 'Total Revenue', 'zh-CN': '总收入' },
+ options: { value: 1930000 },
+ }));
+
+ expect(screen.getByText('总收入')).toBeTruthy();
+ // The pre-fix failure mode: `resolveLabel` answered `undefined` for a map,
+ // so `|| widgetType` rendered the literal string "metric".
+ expect(screen.queryByText('metric')).toBeNull();
+ // And the sibling surface's failure mode, which this path never had.
+ // Asserted on the HEADING, not on `container.innerHTML`: the card also
+ // carries an unrelated `schema="[object Object]"` DOM attribute, present on
+ // this path before and after this change and on plain-string titles too —
+ // `SchemaRenderer` hands the widget schema down and `MetricWidget` spreads
+ // `...props` onto the `Card`. Filed separately rather than widened into
+ // this pin, which would have made the assertion pass for the wrong reason.
+ expect(cardHeading().innerHTML).not.toContain('[object Object]');
+ });
+
+ it('(b2) resolves the inline map for `en` too — the authored en text, not the raw map', () => {
+ renderIn('en', dashboard({
+ id: 'unkeyed',
+ type: 'metric',
+ title: { en: 'Total Revenue', 'zh-CN': '总收入' },
+ options: { value: 1930000 },
+ }));
+
+ expect(screen.getByText('Total Revenue')).toBeTruthy();
+ expect(cardHeading().innerHTML).not.toContain('[object Object]');
+ });
+
+ it('(b3) prefers the bundle over the inline map when both exist', () => {
+ // Precedence is the same one `useObjectLabel` applies everywhere: the
+ // authored value is collapsed to the active language FIRST and then offered
+ // to the bundle as its fallback, so a bundle entry always wins.
+ renderIn('zh', dashboard({
+ id: 'revenue',
+ type: 'metric',
+ title: { en: 'Total Revenue', 'zh-CN': '收入合计' },
+ options: { value: 1930000 },
+ }));
+
+ expect(screen.getByText('总收入')).toBeTruthy();
+ expect(screen.queryByText('收入合计')).toBeNull();
+ });
+
+ it('(f) leaves a plain-string title on a metric exactly as authored when nothing translates it', () => {
+ // Non-vacuity + the acceptance boundary: an app with no bundle entry for
+ // this widget keeps the exact label it renders today.
+ renderIn('zh', dashboard({
+ id: 'untranslated',
+ type: 'metric',
+ title: 'Win Rate',
+ options: { value: '42%' },
+ }));
+
+ expect(screen.getByText('Win Rate')).toBeTruthy();
+ });
+
+ it('(f2) leaves the NON-metric title path unchanged — it already read the bundle', () => {
+ renderIn('zh', dashboard({
+ id: 'pipeline',
+ type: 'bar',
+ title: 'Pipeline',
+ options: { data: [{ name: 'a', value: 1 }] },
+ }));
+
+ expect(screen.getByText('销售漏斗')).toBeTruthy();
+ });
+});
diff --git a/packages/plugin-dashboard/src/__tests__/MetricWidget.i18nLabel.test.tsx b/packages/plugin-dashboard/src/__tests__/MetricWidget.i18nLabel.test.tsx
new file mode 100644
index 000000000..bb3066d49
--- /dev/null
+++ b/packages/plugin-dashboard/src/__tests__/MetricWidget.i18nLabel.test.tsx
@@ -0,0 +1,84 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#4032 — the OTHER two of the three private `resolveLabel` copies.
+ *
+ * `MetricWidget` and `MetricCard` are public SDUI blocks: a schema may address
+ * them directly (`type: 'metric'` / `'metric-card'`) without going through
+ * `DashboardRenderer`, so each has to resolve its own labels. Both carried the
+ * same private resolver ending `return label.defaultValue || label.key`, and
+ * both therefore rendered NOTHING for the inline per-locale map that
+ * `@objectstack/spec` actually admits — the map has neither key.
+ *
+ * DIRECTIONS: every map case below is RED before the change (empty heading),
+ * every plain-string case is GREEN on both sides.
+ *
+ * The `bare` variant is pinned alongside `card` because it is a SECOND read of
+ * the same prop in the same file — the kind of second site a per-call fix
+ * misses.
+ */
+
+import * as React from 'react';
+import { describe, it, expect, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import { I18nProvider } from '@object-ui/i18n';
+import { MetricWidget } from '../MetricWidget';
+import { MetricCard } from '../MetricCard';
+
+afterEach(cleanup);
+
+function renderIn(language: string, ui: React.ReactElement) {
+ return render(
+
+ {ui}
+ ,
+ );
+}
+
+describe('MetricWidget / MetricCard — inline per-locale labels (#4032)', () => {
+ it('MetricWidget resolves a map `label` to the active locale', () => {
+ renderIn('zh', );
+ expect(screen.getByText('收入')).toBeTruthy();
+ });
+
+ it('MetricWidget resolves a map `label` in the `bare` variant too', () => {
+ renderIn('zh', );
+ expect(screen.getByText('收入')).toBeTruthy();
+ });
+
+ it('MetricWidget resolves a map `description` (the sub-caption slot)', () => {
+ renderIn('zh', );
+ expect(screen.getByText('本月')).toBeTruthy();
+ });
+
+ it('MetricWidget keeps the trend-phrase translation working on a plain string', () => {
+ // #4333's neighbour behaviour: a recognised English trend phrase still maps
+ // to its canonical key and translates. The map support must not disturb it.
+ renderIn('en', );
+ expect(screen.getByText('vs last quarter')).toBeTruthy();
+ });
+
+ it('MetricCard resolves map `title` and `description`', () => {
+ renderIn('zh', (
+
+ ));
+ expect(screen.getByText('收入')).toBeTruthy();
+ expect(screen.getByText('本月')).toBeTruthy();
+ });
+
+ it('leaves plain-string labels exactly as authored', () => {
+ // Non-vacuity for all of the above.
+ renderIn('zh', );
+ expect(screen.getByText('Revenue')).toBeTruthy();
+ });
+});
diff --git a/packages/types/src/__tests__/dashboard-global-filters-spec-binding.test.ts b/packages/types/src/__tests__/dashboard-global-filters-spec-binding.test.ts
new file mode 100644
index 000000000..63b02d238
--- /dev/null
+++ b/packages/types/src/__tests__/dashboard-global-filters-spec-binding.test.ts
@@ -0,0 +1,105 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#4032 — `DashboardComponentSchema.globalFilters` is BOUND to
+ * `@objectstack/spec`'s `GlobalFilter`, not restated.
+ *
+ * ## Why this file exists at all
+ *
+ * This is the one part of #4032 that **no runtime test can falsify**. The
+ * restatement was a hand-written object literal carrying the same nine keys, so
+ * it type-checked, rendered, and shipped — it was simply NARROWER than the
+ * contract in one place (`label`, and `options[].label`, declared `string` after
+ * the spec widened both to `I18nLabel`). A type that is too narrow does not
+ * fail; it makes the read sites that would have failed invisible to `tsc`
+ * instead. That invisibility is the whole reason the filter bar shipped
+ * `[object Object]: All` and `normalizeFilterOptions` shipped a coercion that
+ * discarded map labels in every locale, English included.
+ *
+ * So the pins below are compile-time by necessity: reverting `globalFilters` to
+ * the old inline literal turns THIS FILE red (`tsc -p tsconfig.test.json`, run
+ * by `type-check`) and leaves every runtime suite green — which is precisely the
+ * failure signature the drift had, reproduced deliberately.
+ *
+ * The sibling half of the same widening WAS compile-visible and got repaired in
+ * #4208, for one structural reason: `DashboardWidgetSchema` takes its keys from
+ * the spec through `extends`. These pins hold `globalFilters` to that same
+ * route.
+ */
+
+import { describe, it, expect } from 'vitest';
+import type { DashboardComponentSchema } from '../complex';
+
+type GlobalFilterOf = NonNullable[number];
+
+type Assert = T;
+type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false;
+/** True when `V` is assignable to the type of `K` on a global filter. */
+type Accepts< K extends keyof GlobalFilterOf, V > = V extends GlobalFilterOf[K] ? true : false;
+
+describe('DashboardComponentSchema.globalFilters — bound to the spec, not restated (#4032)', () => {
+ it('accepts an inline per-locale map on the filter label', () => {
+ // The exact shape the restatement made unrepresentable. `Record< string,
+ // string >` is `I18nLabel`'s inline-map half.
+ type _MapLabel = Assert< Accepts< 'label', { en: string; 'zh-CN': string } > >;
+ // …without having lost the plain-string half.
+ type _StringLabel = Assert< Accepts< 'label', string > >;
+
+ const filter: GlobalFilterOf = {
+ field: 'owner',
+ name: 'owner',
+ label: { en: 'Owner', 'zh-CN': '负责人' },
+ };
+ expect(filter.label).toEqual({ en: 'Owner', 'zh-CN': '负责人' });
+ });
+
+ it('accepts an inline per-locale map on a static option label', () => {
+ // The second, worse site: this one was not merely unrendered, it was
+ // DISCARDED by `normalizeFilterOptions` in every locale.
+ const filter: GlobalFilterOf = {
+ field: 'sales_channel',
+ name: 'channel',
+ type: 'select',
+ options: [{ value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } }],
+ };
+ expect(filter.options).toHaveLength(1);
+ });
+
+ it('no longer DECLARES the bare-string option shorthand, which the spec rejects', () => {
+ // Measured, not assumed — `DashboardSchema.safeParse` on
+ // `options: ['EMEA', 'APAC']` returns two issues, both
+ // "Invalid input: expected object, received string". So the restatement was
+ // not only too narrow on `label`, it was also too WIDE here: it declared a
+ // shorthand `@objectstack/spec` refuses at publish, i.e. a second de-facto
+ // contract of exactly the kind AGENTS.md #0.1 forbids. A dashboard authored
+ // that way type-checked and rendered in objectui and would have been
+ // rejected the moment it reached the platform.
+ //
+ // Binding fixes both directions at once, which is the point of binding.
+ type _ShorthandGone = Assert< Equal< Accepts< 'options', string[] >, false > >;
+
+ // NOT a runtime removal. `normalizeFilterOptions` in `@object-ui/core`
+ // still lifts a bare string for STORED documents — narrowing the authoring
+ // type and dropping tolerance for already-persisted metadata are different
+ // changes, and only the first is in this card's scope. The runtime half is
+ // filed separately.
+ expect(true).toBe(true);
+ });
+
+ it('is the spec type by identity, not a structural look-alike', () => {
+ // The load-bearing assertion. Every check above would also pass against a
+ // hand-written literal that happened to be widened by hand today — which is
+ // exactly how the drift was born the first time. This one can only pass
+ // while the binding itself is in place, so a future "just widen it locally"
+ // edit fails here rather than silently restarting the same divergence.
+ type SpecGlobalFilter = import('@objectstack/spec/ui').GlobalFilter;
+ type _Bound = Assert< Equal< GlobalFilterOf, SpecGlobalFilter > >;
+ expect(true).toBe(true);
+ });
+});
diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts
index fa0af2d39..d8b073187 100644
--- a/packages/types/src/complex.ts
+++ b/packages/types/src/complex.ts
@@ -15,7 +15,10 @@
* @packageDocumentation
*/
-import type { DashboardWidget as SpecDashboardWidget } from '@objectstack/spec/ui';
+import type {
+ DashboardWidget as SpecDashboardWidget,
+ GlobalFilter as SpecGlobalFilter,
+} from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base';
/**
@@ -753,35 +756,26 @@ export interface DashboardComponentSchema extends BaseSchema {
/**
* Global filter configurations.
* Applied across all dashboard widgets.
- * Aligned with @objectstack/spec GlobalFilterSchema.
- */
- globalFilters?: Array<{
- /**
- * Stable filter name used as the dashboard-variable key and as the key
- * widgets reference in `filterBindings`. Defaults to `field`.
- * Aligned with @objectstack/spec GlobalFilterSchema.name
- * (framework#2501).
- */
- name?: string;
- field: string;
- label?: string;
- type?: 'text' | 'select' | 'date' | 'number' | 'lookup';
- /**
- * Static options. The @objectstack/spec form is `{ value, label }`
- * objects; the bare-string shorthand is also accepted (normalized by
- * the runtime).
- */
- options?: Array;
- optionsFrom?: {
- object: string;
- valueField: string;
- labelField?: string;
- filter?: any;
- };
- defaultValue?: any;
- scope?: string;
- targetWidgets?: string[];
- }>;
+ *
+ * BOUND to `@objectstack/spec`'s `GlobalFilter` rather than restated
+ * (objectui#4032, merged scope from the #4163 part-1 audit). It used to be a
+ * hand-written inline object literal that happened to carry the same nine
+ * keys, and the copy drifted in the one direction that costs the most:
+ * `label` was declared `string` here long after the spec widened it (and its
+ * `options[].label`) to `I18nLabel`. Because the restatement was NARROWER
+ * than the contract, an authored per-locale map was a legal document that
+ * objectui's own types said could not exist — so every read site that
+ * stringified one was invisible to `tsc`, which is precisely how the filter
+ * bar shipped `[object Object]: All` and how `normalizeFilterOptions` shipped
+ * a coercion that discarded map labels in every locale.
+ *
+ * `DashboardWidgetSchema` above already takes this route (`extends
+ * Omit, …>`), which is why the widget half of
+ * the same widening WAS compile-visible and got repaired in #4208. Binding is
+ * preferred over restating for exactly that asymmetry: the key set and the
+ * value vocabularies now track the protocol instead of a snapshot of it.
+ */
+ globalFilters?: SpecGlobalFilter[];
/**
* Date range filter configuration.
* Aligned with @objectstack/spec DashboardSchema.dateRange.