mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
Closes https://github.com/webstudio-is/webstudio/issues/3510 ## Summary This PR adds a **Current Date** system resource, extends the `system` object with a `pathname` property, and enhances the **Time component** with custom date formatting capabilities to provide more flexibility and prevent hydration errors. ### Key Features 1. **Current Date System Resource** - Provides normalized date/time information that prevents React hydration errors 2. **System Pathname** - Adds `pathname` property directly to the `system` object for easy access in expressions 3. **Time Component Custom Formatting** - Adds a `format` prop with template tokens for flexible date/time display ## Motivation These changes solve important use cases: - **Current Date**: Prevents React hydration errors when displaying dynamic dates (e.g., copyright years in footers). Previously, using `new Date()` in expressions would cause server/client mismatch. - **System Pathname**: Provides easy access to the current page path in expressions without needing to parse the URL manually. - **Custom Time Formatting**: Allows users to format dates with simple templates (e.g., `YYYY-MM-DD`) without being limited to `Intl.DateTimeFormat` options, and provides a simple way to exclude specific date or time components to avoid hydration issues. ## Changes ### Current Date Resource - Created [`apps/builder/app/shared/$resources/current-date.server.ts`](apps/builder/app/shared/$resources/current-date.server.ts) - Provides date fields: `year`, `month`, `day`, `iso`, `timestamp` - All values normalized to midnight UTC (00:00:00.000Z) to prevent hydration errors - Registered in [`rest.resources-loader.ts`](apps/builder/app/routes/rest.resources-loader.ts) - Added to SystemResourceForm dropdown in UI ### System Pathname - Added `pathname` property to [`System`](packages/sdk/src/schema/pages.ts) type - Updated [`system.ts`](apps/builder/app/shared/system.ts) to compute and include pathname - Normalized empty pathname to `'/'` for consistency - Available in expressions as `system.pathname` ### Time Component Enhancements - Added [`formatDate()`](packages/sdk-components-react/src/time.tsx:353) function supporting template tokens: - `YYYY`, `YY` - year (4-digit, 2-digit) - `MM`, `M` - month (zero-padded, no padding) - `DD`, `D` - day (zero-padded, no padding) - `HH`, `H` - hours (zero-padded, no padding) - `mm`, `m` - minutes (zero-padded, no padding) - `ss`, `s` - seconds (zero-padded, no padding) - Added `format` prop to [`Time`](packages/sdk-components-react/src/time.tsx:386) component that overrides `dateStyle`/`timeStyle` when provided - Updated component logic to check for `format` prop first before falling back to `Intl.DateTimeFormat` - Added comprehensive tests for date formatting in [`time.test.ts`](packages/sdk-components-react/src/time.test.ts) - Updated component metadata in [`time.ws.ts`](packages/sdk-components-react/src/time.ws.ts) to include `format` in initialProps - Example: `format="YYYY-MM-DD HH:mm:ss"` displays as "2025-11-03 18:47:25" ### Template & Fixture Updates - Updated all CLI templates (defaults, react-router, SSG) to include `pathname` in system objects - Updated all fixture files (47 files total) to include `pathname` in system objects - All 694 tests passing - All typecheck passing ## Usage ### Current Date Resource Users can now add the Current Date resource from the Data Variables panel: 1. Click + next to Data Variables 2. Select "System Resource" 3. Choose "Current Date" from the dropdown 4. Access in expressions: - `date.year` - for copyright year - `date.month` - current month (1-12) - `date.day` - current day (1-31) - `date.iso` - ISO 8601 string - `date.timestamp` - Unix timestamp ### System Pathname Access the current page path directly: - `system.pathname` - current page path (e.g., `/about`, `/blog/post-1`) ### Time Component Custom Format Use the `format` prop for custom date/time display: ```jsx <Time datetime="2025-11-03T18:47:25Z" format="YYYY-MM-DD" /> // Displays: 2025-11-03 <Time datetime="2025-11-03T18:47:25Z" format="YYYY-MM-DD HH:mm:ss" /> // Displays: 2025-11-03 18:47:25 <Time datetime="2025-11-03T18:47:25Z" format="M/D/YYYY" /> // Displays: 11/3/2025 ``` Technical Notes - Current Date values are normalized to midnight UTC to ensure consistency throughout the day and prevent hydration mismatches - The pathname property is computed from the request URL and normalized to '/' when empty - Custom format templates in the Time component are processed before falling back to Intl.DateTimeFormat - All changes are backward compatible
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import { __testing__ } from "./time";
|
|
|
|
const { parseDate, formatDate } = __testing__;
|
|
|
|
test("13-digit Unix timestamp", () => {
|
|
expect(parseDate("1724938577059")).toEqual(
|
|
new Date("2024-08-29T13:36:17.059Z")
|
|
);
|
|
});
|
|
|
|
test("10-digit Unix timestamp ", () => {
|
|
expect(parseDate("1724938577")).toEqual(new Date("2024-08-29T13:36:17.000Z"));
|
|
});
|
|
|
|
test("maximum 32-bit signed integer timestamp", () => {
|
|
expect(parseDate("2147483647")).toEqual(new Date("2038-01-19T03:14:07.000Z"));
|
|
});
|
|
|
|
test("far future date", () => {
|
|
expect(parseDate("9999999999999")).toEqual(
|
|
new Date("2286-11-20T17:46:39.999Z")
|
|
);
|
|
});
|
|
|
|
test("parse ISO date string", () => {
|
|
expect(parseDate("2024-08-29T13:36:17.000Z")).toEqual(
|
|
new Date("2024-08-29T13:36:17.000Z")
|
|
);
|
|
});
|
|
|
|
test.skip("parse short date", () => {
|
|
expect(parseDate("2024.10")).toEqual(new Date("2024-10-01T00:00:00.000Z"));
|
|
expect(parseDate("2024/10")).toEqual(new Date("2024-10-01T00:00:00.000Z"));
|
|
expect(parseDate("2024-10")).toEqual(new Date("2024-10-01T00:00:00.000Z"));
|
|
expect(parseDate("2024")).toEqual(new Date("2024-01-01T00:00:00.000Z"));
|
|
});
|
|
|
|
test("empty string", () => {
|
|
expect(parseDate("")).toEqual(undefined);
|
|
});
|
|
|
|
test("invalid date string", () => {
|
|
expect(parseDate("whatever that is")).toEqual(undefined);
|
|
});
|
|
|
|
test("formatDate with full template", () => {
|
|
const date = new Date("2025-10-31T14:05:09");
|
|
expect(formatDate(date, "YYYY-MM-DD HH:mm:ss")).toBe("2025-10-31 14:05:09");
|
|
});
|
|
|
|
test("formatDate with short date", () => {
|
|
const date = new Date("2025-10-31T14:05:09");
|
|
expect(formatDate(date, "DD/MM/YY")).toBe("31/10/25");
|
|
});
|
|
|
|
test("formatDate with time only", () => {
|
|
const date = new Date("2025-10-31T14:05:09");
|
|
expect(formatDate(date, "H:m:s")).toBe("14:5:9");
|
|
});
|
|
|
|
test("formatDate with padded values", () => {
|
|
const date = new Date("2025-01-05T08:03:07");
|
|
expect(formatDate(date, "YYYY-MM-DD HH:mm:ss")).toBe("2025-01-05 08:03:07");
|
|
});
|
|
|
|
test("formatDate with unpadded values", () => {
|
|
const date = new Date("2025-01-05T08:03:07");
|
|
expect(formatDate(date, "YYYY-M-D H:m:s")).toBe("2025-1-5 8:3:7");
|
|
});
|
|
|
|
test("formatDate with year only", () => {
|
|
const date = new Date("2025-10-31T14:05:09");
|
|
expect(formatDate(date, "YYYY")).toBe("2025");
|
|
});
|
|
|
|
test("formatDate with custom separator", () => {
|
|
const date = new Date("2025-10-31T14:05:09");
|
|
expect(formatDate(date, "DD.MM.YYYY")).toBe("31.10.2025");
|
|
});
|