You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(spec): make the dashboard date-range preset names one vocabulary and check a date filter's defaultValue against it (#4614)
A dashboard's built-in `dateRange` validated its preset name and a
`globalFilters` entry of `type: 'date'` did not, so the same typo was an
author-time error on one surface and a silent wrong answer on the other.
`GlobalFilterSchema.defaultValue` is `string | number | boolean`, which makes a
bare preset name the only spelling available for a date filter's default — and
nothing checked it. An unrecognised name cannot be lifted to a range, so it fell
through to "a bare string date means equality on that day" and reached the
backend as `created_at = 'last_7_dayz'`: a condition no row matches, answered
200 OK with a zero. Every tile read 0 while the filter bar showed "All time".
WHAT LANDED (A案 two steps)
1. Vocabulary migrated into spec as the single source of truth.
`DATE_RANGE_PRESETS` (13 names) + `DateRangePreset`, and
`DATE_RANGE_DEFAULT_RANGES` (presets + `custom`) + `DateRangeDefaultRange`,
in packages/spec/src/ui/dashboard.zod.ts. Shape copied from
`DATE_MACRO_TOKENS`: `as const` array + `(typeof X)[number]` alias.
`dateRange.defaultRange` now reads the second list, so its accepted set is
unchanged member-for-member (asserted by a test).
2. `GlobalFilterSchema.superRefine` — on `type: 'date'`, a declared
`defaultValue` must be a preset name, an ISO date, or a known date-macro
token. The macro arm calls `isDateMacroToken`/`DATE_MACRO_WRAPPED_RE` rather
than restating the grammar: one token vocabulary, no second dialect. The
rejection quotes the offending value and lists all three legal spellings
(strict gate + fixable text). Other filter types are untouched.
PREMISE DIVERGENCE FOUND AND RESOLVED (no裁决 needed)
The brief scoped the check to `type: date|dateRange`. Spec's
`GlobalFilterSchema.type` enum has no `dateRange` member — it is
['text','select','date','number','lookup']. objectui's `DashboardFilterDef.type`
does have `'dateRange'`, but `resolveDashboardFilterDefs` SYNTHESISES that def
from `schema.dateRange` under the reserved name "dateRange"; the `globalFilters`
loop only ever reads `f.type ?? 'text'`. So `dateRange` is an objectui-internal
def type, not an authorable `globalFilters[].type`, and no enum member was
added. The two halves of "date|dateRange" map to: `date` = the globalFilters
entry (this superRefine), `dateRange` = the built-in, already an enum and now
reading the shared constant.
`custom` is deliberately NOT a preset: objectui's PRESET_RANGES has 13 keys,
spec's old inline enum had those 13 + `custom`. `custom` names no window ("open
the picker"), so it stays legal as `defaultRange` and is rejected as a bare
filter default, which gives it no from/to to hand over.
存量 EVALUATION — no ADR-0087 conversion required
Scanned the three example apps, content/docs, and packages/ for a date filter
`defaultValue` carrying a misspelled preset name. ZERO hits.
Reverse-check proving the scan was live (a known-good name must be findable):
grepping `this_month` / `this_quarter` / `last_7_days` / `last_30_days` surfaced
`defaultRange: 'this_month'` and `'this_quarter'` in content/docs/ui/dashboards.mdx
plus its 14-row preset table — i.e. the scan does find preset names where they
exist.
The tree's ONLY date-filter default is
packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts:158
`defaultValue: 'last_7_days'` — a VALID preset, unaffected. Verified by parsing
the real shipped module through the new schema (not a fixture): parsed OK,
defaultValue preserved as "last_7_days". Also pinned by a named test.
`docs/notes/airtable-dashboard-analysis.mdx` has `defaultValue: 'this_quarter'`
on a `type: 'select'` filter — a different surface, untouched by this rule.
VERIFICATION — real readings
- spec full suite: 343 files / 8819 tests passed
- spec typecheck: tsc + scripts-typecheck + test-typecheck all clean
- check:generated: 10/10 green (2 were stale — skill-refs, api-surface — and
were regenerated with --fix, then re-checked green). api-surface delta is 4
pure ADDITIONS (2 const + 2 type), zero removals — no baseline debt (#4593).
- check:spec-parsed-alias: OK (1443 bare / 749 pinned / 694 paired). No pin
needed: the new types read `(typeof X)[number]`, not z.input/z.infer, so they
are outside ADR-0122's population.
- pnpm lint: clean
- three examples `validate`: all REAL_EXIT=0 with "✓ Validation passed"
(warnings present are pre-existing and unrelated — i18n section names,
liveness, permissions, flow status)
REVERSE VALIDATION (direction predicted BEFORE running)
Predicted: neutralising the superRefine turns exactly 5 tests red (misspelled
preset, `custom`, non-string, error-message, unknown-macro-token) and leaves the
other 45 green. Measured: 5 failed / 45 passed, precisely those 5. Probe
reverted; restored run 50/50 green, and the file greps clean of the probe.
FOR PM — objectui 联动单 material
Repo objectstack-ai/objectui @ 0cf8f0f, file
packages/core/src/utils/dashboard-filters.ts:
- line 73 `const PRESET_RANGES: Record<string, {from?,to?}>` — the 13 names
with their date-macro bounds. Landing point: key it off the spec
vocabulary so a spec-side addition becomes a compile error until
bounds are supplied:
`const PRESET_RANGES: Record<DateRangePreset, {from?: string; to?: string}> = {...}`
- line 90 `export const DATE_RANGE_PRESETS = Object.keys(PRESET_RANGES)`
becomes a re-export of spec's constant (same NAME, so no consumer
churn), which also fixes its type: `string[]` today, a literal
union after.
- import: `import { DATE_RANGE_PRESETS, type DateRangePreset } from '@objectstack/spec/ui';`
`@objectstack/spec/ui` is already objectui's most-used spec subpath
(134 imports) and packages/core already imports from it;
packages/core/package.json depends on `@objectstack/spec ^17.0.0-rc.5`,
so the range covers this minor once published.
- consumer: packages/plugin-dashboard/src/DashboardFilterBar.tsx:37,97 —
display order now comes from spec; no code change expected.
Nothing in objectui was modified by this commit.
changeset: @objectstack/spec minor (new authorable validation surface).
Docs: content/docs/ui/dashboards.mdx gains a "Date Filter Defaults" section and
a pointer from the preset table to the source-of-truth constant.
* chore(spec): regenerate api-surface/ui.json after merging origin/main (#4614)
The merge driver defers generator-owned artifacts rather than text-merging
them (AGENTS.md §11), so the merge commit carried this branch's pre-merge
ui.json — which predates #4593's export-type backfill on main. Regenerated
from the rebuilt dist so the file describes the MERGED source: main's
73-schema backfill (ActionType/PageComponentType/ReportType and friends
reclassified const → type, plus the newly-named types) is restored alongside
this branch's four DATE_RANGE_* additions.
check:generated 10/10 green after regeneration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8pDLTt6UHXUYY9YE7T1cA
---------
Co-authored-by: Claude <noreply@anthropic.com>
field: z.string().optional().describe('Default date field name for time-based filtering'),
793
-
defaultRange: z.enum(['today','yesterday','this_week','last_week','this_month','last_month','this_quarter','last_quarter','this_year','last_year','last_7_days','last_30_days','last_90_days','custom']).default('this_month').describe('Default date range preset'),
897
+
defaultRange: z.enum(DATE_RANGE_DEFAULT_RANGES).default('this_month').describe('Default date range preset'),
794
898
allowCustomRange: z.boolean().default(true).describe('Allow users to pick a custom date range'),
795
899
}).optional().describe('Global dashboard date range filter configuration'),
0 commit comments