From 1c03fbb853b5885547835f50fc9a0932f63c363b Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 16 Jun 2022 22:50:09 +0800 Subject: [PATCH] fix(client): improve datepicker component, date with time zone, gmt support --- .../collection-manager/interfaces/datetime.ts | 8 + packages/core/client/src/locale/en_US.ts | 1 + packages/core/client/src/locale/zh_CN.ts | 3 +- .../antd/date-picker/ReadPretty.tsx | 16 +- .../antd/date-picker/__tests__/util.test.ts | 79 ++++++++ .../antd/date-picker/demos/demo1.tsx | 1 + .../antd/date-picker/demos/demo3.tsx | 7 +- .../antd/date-picker/index.md | 4 +- .../schema-component/antd/date-picker/util.ts | 169 ++++++++++-------- 9 files changed, 206 insertions(+), 82 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts diff --git a/packages/core/client/src/collection-manager/interfaces/datetime.ts b/packages/core/client/src/collection-manager/interfaces/datetime.ts index aeefb0379..ddf582b61 100644 --- a/packages/core/client/src/collection-manager/interfaces/datetime.ts +++ b/packages/core/client/src/collection-manager/interfaces/datetime.ts @@ -23,6 +23,14 @@ export const datetime: IField = { properties: { ...defaultProps, ...dateTimeProps, + 'uiSchema.x-component-props.gmt': { + type: 'boolean', + title: '{{t("GMT")}}', + 'x-component': 'Checkbox', + 'x-content': '{{t("Use the same time zone (GMT) for all users")}}', + 'x-decorator': 'FormItem', + default: false, + }, }, filterable: { operators: operators.datetime, diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 31fc60e05..3bc52c473 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -540,4 +540,5 @@ export default { "Field value changes": "Field value changes", "One to one (has one)": "One to one (has one)", "One to one (belongs to)": "One to one (belongs to)", + "Use the same time zone (GMT) for all users": "Use the same time zone (GMT) for all users", } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index beb1f089c..7260d66cf 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -595,5 +595,6 @@ export default { "Select Field": "选择字段", "Field value changes": "变更记录", "One to one (has one)": "一对一(has one)", - "One to one (belongs to)": "一对一(belongs to)" + "One to one (belongs to)": "一对一(belongs to)", + "Use the same time zone (GMT) for all users": "所有用户使用同一时区 (格林尼治标准时间)", } diff --git a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx index 9b4e0f52b..158c19a6e 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx @@ -1,4 +1,4 @@ -import { formatMomentValue, usePrefixCls } from '@formily/antd/lib/__builtins__'; +import { usePrefixCls } from '@formily/antd/lib/__builtins__'; import { isArr } from '@formily/shared'; import type { DatePickerProps as AntdDatePickerProps, @@ -7,7 +7,7 @@ import type { import cls from 'classnames'; import moment from 'moment'; import React from 'react'; -import { getDefaultFormat } from './util'; +import { getDefaultFormat, str2moment } from './util'; type Composed = { DatePicker: React.FC; @@ -22,8 +22,9 @@ ReadPretty.DatePicker = (props: any) => { } const prefixCls = usePrefixCls('description-date-picker', props); const getLabels = () => { - const d = moment(props.value); - const labels = formatMomentValue(d.isValid() ? d : null, getDefaultFormat(props), props.placeholder); + const format = getDefaultFormat(props) as string; + const m = str2moment(props.value, props); + const labels = moment.isMoment(m) ? m.format(format) : ''; return isArr(labels) ? labels.join('~') : labels; }; return
{getLabels()}
; @@ -31,8 +32,13 @@ ReadPretty.DatePicker = (props: any) => { ReadPretty.DateRangePicker = (props: any) => { const prefixCls = usePrefixCls('description-text', props); + const format = getDefaultFormat(props); const getLabels = () => { - const labels = formatMomentValue(props.value, props.format, props.placeholder); + const m = str2moment(props.value, props); + if (!m) { + return ''; + } + const labels = m.map(m => m.format(format)); return isArr(labels) ? labels.join('~') : labels; }; return ( diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts new file mode 100644 index 000000000..8a058a19c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -0,0 +1,79 @@ +import moment from 'moment'; +import { moment2str, str2moment } from '../util'; + +describe('str2moment', () => { + describe('string value', () => { + test('gmt date', async () => { + const m = str2moment('2022-06-21T00:00:00.000Z', { gmt: true }); + expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-21 00:00:00'); + }); + + test('local date', async () => { + const m = str2moment('2022-06-21T00:00:00.000Z'); + expect(m.toISOString()).toBe('2022-06-21T00:00:00.000Z'); + }); + + test('value is null', async () => { + const m = str2moment(null); + expect(m).toBeNull(); + }); + + test('picker is month', async () => { + const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month' }); + expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-01 00:00:00'); + }); + }); + + describe('array value', () => { + test('gmt date', async () => { + const arr = str2moment(['2022-06-21T00:00:00.000Z', '2022-06-21T00:00:00.000Z'], { gmt: true }); + for (const m of arr) { + expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-21 00:00:00'); + } + }); + + test('local date', async () => { + const arr = str2moment(['2022-06-21T00:00:00.000Z', '2022-06-21T00:00:00.000Z']); + for (const m of arr) { + expect(m.toISOString()).toBe('2022-06-21T00:00:00.000Z'); + } + }); + }); +}); + +describe('moment2str', () => { + test('gmt date', () => { + const m = moment('2022-06-21 10:10:00'); + const str = moment2str(m, { showTime: true, gmt: true }); + expect(str).toBe('2022-06-21T10:10:00.000Z'); + }); + + test('gmt date only', () => { + const m = moment('2022-06-21 10:10:00'); + const str = moment2str(m); + expect(str).toBe('2022-06-21T00:00:00.000Z'); + }); + + test('with time', () => { + const m = moment('2022-06-21 10:10:00'); + const str = moment2str(m, { showTime: true }); + expect(str).toBe(m.toISOString()); + }); + + test('picker is year', () => { + const m = moment('2022-06-21 10:10:00'); + const str = moment2str(m, { picker: 'year' }); + expect(str).toBe('2022-01-01T00:00:00.000Z'); + }); + + test('picker is month', () => { + const m = moment('2022-06-21 10:10:00'); + const str = moment2str(m, { picker: 'month' }); + expect(str).toBe('2022-06-01T00:00:00.000Z'); + }); + + test('value is null', async () => { + const m = moment2str(null); + expect(m).toBeNull(); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/date-picker/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/date-picker/demos/demo1.tsx index d8dd7a6d3..df0203511 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/demos/demo1.tsx @@ -17,6 +17,7 @@ const schema = { dateFormat: 'YYYY/MM/DD', showTime: true, }, + // default: '2022-11-22', 'x-reactions': { target: '*(read1,read2)', fulfill: { diff --git a/packages/core/client/src/schema-component/antd/date-picker/demos/demo3.tsx b/packages/core/client/src/schema-component/antd/date-picker/demos/demo3.tsx index 48d7fe76c..d38cea57f 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/demos/demo3.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/demos/demo3.tsx @@ -1,5 +1,5 @@ /** - * title: DatePicker (with timezone) + * title: DatePicker (GMT) */ import { FormItem } from '@formily/antd'; import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; @@ -16,7 +16,7 @@ const schema = { 'x-component-props': { dateFormat: 'YYYY/MM/DD', showTime: true, - withTz: true, + gmt: true, }, default: '2022-06-04T15:00:00.000Z', 'x-reactions': { @@ -36,7 +36,8 @@ const schema = { 'x-component': 'DatePicker', 'x-component-props': { dateFormat: 'YYYY/MM/DD', - // showTime: true, + showTime: true, + gmt: true, }, }, read2: { diff --git a/packages/core/client/src/schema-component/antd/date-picker/index.md b/packages/core/client/src/schema-component/antd/date-picker/index.md index b62b8ced8..6af0a6fff 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/index.md +++ b/packages/core/client/src/schema-component/antd/date-picker/index.md @@ -9,11 +9,11 @@ group: ## Examples -### DatePicker +### Basic -### DatePicker (with timezone) +### DatePicker (GMT) diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 17f468bce..275e7571e 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -1,7 +1,102 @@ -import { isArr, isEmpty, isFn } from '@formily/shared'; import type { DatePickerProps } from 'antd/lib/date-picker'; import moment from 'moment'; +const toGmt = (value: moment.Moment | moment.Moment[]) => { + if (!value) { + return value; + } + if (Array.isArray(value)) { + return value.map((val) => `${val.format('YYYY-MM-DD')}T${val.format('HH:mm:ss.SSS')}Z`); + } + if (moment.isMoment(value)) { + return `${value.format('YYYY-MM-DD')}T${value.format('HH:mm:ss.SSS')}Z`; + } +}; + +const toStringByPicker = (value, picker) => { + if (picker === 'year') { + return value.format('YYYY') + '-01-01T00:00:00.000Z'; + } + if (picker === 'month') { + return value.format('YYYY-MM') + '-01T00:00:00.000Z'; + } + if (picker === 'quarter') { + return value.format('YYYY-MM') + '-01T00:00:00.000Z'; + } + if (picker === 'week') { + return value.format('YYYY-MM-DD') + 'T00:00:00.000Z'; + } + return value.format('YYYY-MM-DD') + 'T00:00:00.000Z'; +}; + +const toGmtByPicker = (value: moment.Moment | moment.Moment[], picker?: any) => { + if (!value) { + return value; + } + if (Array.isArray(value)) { + return value.map((val) => toStringByPicker(val, picker)); + } + if (moment.isMoment(value)) { + return toStringByPicker(value, picker); + } +}; + +const toLocal = (value: moment.Moment | moment.Moment[]) => { + if (!value) { + return value; + } + if (Array.isArray(value)) { + return value.map((val) => val.toISOString()); + } + if (moment.isMoment(value)) { + return value.toISOString(); + } +}; + +export interface Str2momentOptions { + gmt?: boolean; + picker?: 'year' | 'month' | 'week' | 'quarter'; +} + +const toMoment = (val: any, options?: Str2momentOptions) => { + if (moment.isMoment(val)) { + return val; + } + const { gmt, picker } = options; + if (gmt || picker) { + val = val.replace('T', ' ').replace('Z', ''); + return moment(val); + } + return moment(val); +}; + +export const str2moment = (value?: string | string[], options: Str2momentOptions = {}): any => { + return Array.isArray(value) + ? value.map((val) => { + return toMoment(val, options); + }) + : value + ? toMoment(value, options) + : value; +}; + +export interface Moment2strOptions { + showTime?: boolean; + gmt?: boolean; + picker?: 'year' | 'month' | 'week' | 'quarter'; +} + +export const moment2str = (value?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => { + const { showTime, gmt, picker } = options; + if (!value) { + return value; + } + if (showTime) { + return gmt ? toGmt(value) : toLocal(value); + } + return toGmtByPicker(value, picker); +}; + export const getDefaultFormat = (props: DatePickerProps & { dateFormat: string; timeFormat: string }) => { if (props.format) { return props.format; @@ -24,70 +119,6 @@ export const getDefaultFormat = (props: DatePickerProps & { dateFormat: string; return props['showTime'] ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; }; -const toMoment = (val, format) => { - if (moment.isMoment(val)) { - return val; - } - if (typeof val === 'string' && val.includes('T')) { - return moment(val); - } - return moment(val, format); -}; - -export const momentable = (value: any, format?: string) => { - return Array.isArray(value) - ? value.map((val) => { - return toMoment(val, format); - }) - : value - ? toMoment(value, format) - : value; -}; - -export const formatMomentValue = (value: any, format: any, placeholder?: string): string | string[] => { - const formatDate = (date: any, format: any, i = 0) => { - if (!date) return placeholder; - if (isArr(format)) { - const _format = format[i]; - if (isFn(_format)) { - return _format(date); - } - if (isEmpty(_format)) { - return date; - } - return moment(date).format(_format); - } else { - if (isFn(format)) { - return format(date); - } - if (isEmpty(format)) { - return date; - } - return moment(date).format(format); - } - }; - if (isArr(value)) { - return value.map((val, index) => { - return formatDate(val, format, index); - }); - } else { - return value ? formatDate(value, format) : value || placeholder; - } -}; - - -const momentToString = (value: moment.Moment | moment.Moment[]) => { - if (!value) { - return value; - } - if (Array.isArray(value)) { - return value.map(val => val.toISOString()); - } - if (moment.isMoment(value)) { - return value.toISOString(); - } -} - export const mapDateFormat = function () { return (props: any) => { const format = getDefaultFormat(props) as any; @@ -95,14 +126,10 @@ export const mapDateFormat = function () { return { ...props, format: format, - value: momentable(props.value, format === 'YYYY-wo' ? 'YYYY-w' : format), + value: str2moment(props.value, props), onChange: (value: moment.Moment | moment.Moment[]) => { if (onChange) { - if (props.withTz) { - onChange(momentToString(value)); - } else { - onChange(formatMomentValue(value, format)); - } + onChange(moment2str(value, props)); } }, };