Skip to content

Commit d3f23d2

Browse files
fix(core): resolve {next_week_end} and {next_year_end} in the client date-macro resolver (#30364)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgNTEGwukC81vXo95pnRQCnF)_ Fixes #30134 domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG Clause-②: no — the client resolver catches up with tokens the spec already declares Metadata-protocol: none ## 维护者速读 - 改了什么:控制台的日期占位符解析器补上 `{next_week_end}`(下周日)与 `{next_year_end}`(明年 12 月 31 日),并加一条测试逐个核对 spec 声明的全部固定占位符都能在客户端解析。 - 为什么改:spec 早已声明这两个占位符,服务端也能解析,但控制台原样显示 `{next_week_end}` 文本,比较区间与趋势标签随之出错。 - 风险与回滚:只新增两个映射,既有占位符不变;回滚即撤销本 PR,无数据迁移。 - 先看:`ui/packages/core/src/utils/date-macros.ts`(两行新映射)、`date-macros.specTokens-30134.test.ts`(防漂移钉)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/core/src/utils/__tests__/date-macros.specTokens-30134.test.ts`,预期 38 条全绿。 - 需要你做的:无,席位落地。 ## What changes - `resolveDateMacros` (`@object-ui/core`) resolves `{next_week_end}` as the Sunday of next week (Monday-start weeks, the same week `{next_week_start}` opens, per the spec's `DATE_MACRO_DESCRIPTIONS` entry "Sunday of next week") and `{next_year_end}` as Dec 31 of next year ("Dec 31 of next year"). - The resolver's header list now names both, and its "keep in sync" note points at the new pin instead of promising a future de-duplication. - New `date-macros.specTokens-30134.test.ts`: iterates the spec's exported `DATE_MACRO_TOKENS` (imported from `@objectstack/spec/data`, not copied) and fails on any token the client leaves unresolved; pins `next_week_end` from a Monday, a Sunday and across the 2026→2027 year boundary (Wed 2026-12-30 → `2027-01-04`..`2027-01-10`), and `next_year_end` on Dec 31 and Jan 1. - No new dependency: `@object-ui/core` already depends on `@objectstack/spec` at runtime. - Changeset `@object-ui/core: patch`. ## Tests <details><summary>Changed tests plus every test file importing the changed module or its importers (head 8dec3da, re-run after the main merge)</summary> - `pnpm --dir ui exec vitest run` over the 5 `date-macros` / `dashboard-filters` / `filter-tokens` test files: Test Files 5 passed, Tests 114 passed. - The 13 test files importing `compare-to` / `dashboard-filters` / `filter-tokens`: Test Files 13 passed, Tests 250 passed. - After merging `origin/main`: the two `date-macros` files, Tests 46 passed. - `pnpm exec turbo run type-check --filter=@object-ui/core --concurrency=2` (runs `tsconfig.test.json` too): 4 tasks successful. - Ablation (`scripts/ablation-replace.mjs --delete` of the `next_week_end` entry, source import, no dist on the path): the new file went red, 2 failed (`{next_week_end} resolves to a day or an instant`, `{next_week_end} is the Sunday of next week`); `git diff HEAD` empty after the restore leg. </details> ## Acceptance notes - The card's observation on `ui/packages/types/src/data-protocol.ts` (`FilterBuilderDateRangePreset`) is not changed here. The union is a deliberately separate vocabulary (renamed from `DateRangePreset` in objectui#4167 because it adds future windows the dashboard schema refuses), so deriving it from `DATE_RANGE_PRESETS` would drop `tomorrow`, `next_week`, `next_year`, `next_7_days` and `next_90_days` that its filter-builder surface uses. Its doc comment's counts ("thirteen", "eight") no longer match the spec table, which now holds sixteen. Reported to the seat; not filed. ## Verification <details><summary>dispatch-gates on 8dec3da</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 35 derived families, each run with its exit code recorded before any pipe, all exit 0 (including `node scripts/check-sdui-manifest.mjs`). `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CXcHcEtzn8WgUC46Xj)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7945a3b commit d3f23d2

3 files changed

Lines changed: 65 additions & 5 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/core': patch
3+
---
4+
5+
fix(core): the client date-macro resolver answers `{next_week_end}` and `{next_year_end}`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`resolveDateMacros` now resolves `{next_week_end}` (Sunday of next week, Monday-start weeks) and `{next_year_end}` (Dec 31 of next year), the two fixed tokens `@objectstack/spec`'s `DATE_MACRO_TOKENS` declared that the Console passed through as raw text. A test now reads `DATE_MACRO_TOKENS` from the spec and fails on any token the client leaves unresolved, so the two lists cannot drift again.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect } from 'vitest';
10+
import { DATE_MACRO_TOKENS } from '@objectstack/spec/data';
11+
import { resolveDateMacros } from '../date-macros';
12+
13+
/**
14+
* The client resolver answers every fixed token the spec declares
15+
* (objectstack#30134). The list is read from the spec, never copied, so a
16+
* token added there without a resolver entry here fails this file.
17+
*/
18+
describe('resolveDateMacros covers the spec vocabulary (objectstack#30134)', () => {
19+
// Monday 2026-05-25, local noon.
20+
const now = new Date(2026, 4, 25, 12, 0, 0);
21+
22+
it.each(DATE_MACRO_TOKENS.map((t) => [t]))('{%s} resolves to a day or an instant', (token) => {
23+
const out = resolveDateMacros(`{${token}}`, now);
24+
expect(out).not.toContain('{');
25+
expect(out).toMatch(/^\d{4}-\d{2}-\d{2}(T|$)/);
26+
});
27+
28+
it('{next_week_end} is the Sunday of next week (Monday-start weeks)', () => {
29+
// From a Monday.
30+
expect(resolveDateMacros('{next_week_end}', now)).toBe('2026-06-07');
31+
// From a Sunday: still the week after the one that Sunday closes.
32+
expect(resolveDateMacros('{next_week_end}', new Date(2026, 4, 31, 12))).toBe('2026-06-07');
33+
// Across a year boundary: Wednesday 2026-12-30 → next week is Jan 4–10, 2027.
34+
const dec30 = new Date(2026, 11, 30, 12);
35+
expect(resolveDateMacros('{next_week_start}', dec30)).toBe('2027-01-04');
36+
expect(resolveDateMacros('{next_week_end}', dec30)).toBe('2027-01-10');
37+
});
38+
39+
it('{next_year_end} is Dec 31 of next year', () => {
40+
expect(resolveDateMacros('{next_year_end}', now)).toBe('2027-12-31');
41+
// On Dec 31 itself the answer is a full year out, not today.
42+
expect(resolveDateMacros('{next_year_end}', new Date(2026, 11, 31, 12))).toBe('2027-12-31');
43+
// On Jan 1 the year has rolled: next year is 2028.
44+
expect(resolveDateMacros('{next_year_end}', new Date(2027, 0, 1, 12))).toBe('2028-12-31');
45+
});
46+
});

‎ui/packages/core/src/utils/date-macros.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import { getDisplayTimeZone } from './date-display.js';
2727
* - last_month_start / last_month_end
2828
* - last_quarter_start / last_quarter_end
2929
* - last_year_start / last_year_end
30-
* - next_week_start / next_month_start / next_quarter_start / next_year_start
31-
* - next_month_end / next_quarter_end
30+
* - next_week_start / next_week_end
31+
* - next_month_start / next_month_end
32+
* - next_quarter_start / next_quarter_end
33+
* - next_year_start / next_year_end
3234
*
3335
* Parameterised tokens (match `N` as any positive integer):
3436
* - {N_minutes_ago} / {N_minutes_from_now} (returns full ISO timestamp)
@@ -39,9 +41,9 @@ import { getDisplayTimeZone } from './date-display.js';
3941
* - {N_years_ago} / {N_years_from_now}
4042
*
4143
* The canonical grammar is also published as part of the platform contract at
42-
* `@objectstack/spec` → `DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`. Keep this
43-
* file in sync with the spec; the duplication is temporary until the next
44-
* coordinated `@objectstack/spec` release.
44+
* `@objectstack/spec` → `DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`. Every
45+
* fixed token there must resolve here; `date-macros.specTokens-30134.test.ts`
46+
* reads the spec list and fails on any token this resolver leaves unresolved.
4547
*
4648
* Walks any plain-object / array structure recursively. Non-string values
4749
* (and unknown tokens) are passed through untouched.
@@ -128,11 +130,13 @@ export function resolveDateMacros<T = any>(filter: T, now: Date = new Date()): T
128130
last_year_end: () => isoDate(new Date(today.getFullYear() - 1, 11, 31)),
129131

130132
next_week_start: () => isoDate(addDays(startOfWeek(today), 7)),
133+
next_week_end: () => isoDate(addDays(startOfWeek(today), 13)),
131134
next_month_start: () => isoDate(addMonths(startOfMonth(today), 1)),
132135
next_month_end: () => isoDate(endOfMonth(addMonths(startOfMonth(today), 1))),
133136
next_quarter_start: () => isoDate(addMonths(startOfQuarter(today), 3)),
134137
next_quarter_end: () => isoDate(endOfQuarter(addMonths(startOfQuarter(today), 3))),
135138
next_year_start: () => isoDate(new Date(today.getFullYear() + 1, 0, 1)),
139+
next_year_end: () => isoDate(new Date(today.getFullYear() + 1, 11, 31)),
136140
};
137141

138142
// Aliases: bare {week_start} etc. mean the current period.

0 commit comments

Comments
 (0)