diff --git a/package.json b/package.json index 251973f..0eb03ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@indec/form-builder", - "version": "2.4.3", + "version": "2.4.4", "description": "Form builder", "main": "index.js", "private": false, diff --git a/src/components/DatePicker/DatePicker.js b/src/components/DatePicker/DatePicker.js index 21cf519..7aeaba5 100644 --- a/src/components/DatePicker/DatePicker.js +++ b/src/components/DatePicker/DatePicker.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import {es} from 'date-fns/locale'; import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFns'; import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider'; -import MuiInputLabel from '@mui/material/InputLabel'; import Stack from '@mui/material/Stack'; import dateTypes from '@/constants/dateTypes'; @@ -10,8 +9,9 @@ import formikField from '@/utils/propTypes/formikField'; import formikForm from '@/utils/propTypes/formikForm'; import FieldMessage from '../FieldMessage'; +import InputLabel from '../InputLabel'; import TextField from '../TextField'; -import DateTimePickerSelector from './DatePickerSelector'; +import DateTimePickerSelector from './DateTimePickerSelector'; function DatePicker({ metadata: {dateType}, field, label, form, warnings, disabled, ...props @@ -19,7 +19,7 @@ function DatePicker({ const isRange = [dateTypes.RANGE_WITHOUT_HOUR, dateTypes.RANGE_WITH_HOUR].includes(dateType); return ( - {label} + ({ export const Basic = Template.bind({}); Basic.args = { - readOnlyMode: false, - label: 'Select dates', + disabled: false, + label: {text: 'Select dates'}, name: 'S1.0.S1P1.answer.value', warnings: {}, metadata: { @@ -100,8 +100,8 @@ Basic.args = { export const DateWithHour = Template.bind({}); DateWithHour.args = { - readOnlyMode: false, - label: 'Select dates', + disabled: false, + label: {text: 'Select dates'}, name: 'S1.0.S1P1.answer.value', warnings: {}, metadata: { @@ -113,8 +113,8 @@ DateWithHour.args = { export const RangeWithoutHour = Template.bind({}); RangeWithoutHour.args = { - readOnlyMode: false, - label: 'Select dates', + disabled: false, + label: {text: 'Select dates'}, name: 'S1.0.S1P1.answer.value', warnings: {}, metadata: { @@ -126,8 +126,8 @@ RangeWithoutHour.args = { export const RangeWithHour = Template.bind({}); RangeWithHour.args = { - readOnlyMode: false, - label: 'Select dates', + disabled: false, + label: {text: 'Select dates'}, name: 'S1.0.S1P1.answer.value', warnings: {}, metadata: { @@ -139,8 +139,8 @@ RangeWithHour.args = { export const WithErrors = Template.bind({}); WithErrors.args = { - readOnlyMode: false, - label: 'Select dates', + disabled: false, + label: {text: 'Select dates'}, name: 'S1.0.S1P1.answer.value', warnings: {}, metadata: { diff --git a/src/components/DatePicker/DatePickerSelector.js b/src/components/DatePicker/DatePickerSelector.js deleted file mode 100644 index 715a81f..0000000 --- a/src/components/DatePicker/DatePickerSelector.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import {DatePicker as MuiDatePicker} from '@mui/x-date-pickers/DatePicker'; -import {DateTimePicker as MuiDateTimePicker} from '@mui/x-date-pickers/DateTimePicker'; - -import dateTypes from '@/constants/dateTypes'; - -function DateTimePickerSelector({type, ...props}) { - if ([dateTypes.DATE_WITH_HOUR, dateTypes.RANGE_WITH_HOUR].includes(type)) { - return ( - - ); - } - return ( - - ); -} - -DateTimePickerSelector.propTypes = { - type: PropTypes.oneOf(Object.values(dateTypes)).isRequired -}; - -export default DateTimePickerSelector; diff --git a/src/components/DatePicker/DateTimePickerSelector/DatePickerSelector.js b/src/components/DatePicker/DateTimePickerSelector/DatePickerSelector.js new file mode 100644 index 0000000..511493e --- /dev/null +++ b/src/components/DatePicker/DateTimePickerSelector/DatePickerSelector.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; +import {DatePicker as MuiDatePicker} from '@mui/x-date-pickers/DatePicker'; +import {DateTimePicker as MuiDateTimePicker} from '@mui/x-date-pickers/DateTimePicker'; + +import dateTypes from '@/constants/dateTypes'; + +function DateTimePickerSelector({type, onChange, value, ...props}) { + const handleChange = date => { + onChange(date ? date.toISOString() : date); + }; + if ([dateTypes.DATE_WITH_HOUR, dateTypes.RANGE_WITH_HOUR].includes(type)) { + return ( + + ); + } + return ( + + ); +} + +DateTimePickerSelector.propTypes = { + onChange: PropTypes.func.isRequired, + type: PropTypes.oneOf(Object.values(dateTypes)).isRequired, + value: PropTypes.string +}; + +DateTimePickerSelector.defaultProps = { + value: undefined +}; + +export default DateTimePickerSelector; diff --git a/src/components/DatePicker/DateTimePickerSelector/index.js b/src/components/DatePicker/DateTimePickerSelector/index.js new file mode 100644 index 0000000..96fb3d9 --- /dev/null +++ b/src/components/DatePicker/DateTimePickerSelector/index.js @@ -0,0 +1,3 @@ +import DateTimePickerSelector from './DatePickerSelector'; + +export default DateTimePickerSelector; diff --git a/src/components/FormBuilder/FormBuilder.js b/src/components/FormBuilder/FormBuilder.js index 3966679..c96fb0b 100644 --- a/src/components/FormBuilder/FormBuilder.js +++ b/src/components/FormBuilder/FormBuilder.js @@ -109,33 +109,31 @@ function FormBuilder({ } /> { - components.NavigationButtons - ? ( - addNewSection(setValues, values) : undefined} - onInterrupt={ - section.interruption.interruptible - ? () => handleOpenModal(modals.INTERRUPTION_MODAL, section.id) - : undefined - } - /> - ) - : ( - addNewSection(setValues, values) : undefined} - onInterrupt={ - section.interruption.interruptible - ? () => handleOpenModal(modals.INTERRUPTION_MODAL, section.id) - : undefined - } - readOnlyMode={isReadOnly} - /> - ) + components.NavigationButtons ? ( + addNewSection(setValues, values) : undefined} + onInterrupt={ + section.interruption.interruptible + ? () => handleOpenModal(modals.INTERRUPTION_MODAL, section.id) + : undefined + } + /> + ) : ( + addNewSection(setValues, values) : undefined} + onInterrupt={ + section.interruption.interruptible + ? () => handleOpenModal(modals.INTERRUPTION_MODAL, section.id) + : undefined + } + readOnlyMode={isReadOnly} + /> + ) } ); diff --git a/src/components/InputLabel/InputLabel.js b/src/components/InputLabel/InputLabel.js index 7ca3785..b2c1c9f 100644 --- a/src/components/InputLabel/InputLabel.js +++ b/src/components/InputLabel/InputLabel.js @@ -1,7 +1,5 @@ import PropTypes from 'prop-types'; -import {blue} from '@mui/material/colors'; -import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; @@ -17,26 +15,13 @@ function InputLabel({ label, form, field, disabled, warnings }) { const {hasWarning, hasError} = hasFormikErrorsAndWarnings({form, field, warnings}); + const labelNumber = label.number ? `${label.number} - ` : ''; + return ( - {label.number && ( - - {label.number} - - )} - {label.text} + {`${labelNumber}${label.text}`} {' '} {hasError && '*'} diff --git a/src/components/index.js b/src/components/index.js index 1d72b37..9ed3ce7 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,4 +1,5 @@ import Checkbox from './Checkbox'; +import Currency from './Currency'; import FieldMessage from './FieldMessage'; import FormBuilder from './FormBuilder'; import InputLabel from './InputLabel'; @@ -7,6 +8,7 @@ import Select from './Select'; import TextField from './TextField'; export {Checkbox}; +export {Currency}; export {FieldMessage}; export {FormBuilder}; export {InputLabel}; diff --git a/src/hooks/useSubQuestions.js b/src/hooks/useSubQuestions.js index d6e860c..e5a22bd 100644 --- a/src/hooks/useSubQuestions.js +++ b/src/hooks/useSubQuestions.js @@ -7,7 +7,9 @@ const useSubQuestions = ({subQuestions, value, name}) => { useEffect(() => { const subQuestionsFiltered = subQuestions.filter( subQuestion => { - const condition = getNavigation({navigation: subQuestion.navigation, answers: value}); + const condition = getNavigation({ + navigation: subQuestion.navigation, answers: value, questionType: subQuestion.type + }); return !condition; } ); diff --git a/src/utils/__tests__/operations.test.js b/src/utils/__tests__/operations.test.js index 271575d..a94689e 100644 --- a/src/utils/__tests__/operations.test.js +++ b/src/utils/__tests__/operations.test.js @@ -1,3 +1,5 @@ +import questionTypes from '@/constants/questionTypes'; + import operations from '../operations'; describe('operations', () => { @@ -21,6 +23,20 @@ describe('operations', () => { expect(operations.eq({a: 'test', b: 'test'}, '')).toBe(false); }); }); + + describe('when `questionType` is date', () => { + it('should return `true` if `a` is equal to `b`', () => { + expect( + operations.eq('2023-09-01T16:54:00.000Z', '2023-09-01T16:54:00.000Z', questionTypes.DATE) + ).toBe(true); + }); + + it('should return `false` if `a` is not equal to `b`', () => { + expect( + operations.eq('2023-09-01T16:53:00.000Z', '2023-09-01T16:54:00.000Z', questionTypes.DATE) + ).toBe(false); + }); + }); }); describe('not equals', () => { @@ -31,6 +47,20 @@ describe('operations', () => { it('should return `true` if `a` is not equals to `b`', () => { expect(operations.ne(1, 2)).toBe(true); }); + + describe('when `questionType` is date', () => { + it('should return `false` if `a` is equal to `b`', () => { + expect( + operations.ne('2023-09-01T16:54:00.000Z', '2023-09-01T16:54:00.000Z', questionTypes.DATE) + ).toBe(false); + }); + + it('should return `true` if `a` is not equal to `b`', () => { + expect( + operations.ne('2023-09-01T16:53:00.000Z', '2023-09-01T16:54:00.000Z', questionTypes.DATE) + ).toBe(true); + }); + }); }); describe('greater than', () => { @@ -61,6 +91,20 @@ describe('operations', () => { it('should return `false` if `a` is not less than `b`', () => { expect(operations.lt('test', 3)).toBe(false); }); + + describe('when `questionType` is date', () => { + it('should return `true` if `a` is before than `b`', () => { + expect( + operations.lt('2023-09-01T16:54:00.000Z', '2023-09-01T16:55:00.000Z', questionTypes.DATE) + ).toBe(true); + }); + + it('should return `false` if `a` is not before than `b`', () => { + expect( + operations.lt('2023-09-01T16:55:00.000Z', '2023-09-01T16:54:00.000Z', questionTypes.DATE) + ).toBe(false); + }); + }); }); describe('less than or equal to', () => { diff --git a/src/utils/buildYupSchema.js b/src/utils/buildYupSchema.js index 9554b5e..7cb01f3 100644 --- a/src/utils/buildYupSchema.js +++ b/src/utils/buildYupSchema.js @@ -37,7 +37,7 @@ const getValidatorType = (type, options, metadata) => { } }; -const handleValidations = ({validator, validations, opts, answers, questionName, multiple = false}) => { +const handleValidations = ({validator, validations, opts, answers, questionName, multiple = false, questionType}) => { let newValidator = validator; validations.forEach((validation) => { const {type: messageType} = validation.message; @@ -54,7 +54,7 @@ const handleValidations = ({validator, validations, opts, answers, questionName, function (currentValue) { let formatAnswer = answers; formatAnswer = multiple ? {...formatAnswer, [questionName]: {answer: {value: currentValue}}} : formatAnswer; - const rules = getValidationRules({validation, answers: formatAnswer}); + const rules = getValidationRules({validation, answers: formatAnswer, questionType}); if (rules.some(value => value === true)) { return this.createError({path: this.path, message: validation.message.text}); } @@ -136,7 +136,7 @@ export default function buildYupSchema(schema, config, values, opts = {}) { return schemaWithValidations; } validator = handleValidations({ - validator, validations, opts, answers: values, questionName: name, multiple + validator, validations, opts, answers: values, questionName: name, multiple, questionType: type }); schemaWithValidations[name] = Yup.object({ id: Yup.number().required(), diff --git a/src/utils/getNavigation.js b/src/utils/getNavigation.js index 4ba714a..3fa75e2 100644 --- a/src/utils/getNavigation.js +++ b/src/utils/getNavigation.js @@ -1,11 +1,11 @@ import getValidationRules from './getValidationRules'; -const getNavigation = ({navigation = [], answers}) => { +const getNavigation = ({navigation = [], answers, questionType}) => { if (navigation.length === 0) { return true; } const navigationRules = navigation.map(nav => { - const rules = getValidationRules({validation: nav, answers}); + const rules = getValidationRules({validation: nav, answers, questionType}); // eslint-disable-next-line consistent-return return { action: nav.action, diff --git a/src/utils/getValidationRules.js b/src/utils/getValidationRules.js index c9a26ca..06e541a 100644 --- a/src/utils/getValidationRules.js +++ b/src/utils/getValidationRules.js @@ -1,18 +1,22 @@ import operations from './operations'; -const getConditions = ({conditions, answers}) => conditions.map( +const getConditions = ({conditions, answers, questionType}) => conditions.map( condition => { if (!Object.prototype.hasOwnProperty.call(answers, condition.question)) { return false; } const question = answers[condition.question]; const value = question?.answer?.value; - return operations[condition.type](typeof value === 'number' ? value : value || '', condition.value); + return operations[condition.type]( + typeof value === 'number' ? value : value || '', + condition.value, + questionType + ); } ); -const getValidationRules = ({validation, answers}) => validation.rules.map(rule => { - const conditions = getConditions({conditions: rule.conditions, answers}); +const getValidationRules = ({validation, answers, questionType}) => validation.rules.map(rule => { + const conditions = getConditions({conditions: rule.conditions, answers, questionType}); return conditions.every(condition => condition); }); diff --git a/src/utils/operations.js b/src/utils/operations.js index 837326d..da49a82 100644 --- a/src/utils/operations.js +++ b/src/utils/operations.js @@ -1,19 +1,49 @@ +import {isEqual, isBefore, isAfter} from 'date-fns'; + +import questionTypes from '@/constants/questionTypes'; + const isString = value => typeof value === 'string'; const isObject = value => typeof value === 'object' && value !== null && !Array.isArray(value); const operations = { - eq: (a, b) => { + eq: (a, b, questionType) => { + if (questionType === questionTypes.DATE) { + const firstDate = new Date(a); + const secondDate = new Date(b); + return isEqual(firstDate, secondDate); + } if (isObject(a)) { return Object.values(a).some(value => value === b); } return a === b; }, - ne: (a, b) => a !== b, - gt: (a, b) => (isString(a) ? a.length > b : a > b), + ne: (a, b, questionType) => { + if (questionType === questionTypes.DATE) { + const firstDate = new Date(a); + const secondDate = new Date(b); + return !isEqual(firstDate, secondDate); + } + return a !== b; + }, + gt: (a, b, questionType) => { + if (questionType === questionTypes.DATE) { + const firstDate = new Date(a); + const secondDate = new Date(b); + return isAfter(firstDate, secondDate); + } + return (isString(a) ? a.length > b : a > b); + }, gte: (a, b) => (isString(a) ? a.length >= b : a >= b), - lt: (a, b) => (isString(a) ? a.length < b : a < b), + lt: (a, b, questionType) => { + if (questionType === questionTypes.DATE) { + const firstDate = new Date(a); + const secondDate = new Date(b); + return isBefore(firstDate, secondDate); + } + return (isString(a) ? a.length < b : a < b); + }, lte: (a, b) => (isString(a) ? a.length <= b : a <= b), in: (a, b) => a.includes(b), nin: (a, b) => !a.includes(b)