fix(client): improve datepicker component, date with time zone, gmt support
This commit is contained in:
		
							parent
							
								
									7a9bab9bab
								
							
						
					
					
						commit
						1c03fbb853
					
				| @ -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, | ||||
|  | ||||
| @ -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", | ||||
| } | ||||
|  | ||||
| @ -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": "所有用户使用同一时区 (格林尼治标准时间)", | ||||
| } | ||||
|  | ||||
| @ -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<AntdDatePickerProps>; | ||||
| @ -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 <div className={cls(prefixCls, props.className)}>{getLabels()}</div>; | ||||
| @ -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 ( | ||||
|  | ||||
| @ -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(); | ||||
|   }); | ||||
| }); | ||||
| @ -17,6 +17,7 @@ const schema = { | ||||
|         dateFormat: 'YYYY/MM/DD', | ||||
|         showTime: true, | ||||
|       }, | ||||
|       // default: '2022-11-22',
 | ||||
|       'x-reactions': { | ||||
|         target: '*(read1,read2)', | ||||
|         fulfill: { | ||||
|  | ||||
| @ -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: { | ||||
|  | ||||
| @ -9,11 +9,11 @@ group: | ||||
| 
 | ||||
| ## Examples | ||||
| 
 | ||||
| ### DatePicker | ||||
| ### Basic | ||||
| 
 | ||||
| <code src="./demos/demo1.tsx" /> | ||||
| 
 | ||||
| ### DatePicker (with timezone) | ||||
| ### DatePicker (GMT) | ||||
| 
 | ||||
| <code src="./demos/demo3.tsx" /> | ||||
| 
 | ||||
|  | ||||
| @ -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)); | ||||
|         } | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user