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/30134-client-next-week-year-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@object-ui/core': patch
---

fix(core): the client date-macro resolver answers `{next_week_end}` and `{next_year_end}`

Clause-②: no
Metadata-protocol: none

`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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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.
*/

import { describe, it, expect } from 'vitest';
import { DATE_MACRO_TOKENS } from '@objectstack/spec/data';
import { resolveDateMacros } from '../date-macros';

/**
* The client resolver answers every fixed token the spec declares
* (objectstack#30134). The list is read from the spec, never copied, so a
* token added there without a resolver entry here fails this file.
*/
describe('resolveDateMacros covers the spec vocabulary (objectstack#30134)', () => {
// Monday 2026-05-25, local noon.
const now = new Date(2026, 4, 25, 12, 0, 0);

it.each(DATE_MACRO_TOKENS.map((t) => [t]))('{%s} resolves to a day or an instant', (token) => {
const out = resolveDateMacros(`{${token}}`, now);
expect(out).not.toContain('{');
expect(out).toMatch(/^\d{4}-\d{2}-\d{2}(T|$)/);
});

it('{next_week_end} is the Sunday of next week (Monday-start weeks)', () => {
// From a Monday.
expect(resolveDateMacros('{next_week_end}', now)).toBe('2026-06-07');
// From a Sunday: still the week after the one that Sunday closes.
expect(resolveDateMacros('{next_week_end}', new Date(2026, 4, 31, 12))).toBe('2026-06-07');
// Across a year boundary: Wednesday 2026-12-30 → next week is Jan 4–10, 2027.
const dec30 = new Date(2026, 11, 30, 12);
expect(resolveDateMacros('{next_week_start}', dec30)).toBe('2027-01-04');
expect(resolveDateMacros('{next_week_end}', dec30)).toBe('2027-01-10');
});

it('{next_year_end} is Dec 31 of next year', () => {
expect(resolveDateMacros('{next_year_end}', now)).toBe('2027-12-31');
// On Dec 31 itself the answer is a full year out, not today.
expect(resolveDateMacros('{next_year_end}', new Date(2026, 11, 31, 12))).toBe('2027-12-31');
// On Jan 1 the year has rolled: next year is 2028.
expect(resolveDateMacros('{next_year_end}', new Date(2027, 0, 1, 12))).toBe('2028-12-31');
});
});
14 changes: 9 additions & 5 deletions ui/packages/core/src/utils/date-macros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import { getDisplayTimeZone } from './date-display.js';
* - last_month_start / last_month_end
* - last_quarter_start / last_quarter_end
* - last_year_start / last_year_end
* - next_week_start / next_month_start / next_quarter_start / next_year_start
* - next_month_end / next_quarter_end
* - next_week_start / next_week_end
* - next_month_start / next_month_end
* - next_quarter_start / next_quarter_end
* - next_year_start / next_year_end
*
* Parameterised tokens (match `N` as any positive integer):
* - {N_minutes_ago} / {N_minutes_from_now} (returns full ISO timestamp)
Expand All @@ -39,9 +41,9 @@ import { getDisplayTimeZone } from './date-display.js';
* - {N_years_ago} / {N_years_from_now}
*
* The canonical grammar is also published as part of the platform contract at
* `@objectstack/spec` → `DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`. Keep this
* file in sync with the spec; the duplication is temporary until the next
* coordinated `@objectstack/spec` release.
* `@objectstack/spec` → `DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`. Every
* fixed token there must resolve here; `date-macros.specTokens-30134.test.ts`
* reads the spec list and fails on any token this resolver leaves unresolved.
*
* Walks any plain-object / array structure recursively. Non-string values
* (and unknown tokens) are passed through untouched.
Expand Down Expand Up @@ -128,11 +130,13 @@ export function resolveDateMacros<T = any>(filter: T, now: Date = new Date()): T
last_year_end: () => isoDate(new Date(today.getFullYear() - 1, 11, 31)),

next_week_start: () => isoDate(addDays(startOfWeek(today), 7)),
next_week_end: () => isoDate(addDays(startOfWeek(today), 13)),
next_month_start: () => isoDate(addMonths(startOfMonth(today), 1)),
next_month_end: () => isoDate(endOfMonth(addMonths(startOfMonth(today), 1))),
next_quarter_start: () => isoDate(addMonths(startOfQuarter(today), 3)),
next_quarter_end: () => isoDate(endOfQuarter(addMonths(startOfQuarter(today), 3))),
next_year_start: () => isoDate(new Date(today.getFullYear() + 1, 0, 1)),
next_year_end: () => isoDate(new Date(today.getFullYear() + 1, 11, 31)),
};

// Aliases: bare {week_start} etc. mean the current period.
Expand Down
Loading