Skip to content

Commit

Permalink
Merge pull request #545 from open-formulieren/feature/1884-more-time-…
Browse files Browse the repository at this point in the history
…custom-errors

[OF#1884] Fine grained custom errors for time components
  • Loading branch information
sergei-maertens authored Sep 6, 2023
2 parents 42a8a63 + 99732a7 commit e2000aa
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 9 deletions.
48 changes: 41 additions & 7 deletions src/formio/validators/MinMaxTimeValidator.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import set from 'lodash/set';
import moment from 'moment';

const validateTimeBoundaries = (minBoundary, maxBoundary, timeValue) => {
Expand All @@ -7,20 +8,28 @@ const validateTimeBoundaries = (minBoundary, maxBoundary, timeValue) => {

// Case 0: no boundaries given
if (!minTime && !maxTime) {
return true;
return {isValid: true};
}

// Case 1: only one boundary is given
if (!minTime || !maxTime) {
if (minTime) return parsedValue >= minTime;
if (maxTime) return parsedValue < maxTime;
if (minTime) return {isValid: parsedValue >= minTime, errorKeys: ['minTime']};
if (maxTime) return {isValid: parsedValue < maxTime, errorKeys: ['maxTime']};
} else {
// Case 2: min boundary is smaller than max boundary
if (minTime < maxTime) {
return parsedValue >= minTime && parsedValue < maxTime;
const isTooEarly = parsedValue < minTime;
const isTooLate = parsedValue >= maxTime;
return {
isValid: !isTooEarly && !isTooLate,
errorKeys: [isTooEarly ? 'minTime' : 'maxTime', 'invalid_time'],
};
} else {
// Case 3: min boundary is bigger than max boundary (it's the next day. For example min = 08:00, max = 01:00)
return !(parsedValue >= maxTime && parsedValue < minTime);
return {
isValid: !(parsedValue >= maxTime && parsedValue < minTime),
errorKeys: ['invalid_time'],
};
}
}
};
Expand All @@ -31,7 +40,23 @@ const MinMaxTimeValidator = {
const minTime = moment(component.component.minTime || '00:00:00', 'HH:mm:ss').format('HH:mm');
const maxTime = moment(component.component.maxTime || '23:59:59', 'HH:mm:ss').format('HH:mm');

const errorMessage = component.component.errors?.invalid_time ?? 'invalid_time';
let errorMessage = component.component.errors?.invalid_time || 'invalid_time';
const errorKeys = component?.openForms?.validationErrorContext?.minMaxTimeValidatorErrorKeys;
const componentErrorMessages = component.component.errors;

// The error keys are in order of priority: for example, if a time is below minTime, the
// errorKeys would be ['minTime', 'invalid_time']. If the form designer configured a custom minTime
// error message, it will be used. Otherwise, it will check if a 'invalid_time' custom error was used.
// If not, it will fall back on the default 'invalid_time'.
if (errorKeys && componentErrorMessages) {
for (const errorKey of errorKeys) {
if (componentErrorMessages[errorKey]) {
errorMessage = componentErrorMessages[errorKey];
break;
}
}
}

return component.t(errorMessage, {
minTime: minTime,
maxTime: maxTime,
Expand All @@ -40,7 +65,16 @@ const MinMaxTimeValidator = {
check(component, setting, value) {
if (!value) return true;

return validateTimeBoundaries(component.component.minTime, component.component.maxTime, value);
const {isValid, errorKeys} = validateTimeBoundaries(
component.component.minTime,
component.component.maxTime,
value
);

if (!isValid) {
set(component, 'openForms.validationErrorContext.minMaxTimeValidatorErrorKeys', errorKeys);
}
return isValid;
},
};

Expand Down
125 changes: 123 additions & 2 deletions src/jstests/formio/components/time.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ describe('Time Component', () => {
testValidity(invalidValues, false);
});

test('Time component with min/max validation and custom error', done => {
test('Time component with both min/max and max > min validation and custom error', done => {
let formJSON = _.cloneDeep(timeForm);
// Note: the backend dynamically updates the configuration so that `translatedErrors` are added to
// `errors` in the correct language.
formJSON.components[0].errors = {
invalid_time: 'Custom error! Min time: {{ minTime }} Max time: {{ maxTime }}.',
minTime: 'Custom error! Min time {{ minTime }}',
maxTime: 'Custom error! max time {{ maxTime }}',
};
formJSON.components[0].maxTime = '13:00:00';
formJSON.components[0].minTime = '12:00:00';
Expand All @@ -221,7 +223,126 @@ describe('Time Component', () => {

setTimeout(() => {
expect(!!component.error).toBeTruthy();
expect(component.error.message).toEqual('Custom error! Min time: 12:00 Max time: 13:00.');
expect(component.error.message).toEqual('Custom error! Min time 12:00');

done();
}, 300);
})
.catch(done);
});

test('Time component with both min/max and max < min validation and custom error', done => {
let formJSON = _.cloneDeep(timeForm);
// Note: the backend dynamically updates the configuration so that `translatedErrors` are added to
// `errors` in the correct language.
formJSON.components[0].errors = {
invalid_time: 'Custom error! Min time: {{ minTime }} Max time: {{ maxTime }}.',
minTime: 'Custom error! Min time {{ minTime }}',
maxTime: 'Custom error! max time {{ maxTime }}',
};
formJSON.components[0].maxTime = '01:00:00'; // One o'clock in the night of the next day
formJSON.components[0].minTime = '08:00:00';

const element = document.createElement('div');

Formio.createForm(element, formJSON)
.then(form => {
form.setPristine(false);
const component = form.getComponent('time');
const changed = component.setValue('07:00');
expect(changed).toBeTruthy();

setTimeout(() => {
expect(!!component.error).toBeTruthy();
expect(component.error.message).toEqual('Custom error! Min time: 08:00 Max time: 01:00.');

done();
}, 300);
})
.catch(done);
});

test('Time component with only min and custom error', done => {
let formJSON = _.cloneDeep(timeForm);
// Note: the backend dynamically updates the configuration so that `translatedErrors` are added to
// `errors` in the correct language.
formJSON.components[0].errors = {
invalid_time: 'Custom error! Min time: {{ minTime }} Max time: {{ maxTime }}.',
minTime: 'Custom error! Min time {{ minTime }}',
maxTime: 'Custom error! max time {{ maxTime }}',
};
formJSON.components[0].minTime = '13:00:00';

const element = document.createElement('div');

Formio.createForm(element, formJSON)
.then(form => {
form.setPristine(false);
const component = form.getComponent('time');
const changed = component.setValue('10:00');
expect(changed).toBeTruthy();

setTimeout(() => {
expect(!!component.error).toBeTruthy();
expect(component.error.message).toEqual('Custom error! Min time 13:00');

done();
}, 300);
})
.catch(done);
});

test('Time component with only max and custom error', done => {
let formJSON = _.cloneDeep(timeForm);
// Note: the backend dynamically updates the configuration so that `translatedErrors` are added to
// `errors` in the correct language.
formJSON.components[0].errors = {
invalid_time: 'Custom error! Min time: {{ minTime }} Max time: {{ maxTime }}.',
minTime: 'Custom error! Min time {{ minTime }}',
maxTime: 'Custom error! Max time {{ maxTime }}',
};
formJSON.components[0].maxTime = '13:00:00';

const element = document.createElement('div');

Formio.createForm(element, formJSON)
.then(form => {
form.setPristine(false);
const component = form.getComponent('time');
const changed = component.setValue('14:00');
expect(changed).toBeTruthy();

setTimeout(() => {
expect(!!component.error).toBeTruthy();
expect(component.error.message).toEqual('Custom error! Max time 13:00');

done();
}, 300);
})
.catch(done);
});

test('Time component with empty string error', done => {
let formJSON = _.cloneDeep(timeForm);
// Note: the backend dynamically updates the configuration so that `translatedErrors` are added to
// `errors` in the correct language.
formJSON.components[0].errors = {
invalid_time: '',
};
formJSON.components[0].maxTime = '13:00:00';

const element = document.createElement('div');

Formio.createForm(element, formJSON)
.then(form => {
form.setPristine(false);
const component = form.getComponent('time');
const changed = component.setValue('14:00');
expect(changed).toBeTruthy();

setTimeout(() => {
expect(!!component.error).toBeTruthy();
expect(component.error.message).toEqual('invalid_time');

done();
}, 300);
Expand Down

0 comments on commit e2000aa

Please sign in to comment.