diff --git a/.changeset/cool-dragons-divide.md b/.changeset/cool-dragons-divide.md new file mode 100644 index 000000000..827f7eb3f --- /dev/null +++ b/.changeset/cool-dragons-divide.md @@ -0,0 +1,7 @@ +--- +"@getodk/xforms-engine": minor +"@getodk/web-forms": minor +"@getodk/xpath": minor +--- + +Improved error messages for invalid forms. diff --git a/packages/web-forms/locales/strings_en.json b/packages/web-forms/locales/strings_en.json index 38914c2ff..4055a1963 100644 --- a/packages/web-forms/locales/strings_en.json +++ b/packages/web-forms/locales/strings_en.json @@ -19,9 +19,13 @@ "string": "Next", "developer_comment": "Label for the button that moves to the next page in a paginated form." }, + "odk_web_forms.evaluation.error": { + "string": "Error found while evaluating this form. Error message: \"{message}\". Reported by {count, plural, one {field} other {fields}}: {fields}. Please contact the person who sent you the form link.", + "developer_comment": "Form error banner message. {message} is the raw error message. {fields} is a string naming the fields affected. {count} is the number of fields reporting the error." + }, "odk_web_forms.validation.error": { "string": "{count, plural, one {# question with error} other {# questions with errors}}", - "developer_comment": "Error banner message. {count} is the number of validation violations." + "developer_comment": "Validation error banner message. {count} is the number of validation issues." }, "odk_web_forms.validation.view.label": { "string": "View", diff --git a/packages/web-forms/src/components/OdkWebForm.i18n.json b/packages/web-forms/src/components/OdkWebForm.i18n.json index 9078fa858..65e116087 100644 --- a/packages/web-forms/src/components/OdkWebForm.i18n.json +++ b/packages/web-forms/src/components/OdkWebForm.i18n.json @@ -11,9 +11,13 @@ "string": "Next", "developer_comment": "Label for the button that moves to the next page in a paginated form." }, + "odk_web_forms.evaluation.error": { + "string": "Error found while evaluating this form. Error message: \"{message}\". Reported by {count, plural, one {field} other {fields}}: {fields}. Please contact the person who sent you the form link.", + "developer_comment": "Form error banner message. {message} is the raw error message. {fields} is a string naming the fields affected. {count} is the number of fields reporting the error." + }, "odk_web_forms.validation.error": { "string": "{count, plural, one {# question with error} other {# questions with errors}}", - "developer_comment": "Error banner message. {count} is the number of validation violations." + "developer_comment": "Validation error banner message. {count} is the number of validation issues." }, "odk_web_forms.validation.view.label": { "string": "View", diff --git a/packages/web-forms/src/components/OdkWebForm.vue b/packages/web-forms/src/components/OdkWebForm.vue index c00fa2d63..1117a1d97 100644 --- a/packages/web-forms/src/components/OdkWebForm.vue +++ b/packages/web-forms/src/components/OdkWebForm.vue @@ -322,10 +322,28 @@ provide(REVEAL_VIOLATIONS, revealViolations); // It returns violations for questions the user has seen. const revealedViolations = computed(() => { const violations = state.value.root?.validationState.violations ?? []; + const nonErrorViolations = violations.filter((violation) => violation.violation.condition !== 'error'); if (submitPressed.value) { - return violations; + return nonErrorViolations; } - return violations.filter(({ nodeId }) => touchedQuestions.has(nodeId)); + return nonErrorViolations.filter((violation) => touchedQuestions.has(violation.nodeId)); +}); + +const errorViolations = computed(() => { + const violations = state.value.root?.validationState.violations ?? []; + const grouped = new Map(); + violations.forEach(error => { + const key = error.violation.condition === 'error' && error.violation.message; + if (!key) { + return; + } + const fieldReferences = grouped.get(key) ?? []; + const rootNodeName = state.value.root?.definition.nodeset; + const fieldReference = error.reference.replace(rootNodeName + '/', ''); + fieldReferences.push(fieldReference); + grouped.set(key, fieldReferences); + }); + return grouped; }); const validationErrorMessage = computed(() => { @@ -339,7 +357,11 @@ const showValidationError = computed(() => { if (errorBannerDismissed.value) { return false; } - return !!(validationErrorMessage.value.length || geolocationErrorMessage.value?.length); + return ( + !!validationErrorMessage.value.length || + !!geolocationErrorMessage.value?.length || + !!errorViolations.value.size + ); }); onUnmounted(() => { @@ -389,6 +411,13 @@ onUnmounted(() => { >