From 1f7a58443e3cd73bb2229785b8ec0b617a96a777 Mon Sep 17 00:00:00 2001 From: "Malik, Junaid" Date: Fri, 29 Dec 2023 13:59:12 +0800 Subject: [PATCH] #1073 add functionality to select date/time locales and timezones --- vuu-ui/cypress/pages/ShellWithNewTheme.ts | 2 +- .../LocalPersistenceManager.ts | 2 +- .../LocalLayoutPersistenceManager.test.ts | 2 +- .../DateTimeFormattingSettings.tsx | 128 +++++++++++------- .../src/table-settings/TableSettingsPanel.tsx | 63 ++++++++- .../src/table-settings/useTableSettings.ts | 20 +++ vuu-ui/packages/vuu-table-types/index.d.ts | 6 + .../packages/vuu-table/src/useTableModel.ts | 13 +- .../packages/vuu-utils/src/date/formatter.ts | 114 ++++++---------- vuu-ui/packages/vuu-utils/src/date/helpers.ts | 2 +- vuu-ui/packages/vuu-utils/src/date/index.ts | 5 + .../vuu-utils/src/date/locale-and-timezone.ts | 64 +++++++++ vuu-ui/packages/vuu-utils/src/date/types.ts | 15 +- .../vuu-utils/src/formatting-utils.ts | 13 +- .../vuu-utils/test/date/formatter.test.ts | 72 +++++++--- .../vuu-utils/test/date/helpers.test.ts | 2 +- .../test/date/locale-and-timezone.test.ts | 59 ++++++++ .../vuu-utils/test/date/types.test.ts | 6 +- .../ColumnSettings.examples.tsx | 2 +- 19 files changed, 425 insertions(+), 165 deletions(-) create mode 100644 vuu-ui/packages/vuu-utils/src/date/locale-and-timezone.ts create mode 100644 vuu-ui/packages/vuu-utils/test/date/locale-and-timezone.test.ts diff --git a/vuu-ui/cypress/pages/ShellWithNewTheme.ts b/vuu-ui/cypress/pages/ShellWithNewTheme.ts index 6195346b0..c0083a387 100644 --- a/vuu-ui/cypress/pages/ShellWithNewTheme.ts +++ b/vuu-ui/cypress/pages/ShellWithNewTheme.ts @@ -33,7 +33,7 @@ export class ShellWithNewTheme { creator: string, date: Date ) => { - const formattedDate = formatDate({ date: "dd.mm.yyyy" })(date); + const formattedDate = formatDate({ date: "ddmmyyyy" })(date); const layoutTileName = `${layoutName} ${creator}, ${formattedDate}`; return cy diff --git a/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts b/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts index 9f7e2f387..3a5daf1ba 100644 --- a/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts +++ b/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts @@ -32,7 +32,7 @@ export class LocalPersistenceManager implements PersistenceManager { const newMetadata: LayoutMetadata = { ...metadata, id, - created: formatDate({ date: "dd.mm.yyyy" })(new Date()), + created: formatDate({ date: "ddmmyyyy" })(new Date()), }; this.saveLayoutsWithMetadata( diff --git a/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index f0f681832..66eff9298 100644 --- a/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -31,7 +31,7 @@ const persistenceManager = new LocalPersistenceManager(); const existingId = "existing_id"; -const newDate = formatDate({ date: "dd.mm.yyyy" })(new Date()); +const newDate = formatDate({ date: "ddmmyyyy" })(new Date()); const existingMetadata: LayoutMetadata = { id: existingId, diff --git a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/DateTimeFormattingSettings.tsx b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/DateTimeFormattingSettings.tsx index ca7807f78..c5892e783 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/DateTimeFormattingSettings.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/DateTimeFormattingSettings.tsx @@ -11,6 +11,7 @@ import { import { FormField, FormFieldLabel, + Switch, ToggleButton, ToggleButtonGroup, } from "@salt-ds/core"; @@ -19,61 +20,18 @@ import { FormattingSettingsProps } from "./types"; export const DateTimeFormattingSettings: React.FC< FormattingSettingsProps -> = (props) => { - const { column, onChangeFormatting: onChange } = props; +> = ({ column, onChangeFormatting: onChange }) => { const formatting = getTypeFormattingFromColumn(column); const { pattern = fallbackDateTimePattern } = formatting; const toggleValue = useMemo(() => getToggleValue(pattern), [pattern]); - const [fallbackState, setFallbackState] = useState>( - { - time: pattern.time ?? defaultPatternsByType.time, - date: pattern.date ?? defaultPatternsByType.date, - } - ); - const onPatternChange = useCallback( (pattern: DateTimePattern) => onChange({ ...formatting, pattern }), [onChange, formatting] ); - const onDropdownChange = useCallback< - ( - key: T - ) => SingleSelectionHandler[T]> - >( - (key) => (_, p) => { - const updatedPattern = { ...(pattern ?? {}), [key]: p }; - setFallbackState((s) => ({ - time: updatedPattern.time ?? s.time, - date: updatedPattern.date ?? s.date, - })); - onPatternChange(updatedPattern); - }, - [onPatternChange, pattern] - ); - - const onToggleChange = useCallback( - (evnt: SyntheticEvent) => { - const value = evnt.currentTarget.value as ToggleValue; - switch (value) { - case "time": - return onPatternChange({ - [value]: pattern[value] ?? fallbackState[value], - }); - case "date": - return onPatternChange({ - [value]: pattern[value] ?? fallbackState[value], - }); - case "both": - return onPatternChange({ - time: pattern.time ?? fallbackState.time, - date: pattern.date ?? fallbackState.date, - }); - } - }, - [onPatternChange] - ); + const { onDropdownChange, onSwitchChange, onToggleChange } = + useDateTimeFormattingSettings({ pattern, onPatternChange }); return ( <> @@ -105,14 +63,16 @@ export const DateTimeFormattingSettings: React.FC< /> ))} + + + {"Show time-zone"} + + ); }; -const labelByType: Record = { - date: "Date", - time: "Time", -}; +const labelByType = { date: "Date", time: "Time" } as const; const toggleValues = ["date", "time", "both"] as const; @@ -127,3 +87,71 @@ function getToggleValue(pattern: DateTimePattern): ToggleValue { ? "time" : "both"; } + +type RequiredDateTimePattern = Required>; + +function useDateTimeFormattingSettings(props: { + pattern: DateTimePattern; + onPatternChange: (p: DateTimePattern) => void; +}) { + const { pattern, onPatternChange } = props; + const [fallbackState, setFallbackState] = useState({ + time: pattern.time ?? defaultPatternsByType.time, + date: pattern.date ?? defaultPatternsByType.date, + }); + + const onDropdownChange = useCallback< + ( + key: T + ) => SingleSelectionHandler + >( + (key) => (_, p) => { + const updatedPattern = { ...(pattern ?? {}), [key]: p }; + setFallbackState((s) => ({ + time: updatedPattern.time ?? s.time, + date: updatedPattern.date ?? s.date, + })); + onPatternChange(updatedPattern); + }, + [onPatternChange, pattern] + ); + + const onToggleChange = useCallback( + (evnt: SyntheticEvent) => { + const value = evnt.currentTarget.value as ToggleValue; + switch (value) { + case "time": + return onPatternChange({ + ...pattern, + time: pattern.time ?? fallbackState.time, + date: undefined, + }); + case "date": + return onPatternChange({ + ...pattern, + time: undefined, + date: pattern.date ?? fallbackState.date, + }); + case "both": + return onPatternChange({ + ...pattern, + time: pattern.time ?? fallbackState.time, + date: pattern.date ?? fallbackState.date, + }); + } + }, + [onPatternChange, pattern, fallbackState] + ); + + const onSwitchChange = useCallback< + React.ChangeEventHandler + >( + (e) => { + const { checked: showTimeZone } = e.target; + onPatternChange({ ...pattern, showTimeZone }); + }, + [onPatternChange, pattern] + ); + + return { onDropdownChange, onSwitchChange, onToggleChange }; +} diff --git a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx index 7da4e5782..e9e5628ca 100644 --- a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx @@ -1,3 +1,5 @@ +import { useMemo } from "react"; +import { Dropdown, SingleSelectionHandler } from "@finos/vuu-ui-controls"; import { Button, FormField, @@ -6,7 +8,15 @@ import { ToggleButton, ToggleButtonGroup, } from "@salt-ds/core"; -import { TableSettingsProps } from "@finos/vuu-table-types"; +import { + localeOptions, + timeZoneOptions, + getDefaultLocaleAndTimeZone, +} from "@finos/vuu-utils"; +import { + DateTimeTableAttributes, + TableSettingsProps, +} from "@finos/vuu-table-types"; import { ColumnList } from "../column-list"; import { useTableSettings } from "./useTableSettings"; @@ -33,6 +43,7 @@ export const TableSettingsPanel = ({ onChangeColumnLabels, onChangeTableAttribute, onColumnChange, + onChangeDateTimeAttribute, onMoveListItem, tableConfig, } = useTableSettings({ @@ -101,6 +112,12 @@ export const TableSettingsPanel = ({ + + ); }; + +const DateTimeAttributesSettings: React.FC<{ + dateTimeAttrs: DateTimeTableAttributes; + onLocaleChange: SingleSelectionHandler; + onTimeZoneChange: SingleSelectionHandler; +}> = ({ dateTimeAttrs, onLocaleChange, onTimeZoneChange }) => { + const { locale: defaultLocale, timeZone: defaultTimeZone } = + getDefaultLocaleAndTimeZone(); + const { locale = defaultLocale, timeZone = defaultTimeZone } = dateTimeAttrs; + + const localesSource = useMemo( + () => [...new Set([...localeOptions, locale, defaultLocale])].sort(), + [locale, defaultLocale] + ); + + const timeZonesSource = useMemo( + () => [...new Set([...timeZoneOptions, timeZone, defaultTimeZone])].sort(), + [timeZone, defaultTimeZone] + ); + + return ( + <> + + Date/time locale + + + + + Time-zone + + + + ); +}; diff --git a/vuu-ui/packages/vuu-table-extras/src/table-settings/useTableSettings.ts b/vuu-ui/packages/vuu-table-extras/src/table-settings/useTableSettings.ts index 6fe6af067..acecdd579 100644 --- a/vuu-ui/packages/vuu-table-extras/src/table-settings/useTableSettings.ts +++ b/vuu-ui/packages/vuu-table-extras/src/table-settings/useTableSettings.ts @@ -2,6 +2,7 @@ import { SchemaColumn } from "@finos/vuu-data-types"; import { updateTableConfig } from "@finos/vuu-table"; import { ColumnDescriptor, + DateTimeTableAttributes, TableConfig, TableSettingsProps, } from "@finos/vuu-table-types"; @@ -20,6 +21,7 @@ import { useState, } from "react"; import { ColumnChangeHandler } from "../column-list"; +import { SingleSelectionHandler } from "@finos/vuu-ui-controls"; const sortOrderFromAvailableColumns = ( availableColumns: SchemaColumn[], @@ -194,6 +196,23 @@ export const useTableSettings = ({ [] ); + const handleChangeDateTimeAttribute = useCallback< + ( + key: T + ) => SingleSelectionHandler + >( + (key) => (_, value) => { + setColumnState((s) => ({ + ...s, + tableConfig: { + ...s.tableConfig, + dateTime: { ...s.tableConfig.dateTime, [key]: value }, + }, + })); + }, + [] + ); + useLayoutEffectSkipFirst(() => { onConfigChange?.(tableConfig); }, [onConfigChange, tableConfig]); @@ -211,6 +230,7 @@ export const useTableSettings = ({ onChangeColumnLabels: handleChangeColumnLabels, onChangeTableAttribute: handleChangeTableAttribute, onColumnChange: handleColumnChange, + onChangeDateTimeAttribute: handleChangeDateTimeAttribute, onMoveListItem: handleMoveListItem, tableConfig, }; diff --git a/vuu-ui/packages/vuu-table-types/index.d.ts b/vuu-ui/packages/vuu-table-types/index.d.ts index a2a37f6a8..4d10ebe3b 100644 --- a/vuu-ui/packages/vuu-table-types/index.d.ts +++ b/vuu-ui/packages/vuu-table-types/index.d.ts @@ -52,6 +52,11 @@ export interface TableCellRendererProps onCommit?: DataItemCommitHandler; } +export declare type DateTimeTableAttributes = { + timeZone?: string; + locale?: string; +}; + export interface TableAttributes { columnDefaultWidth?: number; columnFormatHeader?: "capitalize" | "uppercase"; @@ -59,6 +64,7 @@ export interface TableAttributes { // showHighlightedRow?: boolean; rowSeparators?: boolean; zebraStripes?: boolean; + dateTime?: DateTimeTableAttributes; } /** diff --git a/vuu-ui/packages/vuu-table/src/useTableModel.ts b/vuu-ui/packages/vuu-table/src/useTableModel.ts index 7c0bfc2dd..3965654d7 100644 --- a/vuu-ui/packages/vuu-table/src/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/useTableModel.ts @@ -288,8 +288,11 @@ const columnDescriptorToRuntimeColumDescriptor = column: ColumnDescriptor & { key?: number }, index: number ): RuntimeColumnDescriptor => { - const { columnDefaultWidth = DEFAULT_COLUMN_WIDTH, columnFormatHeader } = - tableAttributes; + const { + columnDefaultWidth = DEFAULT_COLUMN_WIDTH, + columnFormatHeader, + dateTime: dateTimeTableAttributes, + } = tableAttributes; const serverDataType = getDataType(column, tableSchema); const { align = getDefaultAlignment(serverDataType), @@ -314,7 +317,11 @@ const columnDescriptorToRuntimeColumDescriptor = name, originalIdx: index, serverDataType, - valueFormatter: getValueFormatter(column, serverDataType), + valueFormatter: getValueFormatter( + column, + serverDataType, + dateTimeTableAttributes + ), width: width, }; diff --git a/vuu-ui/packages/vuu-utils/src/date/formatter.ts b/vuu-ui/packages/vuu-utils/src/date/formatter.ts index 5b67aa363..199f1c201 100644 --- a/vuu-ui/packages/vuu-utils/src/date/formatter.ts +++ b/vuu-ui/packages/vuu-utils/src/date/formatter.ts @@ -1,91 +1,65 @@ -import { isNotNullOrUndefined } from "../ts-utils"; +import { DateTimeTableAttributes } from "@finos/vuu-table-types"; import { DatePattern, DateTimePattern, TimePattern } from "./types"; - -type DateTimeFormatConfig = { - locale?: string; - options: Intl.DateTimeFormatOptions; - postProcessor?: (s: string) => string; -}; +import { + validateLocaleOrGetDefault, + validateTimeZoneOrGetDefault, +} from "./locale-and-timezone"; // Time format config -const baseTimeFormatOptions: Intl.DateTimeFormatOptions = { +const baseTimeFormatConfig: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", second: "2-digit", }; -const formatConfigByTimePatterns: Record = { - "hh:mm:ss": { - locale: "en-GB", - options: { ...baseTimeFormatOptions, hour12: false }, - }, - "hh:mm:ss a": { - locale: "en-GB", - options: { ...baseTimeFormatOptions, hour12: true }, - }, +const formatConfigByTimePatterns: Record< + TimePattern, + Intl.DateTimeFormatOptions +> = { + "hh:mm:ss": { ...baseTimeFormatConfig, hour12: false }, + "hh:mm:ss a": { ...baseTimeFormatConfig, hour12: true }, }; // Date format config -const baseDateFormatOptions: Intl.DateTimeFormatOptions = { +const baseDateFormatConfig: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit", year: "numeric", }; -const formatConfigByDatePatterns: Record = { - "dd.mm.yyyy": { - locale: "en-GB", - options: { ...baseDateFormatOptions }, - postProcessor: (s) => s.replaceAll("/", "."), - }, - "dd/mm/yyyy": { locale: "en-GB", options: { ...baseDateFormatOptions } }, - "dd MMM yyyy": { - locale: "en-GB", - options: { ...baseDateFormatOptions, month: "short" }, - }, - "dd MMMM yyyy": { - locale: "en-GB", - options: { ...baseDateFormatOptions, month: "long" }, - }, - "mm/dd/yyyy": { locale: "en-US", options: { ...baseDateFormatOptions } }, - "MMM dd, yyyy": { - locale: "en-US", - options: { ...baseDateFormatOptions, month: "short" }, - }, - "MMMM dd, yyyy": { - locale: "en-US", - options: { ...baseDateFormatOptions, month: "long" }, - }, +const formatConfigByDatePatterns: Record< + DatePattern, + Intl.DateTimeFormatOptions +> = { + ddmmyyyy: { ...baseDateFormatConfig }, + ddMMMyyyy: { ...baseDateFormatConfig, month: "short" }, + ddMMMMyyyy: { ...baseDateFormatConfig, month: "long" }, }; -function getFormatConfigs(pattern: DateTimePattern) { - return [ - isNotNullOrUndefined(pattern["date"]) - ? formatConfigByDatePatterns[pattern["date"]] - : null, - isNotNullOrUndefined(pattern["time"]) - ? formatConfigByTimePatterns[pattern["time"]] - : null, - ]; -} - -function applyFormatting( - d: Date, - opts: Pick & { - dateTimeFormat: Intl.DateTimeFormat; +function getFormatConfig(pattern: DateTimePattern) { + if (!pattern.date) { + return formatConfigByTimePatterns[pattern.time]; + } else if (!pattern.time) { + return formatConfigByDatePatterns[pattern.date]; + } else { + return { + ...formatConfigByDatePatterns[pattern.date], + ...formatConfigByTimePatterns[pattern.time], + }; } -): string { - const { dateTimeFormat, postProcessor } = opts; - const dateStr = dateTimeFormat.format(d); - return postProcessor ? postProcessor(dateStr) : dateStr; } -export function formatDate(pattern: DateTimePattern): (d: Date) => string { - const formattingOpts = getFormatConfigs(pattern) - .filter(isNotNullOrUndefined) - .map((c) => ({ - dateTimeFormat: Intl.DateTimeFormat(c.locale, c.options), - postProcessor: c.postProcessor, - })); +export function formatDate( + pattern: DateTimePattern, + { locale, timeZone }: DateTimeTableAttributes = {} +): (d: Date) => string { + const formatConfig = getFormatConfig(pattern); + const validatedLocale = validateLocaleOrGetDefault(locale); + const validatedTimeZone = validateTimeZoneOrGetDefault(timeZone); + + const dateTimeFormat = Intl.DateTimeFormat(validatedLocale, { + ...formatConfig, + timeZoneName: !!pattern.showTimeZone ? "short" : undefined, + timeZone: validatedTimeZone, + }); - return (d) => - formattingOpts.map((opts) => applyFormatting(d, opts)).join(" "); + return dateTimeFormat.format; } diff --git a/vuu-ui/packages/vuu-utils/src/date/helpers.ts b/vuu-ui/packages/vuu-utils/src/date/helpers.ts index 23a735d13..ccb66c00c 100644 --- a/vuu-ui/packages/vuu-utils/src/date/helpers.ts +++ b/vuu-ui/packages/vuu-utils/src/date/helpers.ts @@ -4,7 +4,7 @@ import { DateTimePattern, isDateTimePattern } from "./types"; export const defaultPatternsByType = { time: "hh:mm:ss", - date: "dd.mm.yyyy", + date: "ddmmyyyy", } as const; export const fallbackDateTimePattern: DateTimePattern = { diff --git a/vuu-ui/packages/vuu-utils/src/date/index.ts b/vuu-ui/packages/vuu-utils/src/date/index.ts index a6d6412df..1de010423 100644 --- a/vuu-ui/packages/vuu-utils/src/date/index.ts +++ b/vuu-ui/packages/vuu-utils/src/date/index.ts @@ -5,3 +5,8 @@ export { supportedDateTimePatterns, } from "./types"; export { defaultPatternsByType, fallbackDateTimePattern } from "./helpers"; +export { + getDefaultLocaleAndTimeZone, + timeZoneOptions, + localeOptions, +} from "./locale-and-timezone"; diff --git a/vuu-ui/packages/vuu-utils/src/date/locale-and-timezone.ts b/vuu-ui/packages/vuu-utils/src/date/locale-and-timezone.ts new file mode 100644 index 000000000..76e3536e1 --- /dev/null +++ b/vuu-ui/packages/vuu-utils/src/date/locale-and-timezone.ts @@ -0,0 +1,64 @@ +import { logger } from "../logging-utils"; + +const { warn } = logger("Date/time validation"); + +type LocaleAndTimeZone = { locale: string; timeZone: string }; +type AttributeType = keyof LocaleAndTimeZone; + +export function getDefaultLocaleAndTimeZone(): LocaleAndTimeZone { + return Intl.DateTimeFormat().resolvedOptions(); +} + +function validateOrGetDefault(type: AttributeType, value?: string): string { + const { locale, options } = + type === "locale" + ? { locale: value, options: {} } + : { locale: undefined, options: { [type]: value } }; + + try { + // if invalid it either throws or falls back to default locale/timeZone + return Intl.DateTimeFormat(locale, options).resolvedOptions()[type]; + } catch (_) { + return getDefaultLocaleAndTimeZone()[type]; + } +} + +function validateOrGetDefaultWithWarning( + type: AttributeType, + value?: string +): string { + const validatedValue = validateOrGetDefault(type, value); + + if (value !== undefined && value !== validatedValue) { + warn?.(`Invalid ${type} ${value} passed. Falling back to user's default.`); + } + + return validatedValue; +} + +export function validateLocaleOrGetDefault(locale?: string): string { + return validateOrGetDefaultWithWarning("locale", locale); +} + +export function validateTimeZoneOrGetDefault(timeZone?: string): string { + return validateOrGetDefaultWithWarning("timeZone", timeZone); +} + +export const localeOptions = [ + "de-DE", + "en-GB", + "en-US", + "ja-JP", + "zh-Hans-CN", +] as const; + +export const timeZoneOptions = [ + "America/Los_Angeles", + "America/New_York", + "Asia/Shanghai", + "Asia/Tokyo", + "Australia/Sydney", + "Australia/Perth", + "Europe/Berlin", + "Europe/London", +] as const; diff --git a/vuu-ui/packages/vuu-utils/src/date/types.ts b/vuu-ui/packages/vuu-utils/src/date/types.ts index f5ae5f6dc..6f304bb70 100644 --- a/vuu-ui/packages/vuu-utils/src/date/types.ts +++ b/vuu-ui/packages/vuu-utils/src/date/types.ts @@ -1,14 +1,6 @@ import { ColumnTypeFormatting } from "@finos/vuu-table-types"; -const supportedDatePatterns = [ - "dd.mm.yyyy", - "dd/mm/yyyy", - "dd MMM yyyy", - "dd MMMM yyyy", - "mm/dd/yyyy", - "MMM dd, yyyy", - "MMMM dd, yyyy", -] as const; +const supportedDatePatterns = ["ddmmyyyy", "ddMMMyyyy", "ddMMMMyyyy"] as const; const supportedTimePatterns = ["hh:mm:ss", "hh:mm:ss a"] as const; @@ -20,9 +12,10 @@ export const supportedDateTimePatterns = { export type DatePattern = (typeof supportedDatePatterns)[number]; export type TimePattern = (typeof supportedTimePatterns)[number]; -export type DateTimePattern = +export type DateTimePattern = { showTimeZone?: boolean } & ( | { date?: DatePattern; time: TimePattern } - | { date: DatePattern; time?: TimePattern }; + | { date: DatePattern; time?: TimePattern } +); const isDatePattern = (pattern?: string): pattern is DatePattern => supportedDatePatterns.includes(pattern as DatePattern); diff --git a/vuu-ui/packages/vuu-utils/src/formatting-utils.ts b/vuu-ui/packages/vuu-utils/src/formatting-utils.ts index eaa3f4d91..6d353974e 100644 --- a/vuu-ui/packages/vuu-utils/src/formatting-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/formatting-utils.ts @@ -3,6 +3,7 @@ import { ColumnTypeValueMap, ColumnTypeFormatting, DateTimeColumnDescriptor, + DateTimeTableAttributes, } from "@finos/vuu-table-types"; import { roundDecimal } from "./round-decimal"; import { @@ -23,9 +24,12 @@ const DEFAULT_NUMERIC_FORMAT: ColumnTypeFormatting = {}; export const defaultValueFormatter = (value: unknown) => value == null ? "" : typeof value === "string" ? value : value.toString(); -const dateFormatter = (column: DateTimeColumnDescriptor) => { +const dateFormatter = ( + column: DateTimeColumnDescriptor, + opts?: DateTimeTableAttributes +) => { const pattern = dateTimePattern(column.type); - const formatter = formatDate(pattern); + const formatter = formatDate(pattern, opts); return (value: unknown) => { if (typeof value === "number" && value !== 0) { @@ -74,10 +78,11 @@ const mapFormatter = (map: ColumnTypeValueMap) => { export const getValueFormatter = ( column: ColumnDescriptor, - serverDataType = column.serverDataType + serverDataType = column.serverDataType, + opts?: DateTimeTableAttributes ): ValueFormatter => { if (isDateTimeColumn(column)) { - return dateFormatter(column); + return dateFormatter(column, opts); } const { type } = column; diff --git a/vuu-ui/packages/vuu-utils/test/date/formatter.test.ts b/vuu-ui/packages/vuu-utils/test/date/formatter.test.ts index 95da9f534..8f9a6a4f0 100644 --- a/vuu-ui/packages/vuu-utils/test/date/formatter.test.ts +++ b/vuu-ui/packages/vuu-utils/test/date/formatter.test.ts @@ -1,32 +1,70 @@ import { describe, expect, it } from "vitest"; import { formatDate } from "../../src/date/formatter"; import { DateTimePattern } from "../../src/date/types"; +import { DateTimeTableAttributes } from "@finos/vuu-table-types"; const testDate = new Date(2010, 5, 12, 15, 50, 37); describe("formatDate", () => { - it.each<{ pattern: DateTimePattern; expected: string }>([ - { pattern: { date: "dd.mm.yyyy" }, expected: "12.06.2010" }, - { pattern: { date: "dd/mm/yyyy" }, expected: "12/06/2010" }, - { pattern: { date: "dd MMM yyyy" }, expected: "12 Jun 2010" }, - { pattern: { date: "dd MMMM yyyy" }, expected: "12 June 2010" }, - { pattern: { date: "mm/dd/yyyy" }, expected: "06/12/2010" }, - { pattern: { date: "MMM dd, yyyy" }, expected: "Jun 12, 2010" }, - { pattern: { date: "MMMM dd, yyyy" }, expected: "June 12, 2010" }, - { pattern: { time: "hh:mm:ss" }, expected: "15:50:37" }, - { pattern: { time: "hh:mm:ss a" }, expected: "03:50:37 pm" }, + it.each<{ + pattern: DateTimePattern; + opts: DateTimeTableAttributes; + expected: string; + }>([ { - pattern: { date: "dd.mm.yyyy", time: "hh:mm:ss a" }, - expected: "12.06.2010 03:50:37 pm", + pattern: { date: "ddmmyyyy" }, + opts: { locale: "en-GB" }, + expected: "12/06/2010", }, { - pattern: { date: "MMMM dd, yyyy", time: "hh:mm:ss a" }, - expected: "June 12, 2010 03:50:37 pm", + pattern: { date: "ddMMMyyyy" }, + opts: { locale: "en-GB" }, + expected: "12 Jun 2010", + }, + { + pattern: { date: "ddMMMMyyyy" }, + opts: { locale: "en-GB" }, + expected: "12 June 2010", + }, + { + pattern: { date: "ddmmyyyy" }, + opts: { locale: "en-US" }, + expected: "06/12/2010", + }, + { + pattern: { date: "ddMMMyyyy" }, + opts: { locale: "en-US" }, + expected: "Jun 12, 2010", + }, + { + pattern: { date: "ddMMMMyyyy" }, + opts: { locale: "en-US" }, + expected: "June 12, 2010", + }, + { + pattern: { time: "hh:mm:ss" }, + opts: { locale: "en-GB" }, + expected: "15:50:37", + }, + { + pattern: { time: "hh:mm:ss a" }, + opts: { locale: "en-GB" }, + expected: "03:50:37 pm", + }, + { + pattern: { date: "ddmmyyyy", time: "hh:mm:ss a" }, + opts: { locale: "en-GB" }, + expected: "12/06/2010, 03:50:37 pm", + }, + { + pattern: { date: "ddMMMMyyyy", time: "hh:mm:ss a" }, + opts: { locale: "en-US" }, + expected: "June 12, 2010 at 03:50:37 PM", }, ])( - "can correctly format date with the given pattern $pattern", - ({ pattern, expected }) => { - const actual = formatDate(pattern)(testDate); + "can correctly format date with the given pattern $pattern and opts $opts", + ({ pattern, opts, expected }) => { + const actual = formatDate(pattern, opts)(testDate); expect(actual).toEqual(expected); } ); diff --git a/vuu-ui/packages/vuu-utils/test/date/helpers.test.ts b/vuu-ui/packages/vuu-utils/test/date/helpers.test.ts index 8a34cf6d4..32132b995 100644 --- a/vuu-ui/packages/vuu-utils/test/date/helpers.test.ts +++ b/vuu-ui/packages/vuu-utils/test/date/helpers.test.ts @@ -5,7 +5,7 @@ import { } from "../../src/date/helpers"; import { DateTimePattern } from "../../src/date/types"; -const testPattern: DateTimePattern = { date: "mm/dd/yyyy" }; +const testPattern: DateTimePattern = { date: "ddmmyyyy" }; describe("dateTimePattern", () => { it("returns exact pattern when found in descriptor type", () => { diff --git a/vuu-ui/packages/vuu-utils/test/date/locale-and-timezone.test.ts b/vuu-ui/packages/vuu-utils/test/date/locale-and-timezone.test.ts new file mode 100644 index 000000000..7c99da5c9 --- /dev/null +++ b/vuu-ui/packages/vuu-utils/test/date/locale-and-timezone.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { + localeOptions, + timeZoneOptions, + validateLocaleOrGetDefault, + validateTimeZoneOrGetDefault, +} from "../../src/date/locale-and-timezone"; + +const defaultResolvedOptions = Intl.DateTimeFormat().resolvedOptions(); + +describe("validateLocaleOrGetDefault", () => { + it("falls back to default when locale is invalid", () => { + const actual = validateLocaleOrGetDefault("invalid-locale"); + + expect(actual).toEqual(defaultResolvedOptions.locale); + }); + + it("returns locale unchanged when valid", () => { + const actual = validateLocaleOrGetDefault("en-US"); + + expect(actual).toEqual("en-US"); + }); + + it("falls back to default when locale is undefined", () => { + const actual = validateLocaleOrGetDefault(undefined); + + expect(actual).toEqual(defaultResolvedOptions.locale); + }); +}); + +describe("validateTimeZoneOrGetDefault", () => { + it("falls back to default when time zone is invalid", () => { + const actual = validateTimeZoneOrGetDefault("Invalid/London"); + + expect(actual).toEqual(defaultResolvedOptions.timeZone); + }); + + it("returns time zone unchanged when valid", () => { + const actual = validateTimeZoneOrGetDefault("Asia/Hong_Kong"); + + expect(actual).toEqual("Asia/Hong_Kong"); + }); + + it("falls back to default when time zone is undefined", () => { + const actual = validateTimeZoneOrGetDefault(undefined); + + expect(actual).toEqual(defaultResolvedOptions.timeZone); + }); +}); + +describe("timezone/locale options", () => { + it.each(timeZoneOptions)("timezone option $timeZone is valid", (timeZone) => { + expect(validateTimeZoneOrGetDefault(timeZone)).toEqual(timeZone); + }); + + it.each(localeOptions)("locale option $locale is valid", (locale) => { + expect(validateLocaleOrGetDefault(locale)).toEqual(locale); + }); +}); diff --git a/vuu-ui/packages/vuu-utils/test/date/types.test.ts b/vuu-ui/packages/vuu-utils/test/date/types.test.ts index 037907f2d..dc1be1e51 100644 --- a/vuu-ui/packages/vuu-utils/test/date/types.test.ts +++ b/vuu-ui/packages/vuu-utils/test/date/types.test.ts @@ -4,12 +4,12 @@ import { ColumnTypeFormatting } from "@finos/vuu-table-types"; describe("isDateTimePattern", () => { it.each<{ pattern: ColumnTypeFormatting["pattern"]; expected: boolean }>([ - { pattern: { date: "dd MMM yyyy" }, expected: true }, + { pattern: { date: "ddMMMyyyy" }, expected: true }, { pattern: { time: "hh:mm:ss a" }, expected: true }, - { pattern: { date: "dd/mm/yyyy", time: "hh:mm:ss" }, expected: true }, + { pattern: { date: "ddmmyyyy", time: "hh:mm:ss" }, expected: true }, { pattern: undefined, expected: false }, ])( - "returns $expected when pattern is a DateTimePattern", + "returns true only when pattern is a DateTimePattern", ({ pattern, expected }) => { const actual = isDateTimePattern(pattern); expect(actual).toEqual(expected); diff --git a/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx b/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx index 99d13ad1d..32830bf22 100644 --- a/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx +++ b/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx @@ -78,7 +78,7 @@ export const ColumnFormattingPanelDateTime = () => { type: { name: "date/time", formatting: { - pattern: { date: "MMMM dd, yyyy", time: "hh:mm:ss" }, + pattern: { date: "ddMMMMyyyy", time: "hh:mm:ss" }, }, }, });