From 43d820f69e6da9db6bc49ffc6785a0797eaea43e Mon Sep 17 00:00:00 2001 From: Chong Jia Chua Date: Thu, 25 Jun 2026 11:35:32 +0100 Subject: [PATCH 1/2] fix: improve error messaging by giving clearer instructions and icon --- apps/e2e/cypress/e2e/genericTemplates.cy.ts | 39 ++++++++++++++++- .../questionary/QuestionaryStepView.tsx | 13 ++++++ ...nericTemplateStepDisplayElementFactory.tsx | 3 ++ .../GenericTemplateDefinition.tsx | 42 +++++++++++++++---- .../QuestionnairesListItem.tsx | 13 ++++-- 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/apps/e2e/cypress/e2e/genericTemplates.cy.ts b/apps/e2e/cypress/e2e/genericTemplates.cy.ts index 4b53b35fd8..d5484add2d 100644 --- a/apps/e2e/cypress/e2e/genericTemplates.cy.ts +++ b/apps/e2e/cypress/e2e/genericTemplates.cy.ts @@ -24,6 +24,11 @@ context('GenericTemplates tests', () => { const copyButtonLabel = faker.lorem.words(3); const genericTemplateTitle = faker.lorem.words(3); const genericTemplateQuestionaryQuestion = twoFakes(3); + const genericTemplateTextMax = 50; + const tooLongGenericTemplateAnswer = faker.string.alpha( + genericTemplateTextMax + 1 + ); + const validGenericTemplateAnswer = faker.string.alpha(genericTemplateTextMax); const genericTemplateTitleAnswers = [ faker.lorem.words(3), faker.lorem.words(3), @@ -63,6 +68,7 @@ context('GenericTemplates tests', () => { cy.updateQuestion({ id: createdQuestion1.id, question: genericTemplateQuestions[0], + config: `{"required":false,"small_label":"","tooltip":"","htmlQuestion":"","isHtmlQuestion":false,"min":null,"max":${genericTemplateTextMax},"multiline":false,"placeholder":"","isCounterHidden":false,"readPermissions":[]}`, }); cy.createQuestionTemplateRelation({ @@ -727,24 +733,51 @@ context('GenericTemplates tests', () => { .should('have.value', genericTemplateTitle) .blur(); + cy.contains(genericTemplateQuestions[0]) + .parent() + .find('input') + .clear() + .type(tooLongGenericTemplateAnswer) + .blur(); + cy.get( '[data-cy="genericTemplate-declaration-modal"] [data-cy="save-button"]' ).click(); cy.finishedLoading(); + cy.contains( + `Value must be at most ${genericTemplateTextMax} characters` + ).should('not.exist'); + cy.get('body').type('{esc}'); cy.finishedLoading(); cy.get('[data-cy="questionnaires-list-item"]').should('have.length', 1); + cy.get('[data-cy="questionnaires-list-item-completed:false"]').should( + 'exist' + ); cy.get('[data-cy="save-and-continue-button"]').click(); - cy.contains('All genericTemplates must be completed'); + cy.contains( + `The following sample(s) are violating constraints: ${genericTemplateTitle}. Fix the red sections and click "Save and continue" in each sample before continuing.` + ); cy.contains(genericTemplateTitle).click(); + cy.contains( + `Value must be at most ${genericTemplateTextMax} characters` + ).should('exist'); + + cy.contains(genericTemplateQuestions[0]) + .parent() + .find('input') + .clear() + .type(validGenericTemplateAnswer) + .blur(); + cy.get( '[data-cy="genericTemplate-declaration-modal"] [data-cy="save-and-continue-button"]' ).click(); @@ -933,7 +966,9 @@ context('GenericTemplates tests', () => { cy.contains('Save and continue').click(); - cy.contains('All genericTemplates must be completed').should('exist'); + cy.contains( + `The following sample(s) are violating constraints: ${genericTemplateTitle}. Fix the red sections and click "Save and continue" in each sample before continuing.` + ).should('exist'); }); it('Should be a character limit of 256 to the template proposal question for office user', () => { diff --git a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx index 60fe8a9a61..42f443e705 100644 --- a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx +++ b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx @@ -69,6 +69,7 @@ function QuestionaryStepView(props: { readonly: boolean; onStepComplete?: (topicId: number) => void; confirm: WithConfirmType; + showValidationErrorsOnMount?: boolean; }) { const { topicId, confirm } = props; @@ -110,6 +111,14 @@ function QuestionaryStepView(props: { state, api ); + const activeFieldIds = activeFields.map((field) => field.question.id); + const initialTouched = activeFieldIds.reduce( + (fields, fieldId) => ({ + ...fields, + [fieldId]: true, + }), + {} + ); const [lastSavedFormValues, setLastSavedFormValues] = useState(initialValues); const [isSaving, setIsSaving] = useState(false); @@ -284,7 +293,11 @@ function QuestionaryStepView(props: { return ( { const isSaveSuccess = await performSave(false); diff --git a/apps/frontend/src/components/questionary/questionaries/genericTemplate/GenericTemplateStepDisplayElementFactory.tsx b/apps/frontend/src/components/questionary/questionaries/genericTemplate/GenericTemplateStepDisplayElementFactory.tsx index dc96216115..69bb6b0c69 100644 --- a/apps/frontend/src/components/questionary/questionaries/genericTemplate/GenericTemplateStepDisplayElementFactory.tsx +++ b/apps/frontend/src/components/questionary/questionaries/genericTemplate/GenericTemplateStepDisplayElementFactory.tsx @@ -29,11 +29,14 @@ const GenericTemplateQuestionaryStepView = ({ const isLastStep = (wizardStep: WizardStep) => state.wizardSteps[state.wizardSteps.length - 1] === wizardStep; + const shouldShowValidationErrorsOnMount = state.genericTemplate.id > 0; + return (
{ if (isLastStep(wizardStep)) { dispatch({ diff --git a/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx index e8f2947c3f..ebd1fd3219 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx @@ -10,6 +10,25 @@ import QuestionaryComponentGenericTemplate from './QuestionaryComponentGenericTe import { QuestionGenericTemplateForm } from './QuestionGenericTemplateForm'; import { QuestionTemplateRelationGenericTemplateForm } from './QuestionTemplateRelationGenericTemplateForm'; import { QuestionaryComponentDefinition } from '../../QuestionaryComponentRegistry'; + +type GenericTemplateValidationValue = { + title?: string | null; + questionary?: { + isCompleted?: boolean | null; + } | null; +}; + +const getIncompleteGenericTemplatesMessage = ( + genericTemplates: GenericTemplateValidationValue[] +) => { + const genericTemplateTitles = genericTemplates + .filter((genericTemplate) => !genericTemplate?.questionary?.isCompleted) + .map((genericTemplate) => genericTemplate.title || 'Untitled sample') + .join(', '); + + return `The following sample(s) are violating constraints: ${genericTemplateTitles}. Fix the red sections and click "Save and continue" in each sample before continuing.`; +}; + export const genericTemplateDefinition: QuestionaryComponentDefinition = { dataType: DataType.GENERIC_TEMPLATE, name: 'Sub Template', @@ -51,14 +70,21 @@ export const genericTemplateDefinition: QuestionaryComponentDefinition = { ); } - schema = schema.test( - 'allGenericTemplatesCompleted', - 'All genericTemplates must be completed', - (value) => - value?.every( - (genericTemplate) => genericTemplate?.questionary.isCompleted - ) ?? false - ); + schema = schema.test('allGenericTemplatesCompleted', function (value) { + const genericTemplates = + (value as GenericTemplateValidationValue[] | undefined) ?? []; + const hasIncompleteGenericTemplates = genericTemplates.some( + (genericTemplate) => !genericTemplate?.questionary?.isCompleted + ); + + if (!hasIncompleteGenericTemplates) { + return true; + } + + return this.createError({ + message: getIncompleteGenericTemplatesMessage(genericTemplates), + }); + }); return schema; }, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/QuestionnairesListItem.tsx b/apps/frontend/src/components/questionary/questionaryComponents/QuestionnairesListItem.tsx index 887acf09e1..d9313778fb 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/QuestionnairesListItem.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/QuestionnairesListItem.tsx @@ -1,6 +1,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import DescriptionIcon from '@mui/icons-material/Description'; import FileCopy from '@mui/icons-material/FileCopy'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import Avatar from '@mui/material/Avatar'; import IconButton from '@mui/material/IconButton'; import ListItemAvatar from '@mui/material/ListItemAvatar'; @@ -33,10 +34,14 @@ export function QuestionnairesListItem({ > - + {record.isCompleted ? ( + + ) : ( + + )} Date: Fri, 26 Jun 2026 12:14:54 +0100 Subject: [PATCH 2/2] fix: change error message wording to be less specific --- apps/e2e/cypress/e2e/genericTemplates.cy.ts | 4 ++-- .../GenericTemplate/GenericTemplateDefinition.tsx | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/e2e/cypress/e2e/genericTemplates.cy.ts b/apps/e2e/cypress/e2e/genericTemplates.cy.ts index d5484add2d..0650f4ad90 100644 --- a/apps/e2e/cypress/e2e/genericTemplates.cy.ts +++ b/apps/e2e/cypress/e2e/genericTemplates.cy.ts @@ -762,7 +762,7 @@ context('GenericTemplates tests', () => { cy.get('[data-cy="save-and-continue-button"]').click(); cy.contains( - `The following sample(s) are violating constraints: ${genericTemplateTitle}. Fix the red sections and click "Save and continue" in each sample before continuing.` + `${genericTemplateTitle} is violating constraints. Please open each entry, fix the validation errors, and click "Save and continue".` ); cy.contains(genericTemplateTitle).click(); @@ -967,7 +967,7 @@ context('GenericTemplates tests', () => { cy.contains('Save and continue').click(); cy.contains( - `The following sample(s) are violating constraints: ${genericTemplateTitle}. Fix the red sections and click "Save and continue" in each sample before continuing.` + `${genericTemplateTitle} is violating constraints. Please open each entry, fix the validation errors, and click "Save and continue".` ).should('exist'); }); diff --git a/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx index ebd1fd3219..e1f0910d25 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/GenericTemplate/GenericTemplateDefinition.tsx @@ -21,12 +21,15 @@ type GenericTemplateValidationValue = { const getIncompleteGenericTemplatesMessage = ( genericTemplates: GenericTemplateValidationValue[] ) => { - const genericTemplateTitles = genericTemplates - .filter((genericTemplate) => !genericTemplate?.questionary?.isCompleted) - .map((genericTemplate) => genericTemplate.title || 'Untitled sample') + const incompleteGenericTemplates = genericTemplates.filter( + (genericTemplate) => !genericTemplate?.questionary?.isCompleted + ); + const genericTemplateTitles = incompleteGenericTemplates + .map((genericTemplate) => genericTemplate.title || 'Untitled entry') .join(', '); + const verb = incompleteGenericTemplates.length === 1 ? 'is' : 'are'; - return `The following sample(s) are violating constraints: ${genericTemplateTitles}. Fix the red sections and click "Save and continue" in each sample before continuing.`; + return `${genericTemplateTitles} ${verb} violating constraints. Please open each entry, fix the validation errors, and click "Save and continue".`; }; export const genericTemplateDefinition: QuestionaryComponentDefinition = {