Skip to content

Commit

Permalink
Support date/dateTime formatting (#2589)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Sep 13, 2023
1 parent fee8c18 commit 01e3cc6
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 14 deletions.
22 changes: 22 additions & 0 deletions packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '@mui/toolpad-components';
import { generateUniqueString } from '@mui/toolpad-utils/strings';
import { NumberFormatEditor } from '@mui/toolpad-core/numberFormat';
import { DateFormatEditor } from '@mui/toolpad-core/dateFormat';
import type { EditorProps } from '../../types';
import { useToolpadComponents } from '../AppEditor/toolpadComponents';
import { ToolpadComponentDefinition } from '../../runtime/toolpadComponents';
Expand Down Expand Up @@ -208,6 +209,27 @@ function GridColumnEditor({
/>
) : null}

{editedColumn.type === 'date' ? (
<DateFormatEditor
disabled={disabled}
disableTimeFormat
value={editedColumn.dateFormat}
onChange={(dateFormat) => {
handleColumnChange({ ...editedColumn, dateFormat });
}}
/>
) : null}

{editedColumn.type === 'dateTime' ? (
<DateFormatEditor
disabled={disabled}
value={editedColumn.dateTimeFormat}
onChange={(dateTimeFormat) => {
handleColumnChange({ ...editedColumn, dateTimeFormat });
}}
/>
) : null}

{editedColumn.type === 'codeComponent' ? (
<TextField
select
Expand Down
24 changes: 22 additions & 2 deletions packages/toolpad-components/src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import { getObjectKey } from '@mui/toolpad-utils/objectKey';
import { errorFrom } from '@mui/toolpad-utils/errors';
import { hasImageExtension } from '@mui/toolpad-utils/path';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { NumberFormat, createStringFormatter } from '@mui/toolpad-core/numberFormat';
import { NumberFormat, createFormat as createNumberFormat } from '@mui/toolpad-core/numberFormat';
import { DateFormat, createFormat as createDateFormat } from '@mui/toolpad-core/dateFormat';
import createBuiltin from './createBuiltin';
import { SX_PROP_HELPER_TEXT } from './constants';
import ErrorOverlay from './components/ErrorOverlay';
Expand Down Expand Up @@ -303,6 +304,8 @@ export const CUSTOM_COLUMN_TYPES: Record<string, GridColTypeDef> = {
export interface SerializableGridColumn
extends Pick<GridColDef, 'field' | 'type' | 'align' | 'width' | 'headerName'> {
numberFormat?: NumberFormat;
dateFormat?: DateFormat;
dateTimeFormat?: DateFormat;
codeComponent?: string;
}

Expand All @@ -325,9 +328,26 @@ export function inferColumns(rows: GridRowsProp): SerializableGridColumns {
export function parseColumns(columns: SerializableGridColumns): GridColDef[] {
return columns.map((column) => {
if (column.type === 'number' && column.numberFormat) {
const format = createNumberFormat(column.numberFormat);
return {
...column,
valueFormatter: createStringFormatter(column.numberFormat),
valueFormatter: ({ value }) => format.format(value),
};
}

if (column.type === 'date' && column.dateFormat) {
const format = createDateFormat(column.dateFormat);
return {
...column,
valueFormatter: ({ value }) => format.format(value),
};
}

if (column.type === 'dateTime' && column.dateTimeFormat) {
const format = createDateFormat(column.dateTimeFormat);
return {
...column,
valueFormatter: ({ value }) => format.format(value),
};
}

Expand Down
166 changes: 166 additions & 0 deletions packages/toolpad-core/src/dateFormat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { MenuItem, Stack, SxProps, TextField, styled } from '@mui/material';
import * as React from 'react';

export interface DateFormatPreset {
label?: string;
}

export type DateStyle = Intl.DateTimeFormatOptions['dateStyle'];
export type TimeStyle = Intl.DateTimeFormatOptions['timeStyle'];

const DEFAULT_DATE_STYLE: DateStyle = 'short';
const DEFAULT_TIME_STYLE: TimeStyle = 'short';

export const DATE_STYLES = new Map<DateStyle, DateFormatPreset>([
['short', { label: 'Short' }],
['medium', { label: 'Medium' }],
['long', { label: 'Long' }],
['full', { label: 'Full' }],
]);

export const TIME_STYLES = new Map<TimeStyle, DateFormatPreset>([
['short', { label: 'Short' }],
['medium', { label: 'Medium' }],
['long', { label: 'Long' }],
['full', { label: 'Full' }],
]);

const DATE_FORMATS = new Map<DateStyle, Intl.DateTimeFormat>(
(['short', 'medium', 'long', 'full'] as const).map((dateStyle) => [
dateStyle,
new Intl.DateTimeFormat(undefined, { dateStyle }),
]),
);

const TIME_FORMATS = new Map<DateStyle, Intl.DateTimeFormat>(
(['short', 'medium', 'long', 'full'] as const).map((timeStyle) => [
timeStyle,
new Intl.DateTimeFormat(undefined, { timeStyle }),
]),
);

// Constructing a demo date whether the time is 12h or 24h.
const DEMO_DATE = new Date();
DEMO_DATE.setHours(13);
DEMO_DATE.setMinutes(28);
DEMO_DATE.setSeconds(54);

export interface DateFormat {
kind: 'shorthand';
dateStyle?: DateStyle;
timeStyle?: TimeStyle;
}

export function createFormat(dateFormat?: DateFormat) {
if (!dateFormat) {
return new Intl.DateTimeFormat(undefined, {});
}
switch (dateFormat.kind) {
case 'shorthand': {
const { dateStyle, timeStyle } = dateFormat;
return new Intl.DateTimeFormat(undefined, { dateStyle, timeStyle });
}
default: {
return new Intl.DateTimeFormat();
}
}
}

const PrettyDateFormatRoot = styled('span')({});

const DEFAULT_FORMAT = new Intl.DateTimeFormat();

export interface FormattedDateProps {
format?: Intl.DateTimeFormat;
children: Date | number;
}

export function FormattedDate({ children, format = DEFAULT_FORMAT }: FormattedDateProps) {
const parts = React.useMemo(() => format.formatToParts(children), [children, format]);
return (
<PrettyDateFormatRoot>
{parts.map((part, i) => (
<span key={i} className={`date-token-type-${part.type}`}>
{part.value}
</span>
))}
</PrettyDateFormatRoot>
);
}

export interface DateFormatEditorProps {
value?: DateFormat;
onChange: (newValue?: DateFormat) => void;
disabled?: boolean;
sx?: SxProps;
label?: string;
disableTimeFormat?: boolean;
}

export function DateFormatEditor({
label,
disabled,
value,
onChange,
sx,
disableTimeFormat,
}: DateFormatEditorProps) {
return (
<Stack sx={sx} gap={1}>
<TextField
select
fullWidth
label={label ?? 'Date format'}
value={value?.dateStyle || DEFAULT_DATE_STYLE}
disabled={disabled}
onChange={(event) => {
let dateFormat: DateFormat | undefined;

if (event.target.value) {
dateFormat = {
kind: 'shorthand',
dateStyle: event.target.value as DateStyle,
timeStyle: disableTimeFormat ? undefined : value?.timeStyle || DEFAULT_TIME_STYLE,
};
}

onChange(dateFormat);
}}
>
{Array.from(DATE_STYLES, ([type, preset]) => (
<MenuItem key={type} value={type}>
{DATE_FORMATS.get(type)?.format(DEMO_DATE) || preset.label || type}
</MenuItem>
))}
</TextField>
{disableTimeFormat ? null : (
<TextField
select
fullWidth
label={label ?? 'Time format'}
value={value?.timeStyle || DEFAULT_TIME_STYLE}
disabled={disabled}
onChange={(event) => {
let dateFormat: DateFormat | undefined;

if (event.target.value) {
dateFormat = {
kind: 'shorthand',
dateStyle: value?.dateStyle || DEFAULT_DATE_STYLE,
timeStyle: event.target.value as TimeStyle,
};
}

onChange(dateFormat);
}}
>
{Array.from(TIME_STYLES, ([type, preset]) => (
<MenuItem key={type} value={type}>
{TIME_FORMATS.get(type)?.format(DEMO_DATE) || preset.label || type}
</MenuItem>
))}
</TextField>
)}
</Stack>
);
}
19 changes: 7 additions & 12 deletions packages/toolpad-core/src/numberFormat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const NUMBER_FORMAT_SCHEMA: JSONSchema7 = {
],
};

interface NumberFormatPreset {
export interface NumberFormatPreset {
label?: string;
options?: Intl.NumberFormatOptions;
}
Expand Down Expand Up @@ -125,30 +125,25 @@ export function createFormat(numberFormat?: NumberFormat): Intl.NumberFormat {
switch (numberFormat.kind) {
case 'preset': {
const preset = NUMBER_FORMAT_PRESETS.get(numberFormat.preset);
return Intl.NumberFormat(undefined, preset?.options);
return new Intl.NumberFormat(undefined, preset?.options);
}
case 'custom': {
return Intl.NumberFormat(undefined, numberFormat.custom);
return new Intl.NumberFormat(undefined, numberFormat.custom);
}
case 'currency': {
const userInput = numberFormat.currency || 'USD';
return Intl.NumberFormat(undefined, {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: ACCEPTABLE_CURRENCY_REGEX.test(userInput) ? userInput : 'USD',
});
}
default: {
return Intl.NumberFormat();
return new Intl.NumberFormat();
}
}
}

export function createStringFormatter(numberFormat?: NumberFormat): NumberFormatter {
const format = createFormat(numberFormat);
return ({ value }) => format.format(Number(value));
}

interface FormattedNumberProps {
export interface FormattedNumberProps {
format?: Intl.NumberFormat;
children: number | string;
}
Expand Down Expand Up @@ -181,7 +176,7 @@ function formatNumberOptionValue(numberFormat: NumberFormat | undefined) {
}
switch (numberFormat.kind) {
case 'preset':
return `preset:${numberFormat.preset}`;
return ['preset', numberFormat.preset].join(':');
case 'custom':
return 'custom';
case 'currency':
Expand Down
1 change: 1 addition & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"@mui/toolpad-components": ["../packages/toolpad-components/src/index.tsx"],
"@mui/toolpad-core": ["../packages/toolpad-core/src/index.tsx"],
"@mui/toolpad-core/numberFormat": ["../packages/toolpad-core/src/numberFormat.tsx"],
"@mui/toolpad-core/dateFormat": ["../packages/toolpad-core/src/dateFormat.tsx"],
"@mui/toolpad-utils/*": [
"../packages/toolpad-utils/src/*.tsx",
"../packages/toolpad-utils/src/*.ts"
Expand Down

0 comments on commit 01e3cc6

Please sign in to comment.