From accbd16874bacb13418a60ff382f860c28ba8dcf Mon Sep 17 00:00:00 2001 From: Shubham Biswas <46351104+Shubhcoder@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:52:57 +0530 Subject: [PATCH] Feat: Implements #297 request (#4004) * [Feat] Add Date re-order option * Added tests for date re-order * Updated docs and example * Update changelog * Added documentation for getDateElementProps function * Fix: test failing due to year change * Removed redundant parameters info --- CHANGELOG.md | 22 ++++ .../antd/src/widgets/AltDateWidget/index.tsx | 36 ++---- .../src/AltDateWidget/AltDateWidget.tsx | 29 ++--- .../src/components/widgets/AltDateWidget.tsx | 34 ++---- packages/core/test/StringField.test.jsx | 96 ++++++++++++++++ .../docs/api-reference/utility-functions.md | 15 +++ packages/docs/docs/usage/widgets.md | 3 + packages/playground/src/samples/date.ts | 2 + packages/utils/src/getDateElementProps.ts | 56 ++++++++++ packages/utils/src/index.ts | 3 + .../utils/test/getDateElementProps.test.ts | 104 ++++++++++++++++++ 11 files changed, 326 insertions(+), 74 deletions(-) create mode 100644 packages/utils/src/getDateElementProps.ts create mode 100644 packages/utils/test/getDateElementProps.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfaa7a1ff..c2f8753040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,28 @@ it according to semantic versioning. For example, if your PR adds a breaking cha should change the heading of the (upcoming) version to include a major version bump. --> +# 5.15.3 + +## @rjsf/utils + +- Added `getDateElementProps()` to refactor duplicate function in `core`, `antd` & `chakra-ui` AltDateWidget's source code. The same function, implements the feature requested in [#297](https://github.com/rjsf-team/react-jsonschema-form/issues/297) + +## @rjsf/core + +- Removed `dateElementProps` function implementation, and replaced it with `getDateElementProps` from `@rjsf/utils`. + +## @rjsf/antd + +- Removed `dateElementProps` function implementation, and replaced it with `getDateElementProps` from `@rjsf/utils`. + +## @rjsf/chakra-ui + +- Removed `dateElementProps` function implementation, and replaced it with `getDateElementProps` from `@rjsf/utils`. + +## Dev / docs / playground + +- Updated docs and playground with the implementation guide of newly added date re-order feature. + # 5.15.2 ## @rjsf/core diff --git a/packages/antd/src/widgets/AltDateWidget/index.tsx b/packages/antd/src/widgets/AltDateWidget/index.tsx index bcf457a889..67adc4e3c6 100644 --- a/packages/antd/src/widgets/AltDateWidget/index.tsx +++ b/packages/antd/src/widgets/AltDateWidget/index.tsx @@ -4,6 +4,7 @@ import Col from 'antd/lib/col'; import Row from 'antd/lib/row'; import { ariaDescribedByIds, + getDateElementProps, pad, parseDateString, toDateString, @@ -14,6 +15,7 @@ import { StrictRJSFSchema, TranslatableString, WidgetProps, + DateElementFormat, } from '@rjsf/utils'; type DateElementProps = Pick< @@ -37,31 +39,6 @@ const readyForChange = (state: DateObject) => { return Object.values(state).every((value) => value !== -1); }; -function dateElementProps( - state: DateObject, - time: boolean, - yearsRange: [number, number] = [1900, new Date().getFullYear() + 2] -) { - const { year, month, day, hour, minute, second } = state; - const data = [ - { - type: 'year', - range: yearsRange, - value: year, - }, - { type: 'month', range: [1, 12], value: month }, - { type: 'day', range: [1, 31], value: day }, - ] as { type: string; range: [number, number]; value: number }[]; - if (time) { - data.push( - { type: 'hour', range: [0, 23], value: hour || -1 }, - { type: 'minute', range: [0, 59], value: minute || -1 }, - { type: 'second', range: [0, 59], value: second || -1 } - ); - } - return data; -} - export default function AltDateWidget< T = any, S extends StrictRJSFSchema = RJSFSchema, @@ -146,7 +123,12 @@ export default function AltDateWidget< return ( - {dateElementProps(state, showTime, options.yearsRange as [number, number] | undefined).map((elemProps, i) => { + {getDateElementProps( + state, + showTime, + options.yearsRange as [number, number] | undefined, + options.format as DateElementFormat | undefined + ).map((elemProps, i) => { const elemId = id + '_' + elemProps.type; return ( @@ -163,7 +145,7 @@ export default function AltDateWidget< select: handleChange, // NOTE: antd components accept -1 rather than issue a warning // like material-ui, so we need to convert -1 to undefined here. - value: elemProps.value < 0 ? undefined : elemProps.value, + value: elemProps.value || -1 < 0 ? undefined : elemProps.value, })} ); diff --git a/packages/chakra-ui/src/AltDateWidget/AltDateWidget.tsx b/packages/chakra-ui/src/AltDateWidget/AltDateWidget.tsx index 7e9569d4c5..c6f258936c 100644 --- a/packages/chakra-ui/src/AltDateWidget/AltDateWidget.tsx +++ b/packages/chakra-ui/src/AltDateWidget/AltDateWidget.tsx @@ -1,8 +1,10 @@ import { MouseEvent, useEffect, useState } from 'react'; import { ariaDescribedByIds, + DateElementFormat, DateObject, FormContextType, + getDateElementProps, pad, parseDateString, RJSFSchema, @@ -91,30 +93,15 @@ function AltDateWidget { - const { year, month, day, hour, minute, second } = state; - - const data: { type: string; range: any; value?: number }[] = [ - { type: 'year', range: options.yearsRange, value: year }, - { type: 'month', range: [1, 12], value: month }, - { type: 'day', range: [1, 31], value: day }, - ]; - - if (showTime) { - data.push( - { type: 'hour', range: [0, 23], value: hour }, - { type: 'minute', range: [0, 59], value: minute }, - { type: 'second', range: [0, 59], value: second } - ); - } - - return data; - }; - return ( - {dateElementProps().map((elemProps: any, i) => { + {getDateElementProps( + state, + showTime, + options.yearsRange as [number, number] | undefined, + options.format as DateElementFormat | undefined + ).map((elemProps: any, i) => { const elemId = id + '_' + elemProps.type; return ( diff --git a/packages/core/src/components/widgets/AltDateWidget.tsx b/packages/core/src/components/widgets/AltDateWidget.tsx index b344dcc554..899a368c54 100644 --- a/packages/core/src/components/widgets/AltDateWidget.tsx +++ b/packages/core/src/components/widgets/AltDateWidget.tsx @@ -5,11 +5,13 @@ import { toDateString, pad, DateObject, + type DateElementFormat, FormContextType, RJSFSchema, StrictRJSFSchema, TranslatableString, WidgetProps, + getDateElementProps, } from '@rjsf/utils'; function rangeOptions(start: number, stop: number) { @@ -24,31 +26,6 @@ function readyForChange(state: DateObject) { return Object.values(state).every((value) => value !== -1); } -function dateElementProps( - state: DateObject, - time: boolean, - yearsRange: [number, number] = [1900, new Date().getFullYear() + 2] -) { - const { year, month, day, hour, minute, second } = state; - const data = [ - { - type: 'year', - range: yearsRange, - value: year, - }, - { type: 'month', range: [1, 12], value: month }, - { type: 'day', range: [1, 31], value: day }, - ] as { type: string; range: [number, number]; value: number | undefined }[]; - if (time) { - data.push( - { type: 'hour', range: [0, 23], value: hour }, - { type: 'minute', range: [0, 59], value: minute }, - { type: 'second', range: [0, 59], value: second } - ); - } - return data; -} - type DateElementProps = Pick< WidgetProps, 'value' | 'name' | 'disabled' | 'readonly' | 'autofocus' | 'registry' | 'onBlur' | 'onFocus' @@ -161,7 +138,12 @@ function AltDateWidget - {dateElementProps(state, time, options.yearsRange as [number, number] | undefined).map((elemProps, i) => ( + {getDateElementProps( + state, + time, + options.yearsRange as [number, number] | undefined, + options.format as DateElementFormat | undefined + ).map((elemProps, i) => (
  • { expect(node.querySelector('#custom')).to.exist; }); + + describe('AltDateTimeWidget with format option', () => { + const uiSchema = { 'ui:widget': 'alt-datetime', 'ui:options': { format: 'YMD' } }; + + it('should render a date field with YMD format', () => { + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_year', 'root_month', 'root_day', 'root_hour', 'root_minute', 'root_second']); + }); + + it('should render a date field with DMY format', () => { + uiSchema['ui:options']['format'] = 'DMY'; + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_day', 'root_month', 'root_year', 'root_hour', 'root_minute', 'root_second']); + }); + + it('should render a date field with MDY format', () => { + uiSchema['ui:options']['format'] = 'MDY'; + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_month', 'root_day', 'root_year', 'root_hour', 'root_minute', 'root_second']); + }); + }); }); describe('AltDateWidget', () => { @@ -1448,6 +1496,54 @@ describe('StringField', () => { } }); + describe('AltDateWidget with format option', () => { + const uiSchema = { 'ui:widget': 'alt-date', 'ui:options': { format: 'YMD' } }; + + it('should render a date field with YMD format', () => { + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_year', 'root_month', 'root_day']); + }); + + it('should render a date field with MDY format', () => { + uiSchema['ui:options']['format'] = 'MDY'; + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_month', 'root_day', 'root_year']); + }); + + it('should render a date field with DMY format', () => { + uiSchema['ui:options']['format'] = 'DMY'; + const { node } = createFormComponent({ + schema: { + type: 'string', + format: 'date', + }, + uiSchema, + }); + + const ids = [].map.call(node.querySelectorAll('select'), (node) => node.id); + + expect(ids).eql(['root_day', 'root_month', 'root_year']); + }); + }); + describe('Action buttons', () => { it('should render action buttons', () => { const { node } = createFormComponent({ diff --git a/packages/docs/docs/api-reference/utility-functions.md b/packages/docs/docs/api-reference/utility-functions.md index 17b1909859..fdee1aa0f6 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -281,6 +281,21 @@ Returns `undefined` when a valid discriminator is not present. - string | undefined: The `discriminator.propertyName` if it exists in the schema, otherwise `undefined` +### getDateElementProps() + +Given date & time information with optional yearRange & format, returns props for DateElement + +#### Parameters + +- date: DateObject - Object containing date with optional time information +- time: boolean - Determines whether to include time or not +- [yearRange=[1900, new Date().getFullYear() + 2]]: [number, number] - Controls the list of years to be displayed +- [format='YMD']: DateElementFormat - Controls the order in which day, month and year input element will be displayed + +#### Returns + +- Array of props for DateElement + ### getInputProps() Using the `schema`, `defaultType` and `options`, extract out the props for the `` element that make sense. diff --git a/packages/docs/docs/usage/widgets.md b/packages/docs/docs/usage/widgets.md index 7d923ea0f3..811a491ca0 100644 --- a/packages/docs/docs/usage/widgets.md +++ b/packages/docs/docs/usage/widgets.md @@ -91,6 +91,8 @@ Please note that, even though they are standardized, `datetime-local`, `date` an You can customize the list of years displayed in the `year` dropdown by providing a `yearsRange` property to `ui:options` in your uiSchema. It's also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. +You can also, customize the order in which date input fields are displayed by providing `format` property to `ui:options` in your uiSchema, available values are `YMD`(default), `MDY` and `DMY`. + ```tsx import { RJSFSchema, UiSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; @@ -103,6 +105,7 @@ const uiSchema: UiSchema = { 'ui:widget': 'alt-datetime', 'ui:options': { yearsRange: [1980, 2030], + format: 'MDY', hideNowButton: true, hideClearButton: true, }, diff --git a/packages/playground/src/samples/date.ts b/packages/playground/src/samples/date.ts index 7c3caad226..12185d5575 100644 --- a/packages/playground/src/samples/date.ts +++ b/packages/playground/src/samples/date.ts @@ -47,12 +47,14 @@ const date: Sample = { 'ui:widget': 'alt-datetime', 'ui:options': { yearsRange: [1980, 2030], + format: 'YMD', }, }, 'alt-date': { 'ui:widget': 'alt-date', 'ui:options': { yearsRange: [1980, 2030], + format: 'MDY', }, }, }, diff --git a/packages/utils/src/getDateElementProps.ts b/packages/utils/src/getDateElementProps.ts new file mode 100644 index 0000000000..237324a763 --- /dev/null +++ b/packages/utils/src/getDateElementProps.ts @@ -0,0 +1,56 @@ +import { type DateObject } from './types'; + +/** Available options for re-ordering date input element */ +export type DateElementFormat = 'DMY' | 'MDY' | 'YMD'; + +/** Type describing format of DateElement prop */ +type DateElementProp = { + type: string; + range: [number, number]; + value: number | undefined; +}; + +/** Given date & time information with optional yearRange & format, returns props for DateElement + * + * @param date - Object containing date with optional time information + * @param time - Determines whether to include time or not + * @param [yearRange=[1900, new Date().getFullYear() + 2]] - Controls the list of years to be displayed + * @param [format='YMD'] - Controls the order in which day, month and year input element will be displayed + * @returns Array of props for DateElement + */ + +export default function getDateElementProps( + date: DateObject, + time: boolean, + yearRange: [number, number] = [1900, new Date().getFullYear() + 2], + format: DateElementFormat = 'YMD' +) { + const { day, month, year, hour, minute, second } = date; + + const dayObj: DateElementProp = { type: 'day', range: [1, 31], value: day }; + const monthObj: DateElementProp = { type: 'month', range: [1, 12], value: month }; + const yearObj: DateElementProp = { type: 'year', range: yearRange, value: year }; + + const dateElementProp: DateElementProp[] = []; + switch (format) { + case 'MDY': + dateElementProp.push(monthObj, dayObj, yearObj); + break; + case 'DMY': + dateElementProp.push(dayObj, monthObj, yearObj); + break; + case 'YMD': + default: + dateElementProp.push(yearObj, monthObj, dayObj); + } + + if (time) { + dateElementProp.push( + { type: 'hour', range: [0, 23], value: hour }, + { type: 'minute', range: [0, 59], value: minute }, + { type: 'second', range: [0, 59], value: second } + ); + } + + return dateElementProp; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d4c18bf5c5..5f40d22f94 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -13,6 +13,7 @@ import enumOptionsSelectValue from './enumOptionsSelectValue'; import enumOptionsValueForIndex from './enumOptionsValueForIndex'; import ErrorSchemaBuilder from './ErrorSchemaBuilder'; import findSchemaDefinition from './findSchemaDefinition'; +import getDateElementProps, { type DateElementFormat } from './getDateElementProps'; import getDiscriminatorFieldFromSchema from './getDiscriminatorFieldFromSchema'; import getInputProps from './getInputProps'; import getSchemaType from './getSchemaType'; @@ -65,6 +66,7 @@ export { canExpand, createErrorHandler, createSchemaUtils, + DateElementFormat, dataURItoBlob, deepEquals, descriptionId, @@ -78,6 +80,7 @@ export { examplesId, ErrorSchemaBuilder, findSchemaDefinition, + getDateElementProps, getDiscriminatorFieldFromSchema, getInputProps, getOptionMatchingSimpleDiscriminator, diff --git a/packages/utils/test/getDateElementProps.test.ts b/packages/utils/test/getDateElementProps.test.ts new file mode 100644 index 0000000000..c6bbd800d2 --- /dev/null +++ b/packages/utils/test/getDateElementProps.test.ts @@ -0,0 +1,104 @@ +import getDateElementProps from '../src/getDateElementProps'; + +describe('getDateElementProps', () => { + const monthRange = [1, 12]; + const dayRange = [1, 31]; + const defaultYearRange = [1900, new Date().getFullYear() + 2] as [number, number]; + + it('returns date in default(YMD) format when time is false and format is not passed', () => { + const date = { day: 11, month: 12, year: 2023 }; + + const prop = getDateElementProps(date, false); + + expect(prop).toEqual([ + { type: 'year', range: defaultYearRange, value: date.year }, + { type: 'month', range: monthRange, value: date.month }, + { type: 'day', range: dayRange, value: date.day }, + ]); + }); + + it('returns date in YMD format when time is not false', () => { + const date = { day: 11, month: 12, year: 2003 }; + const yearRange = [1950, 2010] as [number, number]; + + const prop = getDateElementProps(date, false, yearRange, 'YMD'); + + expect(prop).toEqual([ + { type: 'year', range: yearRange, value: date.year }, + { type: 'month', range: monthRange, value: date.month }, + { type: 'day', range: dayRange, value: date.day }, + ]); + }); + + it("returns date in 'MDY' format when time is false and format is not passed", () => { + const date = { day: 21, month: 10, year: 2021 }; + const yearRange = [2000, 2024] as [number, number]; + + const prop = getDateElementProps(date, false, yearRange, 'MDY'); + + expect(prop).toEqual([ + { type: 'month', range: monthRange, value: date.month }, + { type: 'day', range: dayRange, value: date.day }, + { type: 'year', range: yearRange, value: date.year }, + ]); + }); + + it("returns date in 'DMY' format when time is false and format is not passed", () => { + const date = { day: 1, month: 7, year: 2017 }; + const yearRange = [2010, 2025] as [number, number]; + + const prop = getDateElementProps(date, false, yearRange, 'DMY'); + + expect(prop).toEqual([ + { type: 'day', range: dayRange, value: date.day }, + { type: 'month', range: monthRange, value: date.month }, + { type: 'year', range: yearRange, value: date.year }, + ]); + }); + + it("returns date in 'YMD' format with time when format is not specified", () => { + const date = { day: 13, month: 10, year: 2003, hour: 12, minute: 45, second: 18 }; + + const prop = getDateElementProps(date, true); + + expect(prop).toEqual([ + { type: 'year', range: defaultYearRange, value: date.year }, + { type: 'month', range: monthRange, value: date.month }, + { type: 'day', range: dayRange, value: date.day }, + { type: 'hour', range: [0, 23], value: date.hour }, + { type: 'minute', range: [0, 59], value: date.minute }, + { type: 'second', range: [0, 59], value: date.second }, + ]); + }); + + it("returns date in 'DMY' format with time when format is not specified", () => { + const date = { day: 13, month: 10, year: 2003, hour: 12, minute: 45, second: 18 }; + + const prop = getDateElementProps(date, true, undefined, 'DMY'); + + expect(prop).toEqual([ + { type: 'day', range: dayRange, value: date.day }, + { type: 'month', range: monthRange, value: date.month }, + { type: 'year', range: defaultYearRange, value: date.year }, + { type: 'hour', range: [0, 23], value: date.hour }, + { type: 'minute', range: [0, 59], value: date.minute }, + { type: 'second', range: [0, 59], value: date.second }, + ]); + }); + + it("returns date in 'MDY' format with time when format is not specified", () => { + const date = { day: 13, month: 10, year: 2003, hour: 12, minute: 45, second: 18 }; + const yearRange = [1999, 2023] as [number, number]; + + const prop = getDateElementProps(date, true, yearRange, 'MDY'); + + expect(prop).toEqual([ + { type: 'month', range: monthRange, value: date.month }, + { type: 'day', range: dayRange, value: date.day }, + { type: 'year', range: yearRange, value: date.year }, + { type: 'hour', range: [0, 23], value: date.hour }, + { type: 'minute', range: [0, 59], value: date.minute }, + { type: 'second', range: [0, 59], value: date.second }, + ]); + }); +});