Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/brave-foxes-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Replaced a few instanced of moment.js usage with native Intl API
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: “instanced” → “instances”.

Suggested change
Replaced a few instanced of moment.js usage with native Intl API
Replaced a few instances of moment.js usage with native Intl API

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { customer } from "@dashboard/customers/fixtures";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";

import CustomerDetails from "./CustomerDetails";

const meta: Meta<typeof CustomerDetails> = {
title: "Customers/CustomerDetails",
component: CustomerDetails,
};

export default meta;
type Story = StoryObj<typeof CustomerDetails>;

const defaultProps = {
customer: customer,
data: {
isActive: true,
note: "Very important customer",
},
disabled: false,
errors: [],
onChange: fn(),
};

export const Default: Story = {
args: defaultProps,
};

export const Loading: Story = {
args: {
...defaultProps,
customer: null,
},
};

export const Inactive: Story = {
args: {
...defaultProps,
data: {
isActive: false,
note: "",
},
},
};

export const Disabled: Story = {
args: {
...defaultProps,
disabled: true,
},
};

export const WithError: Story = {
args: {
...defaultProps,
errors: [
{
__typename: "AccountError" as const,
code: "INVALID" as any,
field: "note",
addressType: null,
message: "This field is required",
},
],
},
};

export const EmptyNote: Story = {
args: {
...defaultProps,
data: {
isActive: true,
note: "",
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { formatMonthYear } from "./CustomerDetails";

describe("formatMonthYear", () => {
it("formats date as abbreviated month and year", () => {
// Arrange & Act
const result = formatMonthYear("en-US")("2024-01-15T14:30:00Z");

// Assert
expect(result).toBe("Jan 2024");
});

it("formats mid-year date correctly", () => {
// Arrange & Act
const result = formatMonthYear("en-US")("2024-06-01T00:00:00Z");

// Assert
expect(result).toBe("Jun 2024");
});

it("formats end-of-year date correctly", () => {
// Arrange & Act
const result = formatMonthYear("en-US")("2023-12-31T23:59:59Z");

// Assert
expect(result).toBe("Dec 2023");
});
});
13 changes: 11 additions & 2 deletions src/customers/components/CustomerDetails/CustomerDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// @ts-strict-ignore
import { DashboardCard } from "@dashboard/components/Card";
import { type AccountErrorFragment, type CustomerDetailsQuery } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import { maybe } from "@dashboard/misc";
import { getFormErrors } from "@dashboard/utils/errors";
import getAccountErrorMessage from "@dashboard/utils/errors/account";
import { TextField } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { Checkbox, Skeleton, Text } from "@saleor/macaw-ui-next";
import moment from "moment-timezone";
import type * as React from "react";
import { FormattedMessage, useIntl } from "react-intl";

Expand All @@ -29,6 +29,14 @@ const useStyles = makeStyles(
{ name: "CustomerDetails" },
);

export const formatMonthYear =
(locale: string) =>
(date: string): string =>
new Intl.DateTimeFormat(locale, {
month: "short",
year: "numeric",
}).format(new Date(date));
Comment on lines +34 to +38
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Intl.DateTimeFormat(...).format(new Date(date)) will throw a RangeError for invalid dates (e.g. empty/undefined dateJoined), which would crash this render path. Consider validating the parsed date (isNaN(parsed.getTime())) and returning a safe placeholder (or "Invalid date" for parity with moment) instead of throwing; also prefer typing locale as Locale (from @dashboard/components/Locale) rather than string to avoid invalid tags.

Suggested change
(date: string): string =>
new Intl.DateTimeFormat(locale, {
month: "short",
year: "numeric",
}).format(new Date(date));
(date: string): string => {
const parsed = new Date(date);
if (isNaN(parsed.getTime())) {
// Fallback for invalid dates, matching Moment's "Invalid date" behavior
return "Invalid date";
}
return new Intl.DateTimeFormat(locale, {
month: "short",
year: "numeric",
}).format(parsed);
};

Copilot uses AI. Check for mistakes.

interface CustomerDetailsProps {
customer: CustomerDetailsQuery["user"];
data: {
Expand All @@ -45,6 +53,7 @@ const CustomerDetails = (props: CustomerDetailsProps) => {

const classes = useStyles(props);
const intl = useIntl();
const { locale } = useLocale();

const formErrors = getFormErrors(["note"], errors);

Expand All @@ -66,7 +75,7 @@ const CustomerDetails = (props: CustomerDetailsProps) => {
defaultMessage="Active member since {date}"
description="section subheader"
values={{
date: moment(customer.dateJoined).format("MMM YYYY"),
date: formatMonthYear(locale)(customer.dateJoined),
}}
/>
</Text>
Expand Down
24 changes: 21 additions & 3 deletions src/discounts/components/VoucherListDatagrid/datagrid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { type VoucherFragment } from "@dashboard/graphql";

import { createGetCellContent } from "./datagrid";
import { createGetCellContent, formatDateTime } from "./datagrid";

const columns: AvailableColumn[] = [
{ id: "code", title: "Code", width: 350 },
Expand Down Expand Up @@ -50,6 +50,24 @@ const createVoucher = (overrides: Partial<VoucherFragment> = {}): VoucherFragmen
...overrides,
}) as VoucherFragment;

describe("formatDateTime", () => {
it("formats date with EN locale", () => {
// Arrange & Act
const result = formatDateTime("2024-06-15T09:30:00Z", Locale.EN);

// Assert
expect(result).toBe("Jun 15, 2024, 9:30 AM");
});

it("formats date with PL locale", () => {
// Arrange & Act
const result = formatDateTime("2024-06-15T09:30:00Z", Locale.PL);

// Assert
expect(result).toBe("15 cze 2024, 09:30");
});
});

describe("VoucherListDatagrid createGetCellContent", () => {
it("returns formatted start date for start-date column", () => {
// Arrange
Expand All @@ -65,7 +83,7 @@ describe("VoucherListDatagrid createGetCellContent", () => {
const cell = getCellContent([2, 0]);

// Assert
expect(cell).toHaveProperty("data", "Jan 15, 2024 12:00 AM");
expect(cell).toHaveProperty("data", "Jan 15, 2024, 12:00 AM");
});

it("returns formatted end date for end-date column", () => {
Expand All @@ -82,7 +100,7 @@ describe("VoucherListDatagrid createGetCellContent", () => {
const cell = getCellContent([3, 0]);

// Assert
expect(cell).toHaveProperty("data", "Dec 31, 2024 11:59 PM");
expect(cell).toHaveProperty("data", "Dec 31, 2024, 11:59 PM");
});

it("returns PLACEHOLDER for null start date", () => {
Expand Down
27 changes: 20 additions & 7 deletions src/discounts/components/VoucherListDatagrid/datagrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ import { type VoucherFragment } from "@dashboard/graphql";
import { type Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { type GridCell, type Item } from "@glideapps/glide-data-grid";
import moment from "moment";
import { type IntlShape } from "react-intl";

import { columnsMessages } from "./messages";

export function formatDateTime(date: string, locale: Locale): string {
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(
new Date(date),
);
}

export const vouchersListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<VoucherListUrlSortField>,
Expand Down Expand Up @@ -92,13 +97,21 @@ export const createGetCellContent =
})
: readonlyTextCell(PLACEHOLDER);
case "start-date":
return readonlyTextCell(
rowData.startDate ? moment(rowData.startDate).locale(locale).format("lll") : PLACEHOLDER,
);
try {
return readonlyTextCell(
rowData.startDate ? formatDateTime(rowData.startDate, locale) : PLACEHOLDER,
);
} catch (e) {
return readonlyTextCell(PLACEHOLDER);
}
case "end-date":
return readonlyTextCell(
rowData.endDate ? moment(rowData.endDate).locale(locale).format("lll") : PLACEHOLDER,
);
try {
return readonlyTextCell(
rowData.endDate ? formatDateTime(rowData.endDate, locale) : PLACEHOLDER,
);
} catch (e) {
return readonlyTextCell(PLACEHOLDER);
}

case "value":
return getVoucherValueCell(rowData, channel);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDateLocalize.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("useDateLocalize", () => {
const formatted = result.current("2024-01-15T14:30:00Z", "lll");

// Assert
expect(formatted).toBe("Jan 15, 2024 2:30 PM");
expect(formatted).toBe("Jan 15, 2024, 2:30 PM");
});

it("formats date with PL locale", () => {
Expand Down
32 changes: 27 additions & 5 deletions src/hooks/useDateLocalize.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import { LocaleContext } from "@dashboard/components/Locale";
import moment from "moment-timezone";
import { useContext } from "react";

export type LocalizeDate = (date: string, format?: string) => string;

/**
* Backwards compat with old moment.js format.
*/
const FORMAT_OPTIONS: Record<string, Intl.DateTimeFormatOptions> = {
ll: { dateStyle: "medium" },
lll: { dateStyle: "medium", timeStyle: "short" },
llll: {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
},
};

function useDateLocalize(): LocalizeDate {
const { locale } = useContext(LocaleContext);

return (date: string, format?: string) =>
moment(date)
.locale(locale)
.format(format || "ll");
return (date: string, format?: "ll" | "lll" | "llll" | string) => {
const parsed = new Date(date);

if (isNaN(parsed.getTime())) {
return "Invalid date";
}

const options = FORMAT_OPTIONS[format || "ll"] ?? FORMAT_OPTIONS.ll;

return new Intl.DateTimeFormat(locale, options).format(parsed);
};
}

export default useDateLocalize;
30 changes: 28 additions & 2 deletions src/orders/components/OrderDraftListDatagrid/datagrid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { type OrderDraft } from "@dashboard/orders/types";

import { createGetCellContent, getCustomerName } from "./datagrid";
import { createGetCellContent, formatDateTime, getCustomerName } from "./datagrid";

const columns: AvailableColumn[] = [
{ id: "number", title: "Number", width: 100 },
Expand Down Expand Up @@ -48,7 +48,7 @@ describe("createGetCellContent", () => {
const cell = getCellContent([1, 0]);

// Assert
expect(cell).toHaveProperty("data", "Jan 15, 2024 2:30 PM");
expect(cell).toHaveProperty("data", "Jan 15, 2024, 2:30 PM");
});

it("returns empty text cell when row data is missing", () => {
Expand Down Expand Up @@ -100,6 +100,32 @@ describe("createGetCellContent", () => {
});
});

describe("formatDateTime", () => {
it("formats date with EN locale", () => {
// Arrange & Act
const result = formatDateTime("2024-01-15T14:30:00Z", Locale.EN);

// Assert
expect(result).toBe("Jan 15, 2024, 2:30 PM");
});

it("formats date with PL locale", () => {
// Arrange & Act
const result = formatDateTime("2024-01-15T14:30:00Z", Locale.PL);

// Assert
expect(result).toBe("15 sty 2024, 14:30");
});

it("formats midnight correctly", () => {
// Arrange & Act
const result = formatDateTime("2024-01-15T00:00:00Z", Locale.EN);

// Assert
expect(result).toBe("Jan 15, 2024, 12:00 AM");
});
});

describe("getCustomerName", () => {
it("should return billing address first name and last name when exists", () => {
// Arrange
Expand Down
13 changes: 11 additions & 2 deletions src/orders/components/OrderDraftListDatagrid/datagrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { type OrderDraft } from "@dashboard/orders/types";
import { type Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { type GridCell, type Item } from "@glideapps/glide-data-grid";
import moment from "moment";
import { type IntlShape } from "react-intl";

import { columnsMessages } from "./messages";

export function formatDateTime(date: string, locale: Locale): string {
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(
new Date(date),
);
}
Comment on lines +12 to +16
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatDateTime will throw on invalid input (new Date(date) -> Invalid Date), and then every cell render relies on try/catch. Prefer handling invalid dates inside formatDateTime (e.g., return "-" when isNaN(parsed.getTime())) so callers don’t need exception control flow on hot paths.

Copilot uses AI. Check for mistakes.

export const orderDraftListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort,
Expand Down Expand Up @@ -67,7 +72,11 @@ export const createGetCellContent =
case "number":
return readonlyTextCell(`#${rowData.number}`);
case "date":
return readonlyTextCell(moment(rowData.created).locale(locale).format("lll"));
try {
return readonlyTextCell(formatDateTime(rowData.created, locale));
} catch (e) {
return readonlyTextCell("-");
}
case "customer":
return readonlyTextCell(getCustomerName(rowData));
case "total":
Expand Down
Loading