diff --git a/packages/core/src/util/defaultDateFormat.ts b/packages/core/src/util/defaultDateFormat.ts new file mode 100644 index 0000000000..c4bd208931 --- /dev/null +++ b/packages/core/src/util/defaultDateFormat.ts @@ -0,0 +1,28 @@ +/* + The MIT License + + Copyright (c) 2023-2023 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +export const defaultDateFormat = 'YYYY-MM-DD'; +export const defaultTimeFormat = 'HH:mm:ss'; +export const defaultDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.sssZ'; diff --git a/packages/core/src/util/index.ts b/packages/core/src/util/index.ts index 489ac18f1e..9dec4bb681 100644 --- a/packages/core/src/util/index.ts +++ b/packages/core/src/util/index.ts @@ -38,3 +38,4 @@ export * from './type'; export * from './uischema'; export * from './util'; export * from './validator'; +export * from './defaultDateFormat'; diff --git a/packages/examples/src/examples/dates.ts b/packages/examples/src/examples/dates.ts index 6bebcfdb20..02816c8b3c 100644 --- a/packages/examples/src/examples/dates.ts +++ b/packages/examples/src/examples/dates.ts @@ -129,12 +129,12 @@ export const uischema = { export const data = { schemaBased: { date: new Date().toISOString().substr(0, 10), - time: '13:37', + time: '13:37:00', datetime: new Date().toISOString(), }, uiSchemaBased: { date: new Date().toISOString().substr(0, 10), - time: '13:37', + time: '13:37:00', datetime: '1999/12/11 10:05 am', }, }; diff --git a/packages/material-renderers/package.json b/packages/material-renderers/package.json index b1f070c29b..38910c0655 100644 --- a/packages/material-renderers/package.json +++ b/packages/material-renderers/package.json @@ -59,7 +59,8 @@ }, "testEnvironment": "jsdom", "testMatch": [ - "**/test/**/*.test.tsx" + "**/test/**/*.test.tsx", + "**/test/**.test.ts" ], "testPathIgnorePatterns": [ "/node_modules/", diff --git a/packages/material-renderers/src/controls/MaterialDateControl.tsx b/packages/material-renderers/src/controls/MaterialDateControl.tsx index a551f0e6a6..645eeed905 100644 --- a/packages/material-renderers/src/controls/MaterialDateControl.tsx +++ b/packages/material-renderers/src/controls/MaterialDateControl.tsx @@ -23,9 +23,10 @@ THE SOFTWARE. */ import merge from 'lodash/merge'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { ControlProps, + defaultDateFormat, isDateControl, isDescriptionHidden, RankedTester, @@ -35,7 +36,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react'; import { FormHelperText, Hidden } from '@mui/material'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnBlurHandler, + createOnChangeHandler, + getData, + useFocus, +} from '../util'; export const MaterialDateControl = (props: ControlProps) => { const [focused, onFocus, onBlur] = useFocus(); @@ -62,8 +68,10 @@ export const MaterialDateControl = (props: ControlProps) => { appliedUiSchemaOptions.showUnfocusedDescription ); + const [key, setKey] = useState(0); + const format = appliedUiSchemaOptions.dateFormat ?? 'YYYY-MM-DD'; - const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? 'YYYY-MM-DD'; + const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? defaultDateFormat; const views = appliedUiSchemaOptions.views ?? ['year', 'day']; @@ -73,20 +81,36 @@ export const MaterialDateControl = (props: ControlProps) => { ? errors : null; const secondFormHelperText = showDescription && !isValid ? errors : null; + + const updateChild = useCallback(() => setKey((key) => key + 1), []); + const onChange = useMemo( () => createOnChangeHandler(path, handleChange, saveFormat), [path, handleChange, saveFormat] ); + const onBlurHandler = useMemo( + () => + createOnBlurHandler( + path, + handleChange, + format, + saveFormat, + updateChild, + onBlur + ), + [path, handleChange, format, saveFormat, updateChild] + ); const value = getData(data, saveFormat); return ( { }, InputLabelProps: data ? { shrink: true } : undefined, onFocus: onFocus, - onBlur: onBlur, + onBlur: onBlurHandler, }, }} /> diff --git a/packages/material-renderers/src/controls/MaterialDateTimeControl.tsx b/packages/material-renderers/src/controls/MaterialDateTimeControl.tsx index 09124c6c03..81366a47da 100644 --- a/packages/material-renderers/src/controls/MaterialDateTimeControl.tsx +++ b/packages/material-renderers/src/controls/MaterialDateTimeControl.tsx @@ -22,10 +22,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import merge from 'lodash/merge'; import { ControlProps, + defaultDateTimeFormat, isDateTimeControl, isDescriptionHidden, RankedTester, @@ -35,7 +36,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react'; import { FormHelperText, Hidden } from '@mui/material'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnBlurHandler, + createOnChangeHandler, + getData, + useFocus, +} from '../util'; export const MaterialDateTimeControl = (props: ControlProps) => { const [focused, onFocus, onBlur] = useFocus(); @@ -64,7 +70,10 @@ export const MaterialDateTimeControl = (props: ControlProps) => { ); const format = appliedUiSchemaOptions.dateTimeFormat ?? 'YYYY-MM-DD HH:mm'; - const saveFormat = appliedUiSchemaOptions.dateTimeSaveFormat ?? undefined; + const saveFormat = + appliedUiSchemaOptions.dateTimeSaveFormat ?? defaultDateTimeFormat; + + const [key, setKey] = useState(0); const views = appliedUiSchemaOptions.views ?? [ 'year', @@ -80,20 +89,35 @@ export const MaterialDateTimeControl = (props: ControlProps) => { : null; const secondFormHelperText = showDescription && !isValid ? errors : null; + const updateChild = useCallback(() => setKey((key) => key + 1), []); + const onChange = useMemo( () => createOnChangeHandler(path, handleChange, saveFormat), [path, handleChange, saveFormat] ); + const onBlurHandler = useMemo( + () => + createOnBlurHandler( + path, + handleChange, + format, + saveFormat, + updateChild, + onBlur + ), + [path, handleChange, format, saveFormat, updateChild] + ); const value = getData(data, saveFormat); return ( { }, InputLabelProps: data ? { shrink: true } : undefined, onFocus: onFocus, - onBlur: onBlur, + onBlur: onBlurHandler, }, }} /> diff --git a/packages/material-renderers/src/controls/MaterialTimeControl.tsx b/packages/material-renderers/src/controls/MaterialTimeControl.tsx index cf898845f4..2592737848 100644 --- a/packages/material-renderers/src/controls/MaterialTimeControl.tsx +++ b/packages/material-renderers/src/controls/MaterialTimeControl.tsx @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import merge from 'lodash/merge'; import { ControlProps, @@ -30,12 +30,18 @@ import { isDescriptionHidden, RankedTester, rankWith, + defaultTimeFormat, } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; import { FormHelperText, Hidden } from '@mui/material'; import { TimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnBlurHandler, + createOnChangeHandler, + getData, + useFocus, +} from '../util'; export const MaterialTimeControl = (props: ControlProps) => { const [focused, onFocus, onBlur] = useFocus(); @@ -56,6 +62,8 @@ export const MaterialTimeControl = (props: ControlProps) => { const appliedUiSchemaOptions = merge({}, config, uischema.options); const isValid = errors.length === 0; + const [key, setKey] = useState(0); + const showDescription = !isDescriptionHidden( visible, description, @@ -64,7 +72,7 @@ export const MaterialTimeControl = (props: ControlProps) => { ); const format = appliedUiSchemaOptions.timeFormat ?? 'HH:mm'; - const saveFormat = appliedUiSchemaOptions.timeSaveFormat ?? 'HH:mm:ss'; + const saveFormat = appliedUiSchemaOptions.timeSaveFormat ?? defaultTimeFormat; const views = appliedUiSchemaOptions.views ?? ['hours', 'minutes']; @@ -75,19 +83,35 @@ export const MaterialTimeControl = (props: ControlProps) => { : null; const secondFormHelperText = showDescription && !isValid ? errors : null; + const updateChild = useCallback(() => setKey((key) => key + 1), []); + const onChange = useMemo( () => createOnChangeHandler(path, handleChange, saveFormat), [path, handleChange, saveFormat] ); + const onBlurHandler = useMemo( + () => + createOnBlurHandler( + path, + handleChange, + format, + saveFormat, + updateChild, + onBlur + ), + [path, handleChange, format, saveFormat, updateChild] + ); const value = getData(data, saveFormat); + return ( { }, InputLabelProps: data ? { shrink: true } : undefined, onFocus: onFocus, - onBlur: onBlur, + onBlur: onBlurHandler, }, }} /> diff --git a/packages/material-renderers/src/util/datejs.tsx b/packages/material-renderers/src/util/datejs.tsx index d3f019cc16..e9e7d56871 100644 --- a/packages/material-renderers/src/util/datejs.tsx +++ b/packages/material-renderers/src/util/datejs.tsx @@ -8,17 +8,56 @@ export const createOnChangeHandler = ( path: string, handleChange: (path: string, value: any) => void, - saveFormat: string | undefined + saveFormat: string ) => - (time: dayjs.Dayjs) => { - if (!time) { + (value: dayjs.Dayjs) => { + if (!value) { handleChange(path, undefined); - return; + } else if (value.toString() !== 'Invalid Date') { + const formatedDate = formatDate(value, saveFormat); + handleChange(path, formatedDate); } - const result = dayjs(time).format(saveFormat); - handleChange(path, result); }; +export const createOnBlurHandler = + ( + path: string, + handleChange: (path: string, value: any) => void, + format: string, + saveFormat: string, + rerenderChild: () => void, + onBlur: () => void + ) => + (e: React.FocusEvent) => { + const date = dayjs(e.target.value, format); + const formatedDate = formatDate(date, saveFormat); + if (formatedDate.toString() === 'Invalid Date') { + handleChange(path, undefined); + rerenderChild(); + } else { + handleChange(path, formatedDate); + } + onBlur(); + }; + +export const formatDate = (date: dayjs.Dayjs, saveFormat: string) => { + let formatedDate = date.format(saveFormat); + // Workaround to address a bug in Dayjs, neglecting leading 0 (https://github.com/iamkun/dayjs/issues/1849) + const indexOfYear = saveFormat.indexOf('YYYY'); + if (date.year() < 1000 && indexOfYear !== -1) { + const stringUpToYear = formatedDate.slice(0, indexOfYear); + const stringFromYear = formatedDate.slice(indexOfYear); + if (date.year() >= 100) { + formatedDate = [stringUpToYear, 0, stringFromYear].join(''); + } else if (date.year() >= 10) { + formatedDate = [stringUpToYear, 0, 0, stringFromYear].join(''); + } else if (date.year() >= 1) { + formatedDate = [stringUpToYear, 0, 0, 0, stringFromYear].join(''); + } + } + return formatedDate; +}; + export const getData = ( data: any, saveFormat: string | undefined diff --git a/packages/material-renderers/test/datejs.test.ts b/packages/material-renderers/test/datejs.test.ts new file mode 100644 index 0000000000..24c8ea3d53 --- /dev/null +++ b/packages/material-renderers/test/datejs.test.ts @@ -0,0 +1,52 @@ +/* + The MIT License + + Copyright (c) 2024-2024 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import dayjs from 'dayjs'; +import { formatDate } from '../src/util/datejs'; + +describe('Date Util tester', () => { + test('format default', () => { + const date = dayjs('2024-01-01'); + const actual = formatDate(date, 'YYYY-MM-DD'); + expect(actual).toBe('2024-01-01'); + }); + + test('format year < 1000', () => { + const date = dayjs('999-01-01'); + const actual = formatDate(date, 'YYYY-MM-DD'); + expect(actual).toBe('0999-01-01'); + }); + + test('format 100 < year < 1000', () => { + const date = dayjs(new Date('0099-01-01')); + const actual = formatDate(date, 'YYYY-MM-DD'); + expect(actual).toBe('0099-01-01'); + }); + + test('format 10 < year < 100', () => { + const date = dayjs(new Date('0019-01-01')); + const actual = formatDate(date, 'YYYY-MM-DD'); + expect(actual).toBe('0019-01-01'); + }); +}); diff --git a/packages/material-renderers/test/renderers/MaterialDateControl.test.tsx b/packages/material-renderers/test/renderers/MaterialDateControl.test.tsx index 5c0f51d299..7f89dd7618 100644 --- a/packages/material-renderers/test/renderers/MaterialDateControl.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialDateControl.test.tsx @@ -225,7 +225,7 @@ describe('Material date control', () => { ); const input = wrapper.find('input').first(); (input.getDOMNode() as HTMLInputElement).value = '1961-04-12'; - input.simulate('change', input); + input.simulate('blur', input); expect(onChangeData.data.foo).toBe('1961-04-12'); }); @@ -421,7 +421,7 @@ describe('Material date control', () => { expect(input.props().value).toBe('1980/06'); (input.getDOMNode() as HTMLInputElement).value = '1961/04'; - input.simulate('change', input); + input.simulate('blur', input); expect(onChangeData.data.foo).toBe('04---1961'); }); }); diff --git a/packages/material-renderers/test/renderers/MaterialDateTimeControl.test.tsx b/packages/material-renderers/test/renderers/MaterialDateTimeControl.test.tsx index 3e61b0d1f4..2c7a3ab5c5 100644 --- a/packages/material-renderers/test/renderers/MaterialDateTimeControl.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialDateTimeControl.test.tsx @@ -24,7 +24,11 @@ */ import './MatchMediaMock'; import React from 'react'; -import { ControlElement, NOT_APPLICABLE } from '@jsonforms/core'; +import { + ControlElement, + defaultDateTimeFormat, + NOT_APPLICABLE, +} from '@jsonforms/core'; import MaterialDateTimeControl, { materialDateTimeControlTester, } from '../../src/controls/MaterialDateTimeControl'; @@ -38,7 +42,7 @@ import { initCore, TestEmitter } from './util'; Enzyme.configure({ adapter: new Adapter() }); -const data = { foo: dayjs('1980-04-04 13:37').format() }; +const data = { foo: dayjs('1980-04-04 13:37').format(defaultDateTimeFormat) }; const schema = { type: 'object', properties: { @@ -228,8 +232,10 @@ describe('Material date time control', () => { ); const input = wrapper.find('input').first(); (input.getDOMNode() as HTMLInputElement).value = '1961-12-12 20:15'; - input.simulate('change', input); - expect(onChangeData.data.foo).toBe(dayjs('1961-12-12 20:15').format()); + input.simulate('blur', input); + expect(onChangeData.data.foo).toBe( + dayjs('1961-12-12 20:15').format(defaultDateTimeFormat) + ); }); it('should update via action', () => { @@ -241,7 +247,10 @@ describe('Material date time control', () => { ); - core.data = { ...core.data, foo: dayjs('1961-12-04 20:15').format() }; + core.data = { + ...core.data, + foo: dayjs('1961-12-04 20:15').format(defaultDateTimeFormat), + }; wrapper.setProps({ initState: { renderers: materialRenderers, core } }); wrapper.update(); const input = wrapper.find('input').first(); @@ -427,7 +436,7 @@ describe('Material date time control', () => { expect(input.props().value).toBe('23-04-80 01:37:pm'); (input.getDOMNode() as HTMLInputElement).value = '10-12-05 11:22:am'; - input.simulate('change', input); + input.simulate('blur', input); expect(onChangeData.data.foo).toBe('2005/12/10 11:22 am'); }); }); diff --git a/packages/material-renderers/test/renderers/MaterialTimeControl.test.tsx b/packages/material-renderers/test/renderers/MaterialTimeControl.test.tsx index b3b6b358e9..d3430016db 100644 --- a/packages/material-renderers/test/renderers/MaterialTimeControl.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialTimeControl.test.tsx @@ -225,8 +225,8 @@ describe('Material time control', () => { ); const input = wrapper.find('input').first(); (input.getDOMNode() as HTMLInputElement).value = '08:40'; - input.simulate('change', input); - expect(onChangeData.data.foo).toBe('08:40:05'); + input.simulate('blur', input); + expect(onChangeData.data.foo).toBe('08:40:00'); }); it('should update via action', () => { @@ -421,7 +421,7 @@ describe('Material time control', () => { expect(input.props().value).toBe('02-13'); (input.getDOMNode() as HTMLInputElement).value = '12-01'; - input.simulate('change', input); + input.simulate('blur', input); expect(onChangeData.data.foo).toBe('1//12 am'); }); });