Fix frontend locale tests after date-fns changed intl formatting

and also attempt to format dates in locale
This commit is contained in:
Jamie Curnow
2026-01-13 22:42:42 +10:00
parent 9c25410331
commit b01a22c393
8 changed files with 38 additions and 21 deletions

View File

@@ -3,6 +3,7 @@ import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend"; import type { AccessList } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useAccessLists } from "src/hooks"; import { useAccessLists } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale"; import { formatDateTime, intl, T } from "src/locale";
@@ -32,6 +33,7 @@ interface Props {
label?: string; label?: string;
} }
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) { export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -48,7 +50,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
{ {
users: item?.items?.length, users: item?.items?.length,
rules: item?.clients?.length, rules: item?.clients?.length,
date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A", date: item?.createdOn ? formatDateTime(item?.createdOn, locale) : "N/A",
}, },
), ),
icon: <IconLock size={14} className="text-lime" />, icon: <IconLock size={14} className="text-lime" />,

View File

@@ -2,6 +2,7 @@ import { IconShield } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend"; import type { Certificate } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useCertificates } from "src/hooks"; import { useCertificates } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale"; import { formatDateTime, intl, T } from "src/locale";
@@ -41,6 +42,7 @@ export function SSLCertificateField({
allowNew, allowNew,
forHttp = true, forHttp = true,
}: Props) { }: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useCertificates(); const { isLoading, isError, error, data } = useCertificates();
const { values, setFieldValue } = useFormikContext(); const { values, setFieldValue } = useFormikContext();
const v: any = values || {}; const v: any = values || {};
@@ -75,7 +77,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({ data?.map((cert: Certificate) => ({
value: cert.id, value: cert.id,
label: cert.niceName, label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`, subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn, locale) : "N/A" })}`,
icon: <IconShield size={14} className="text-pink" />, icon: <IconShield size={14} className="text-pink" />,
})) || []; })) || [];

View File

@@ -1,5 +1,6 @@
import cn from "classnames"; import cn from "classnames";
import { differenceInDays, isPast } from "date-fns"; import { differenceInDays, isPast } from "date-fns";
import { useLocaleState } from "src/context";
import { formatDateTime, parseDate } from "src/locale"; import { formatDateTime, parseDate } from "src/locale";
interface Props { interface Props {
@@ -8,6 +9,7 @@ interface Props {
highlistNearlyExpired?: boolean; highlistNearlyExpired?: boolean;
} }
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) { export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
const { locale } = useLocaleState();
const d = parseDate(value); const d = parseDate(value);
const dateIsPast = d ? isPast(d) : false; const dateIsPast = d ? isPast(d) : false;
const days = d ? differenceInDays(d, new Date()) : 0; const days = d ? differenceInDays(d, new Date()) : 0;
@@ -15,5 +17,5 @@ export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: P
"text-danger": highlightPast && dateIsPast, "text-danger": highlightPast && dateIsPast,
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0, "text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
}); });
return <span className={cl}>{formatDateTime(value)}</span>; return <span className={cl}>{formatDateTime(value, locale)}</span>;
} }

View File

@@ -1,5 +1,6 @@
import cn from "classnames"; import cn from "classnames";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
interface Props { interface Props {
@@ -37,7 +38,9 @@ const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
}; };
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) { export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
const { locale } = useLocaleState();
const elms: ReactNode[] = []; const elms: ReactNode[] = [];
if ((!domains || domains.length === 0) && !niceName) { if ((!domains || domains.length === 0) && !niceName) {
elms.push( elms.push(
<span key="nice-name" className="badge bg-danger-lt me-2"> <span key="nice-name" className="badge bg-danger-lt me-2">
@@ -62,7 +65,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
<div className="font-weight-medium">{...elms}</div> <div className="font-weight-medium">{...elms}</div>
{createdOn ? ( {createdOn ? (
<div className="text-secondary mt-1"> <div className="text-secondary mt-1">
<T id="created-on" data={{ date: formatDateTime(createdOn) }} /> <T id="created-on" data={{ date: formatDateTime(createdOn, locale) }} />
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -1,6 +1,7 @@
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
import cn from "classnames"; import cn from "classnames";
import type { AuditLog } from "src/api/backend"; import type { AuditLog } from "src/api/backend";
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
const getEventValue = (event: AuditLog) => { const getEventValue = (event: AuditLog) => {
@@ -66,6 +67,7 @@ interface Props {
row: AuditLog; row: AuditLog;
} }
export function EventFormatter({ row }: Props) { export function EventFormatter({ row }: Props) {
const { locale } = useLocaleState();
return ( return (
<div className="flex-fill"> <div className="flex-fill">
<div className="font-weight-medium"> <div className="font-weight-medium">
@@ -73,7 +75,7 @@ export function EventFormatter({ row }: Props) {
<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} /> <T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />
&nbsp; &mdash; <span className="badge">{getEventValue(row)}</span> &nbsp; &mdash; <span className="badge">{getEventValue(row)}</span>
</div> </div>
<div className="text-secondary mt-1">{formatDateTime(row.createdOn)}</div> <div className="text-secondary mt-1">{formatDateTime(row.createdOn, locale)}</div>
</div> </div>
); );
} }

View File

@@ -1,3 +1,4 @@
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
interface Props { interface Props {
@@ -6,6 +7,7 @@ interface Props {
disabled?: boolean; disabled?: boolean;
} }
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) { export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
const { locale } = useLocaleState();
return ( return (
<div className="flex-fill"> <div className="flex-fill">
<div className="font-weight-medium"> <div className="font-weight-medium">
@@ -13,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
</div> </div>
{createdOn ? ( {createdOn ? (
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}> <div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn) }} /> <T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn, locale) }} />
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -39,19 +39,19 @@ describe("DateFormatter", () => {
it("format date from iso date", () => { it("format date from iso date", () => {
const value = "2024-01-01T00:00:00.000Z"; const value = "2024-01-01T00:00:00.000Z";
const text = formatDateTime(value); const text = formatDateTime(value);
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am"); expect(text).toBe("1 Jan 2024, 12:00:00 am");
}); });
it("format date from unix timestamp number", () => { it("format date from unix timestamp number", () => {
const value = 1762476112; const value = 1762476112;
const text = formatDateTime(value); const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am"); expect(text).toBe("7 Nov 2025, 12:41:52 am");
}); });
it("format date from unix timestamp string", () => { it("format date from unix timestamp string", () => {
const value = "1762476112"; const value = "1762476112";
const text = formatDateTime(value); const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am"); expect(text).toBe("7 Nov 2025, 12:41:52 am");
}); });
it("catch bad format from string", () => { it("catch bad format from string", () => {

View File

@@ -1,4 +1,9 @@
import { fromUnixTime, intlFormat, parseISO } from "date-fns"; import {
fromUnixTime,
type IntlFormatFormatOptions,
intlFormat,
parseISO,
} from "date-fns";
const isUnixTimestamp = (value: unknown): boolean => { const isUnixTimestamp = (value: unknown): boolean => {
if (typeof value !== "number" && typeof value !== "string") return false; if (typeof value !== "number" && typeof value !== "string") return false;
@@ -20,20 +25,19 @@ const parseDate = (value: string | number): Date | null => {
} }
}; };
const formatDateTime = (value: string | number): string => { const formatDateTime = (value: string | number, locale = "en-US"): string => {
const d = parseDate(value); const d = parseDate(value);
if (!d) return `${value}`; if (!d) return `${value}`;
try { try {
return intlFormat(d, { return intlFormat(
weekday: "long", d,
year: "numeric", {
month: "numeric", dateStyle: "medium",
day: "numeric", timeStyle: "medium",
hour: "numeric", hourCycle: "h12",
minute: "numeric", } as IntlFormatFormatOptions,
second: "numeric", { locale },
hour12: true, );
});
} catch { } catch {
return `${value}`; return `${value}`;
} }