diff --git a/eq-author-api/src/businessLogic/onAnswerDeleted.js b/eq-author-api/src/businessLogic/onAnswerDeleted.js index 64e3ad9ea5..a12341225d 100644 --- a/eq-author-api/src/businessLogic/onAnswerDeleted.js +++ b/eq-author-api/src/businessLogic/onAnswerDeleted.js @@ -81,11 +81,24 @@ const removeAnswerFromPiping = (ctx, deletedAnswer, pages) => { deletedAnswer.id, "Deleted answer" ); + + page.answers?.forEach((answer) => { + answer.label = updatePipingValue( + answer.label, + deletedAnswer.id, + "Deleted answer" + ); + }); }); const sections = getSections(ctx); sections.forEach((section) => { + section.title = updatePipingValue( + section.title, + deletedAnswer.id, + "Deleted answer" + ); section.introductionTitle = updatePipingValue( section.introductionTitle, deletedAnswer.id, diff --git a/eq-author-api/src/businessLogic/onAnswerUpdated.js b/eq-author-api/src/businessLogic/onAnswerUpdated.js index 24dae73275..c9af18e4f0 100644 --- a/eq-author-api/src/businessLogic/onAnswerUpdated.js +++ b/eq-author-api/src/businessLogic/onAnswerUpdated.js @@ -3,6 +3,7 @@ const cheerio = require("cheerio"); const { getListById, getSupplementaryDataAsCollectionListById, + getSections, } = require("../../schema/resolvers/utils"); const updatePipingValue = (htmlText, answerId, newValue) => { @@ -17,6 +18,34 @@ const updatePipingValue = (htmlText, answerId, newValue) => { return htmlDoc.html(); }; +const updatePipingInSections = (ctx, updatedAnswer) => { + const sections = getSections(ctx); + + sections.forEach((section) => { + if (section.title?.includes(updatedAnswer.id)) { + section.title = updatePipingValue( + section.title, + updatedAnswer.id, + updatedAnswer.label.replace(/(<([^>]+)>)/gi, "") + ); + } + if (section.introductionTitle?.includes(updatedAnswer.id)) { + section.introductionTitle = updatePipingValue( + section.introductionTitle, + updatedAnswer.id, + updatedAnswer.label.replace(/(<([^>]+)>)/gi, "") + ); + } + if (section.introductionContent?.includes(updatedAnswer.id)) { + section.introductionContent = updatePipingValue( + section.introductionContent, + updatedAnswer.id, + updatedAnswer.label.replace(/(<([^>]+)>)/gi, "") + ); + } + }); +}; + const updatePipingInAnswers = (updatedAnswer, pages) => { if (updatedAnswer.label) { pages.forEach((page) => { @@ -102,4 +131,5 @@ const updatePipingRepeatingAnswer = (ctx, updatedAnswer, oldAnswer) => { module.exports = (ctx, updatedAnswer, pages, oldAnswer) => { updatePipingInAnswers(updatedAnswer, pages); updatePipingRepeatingAnswer(ctx, updatedAnswer, oldAnswer); + updatePipingInSections(ctx, updatedAnswer); }; diff --git a/eq-author-api/src/businessLogic/onSectionUpdated.js b/eq-author-api/src/businessLogic/onSectionUpdated.js index 3a24b7b588..27535f77f1 100644 --- a/eq-author-api/src/businessLogic/onSectionUpdated.js +++ b/eq-author-api/src/businessLogic/onSectionUpdated.js @@ -38,6 +38,11 @@ const deletePiping = (answers, section, pages) => { answer.id, "Deleted answer" ); + section.title = updatePipingValue( + section.title, + answer.id, + "Deleted answer" + ); }); }; @@ -54,6 +59,11 @@ const updatePiping = (answers, section, pages) => { answer.id, answer.label || "Untitled answer" ); + section.title = updatePipingValue( + section.title, + answer.id, + answer.label || "Untitled answer" + ); }); }; diff --git a/eq-author-api/src/validation/schemas/section.json b/eq-author-api/src/validation/schemas/section.json index 3733cc89f7..bbc67a3b90 100644 --- a/eq-author-api/src/validation/schemas/section.json +++ b/eq-author-api/src/validation/schemas/section.json @@ -23,6 +23,12 @@ }, { "requiredWhenSectionSetting": "sectionSummary" + }, + { + "validatePipingAnswerInTitle": true + }, + { + "validatePipingMetadataInTitle": true } ], "errorMessage": "ERR_REQUIRED_WHEN_SETTING" diff --git a/eq-author/src/App/collectionLists/collectionListsPage.js b/eq-author/src/App/collectionLists/collectionListsPage.js index 43a8cf350e..bcd59253e7 100644 --- a/eq-author/src/App/collectionLists/collectionListsPage.js +++ b/eq-author/src/App/collectionLists/collectionListsPage.js @@ -110,6 +110,7 @@ const CollectionListsPage = ({ const handleDeleteList = (id) => () => { deleteList({ variables: { input: { id: id } }, + refetchQueries: ["GetQuestionnaire"], }); }; @@ -127,6 +128,7 @@ const CollectionListsPage = ({ const handleDeleteAnswer = (answerId) => { deleteAnswer({ variables: { input: { id: answerId } }, + refetchQueries: ["GetQuestionnaire"], }); }; diff --git a/eq-author/src/App/section/Design/SectionEditor/SectionEditor.test.js b/eq-author/src/App/section/Design/SectionEditor/SectionEditor.test.js index 6df3d3f577..2a8bae98bb 100644 --- a/eq-author/src/App/section/Design/SectionEditor/SectionEditor.test.js +++ b/eq-author/src/App/section/Design/SectionEditor/SectionEditor.test.js @@ -3,7 +3,6 @@ import { shallow } from "enzyme"; import { SectionEditor } from "App/section/Design/SectionEditor"; import RichTextEditor from "components/RichTextEditor"; -import { sectionErrors } from "constants/validationMessages"; import suppressConsoleMessage from "tests/utils/supressConsol"; /* @@ -96,7 +95,6 @@ describe("SectionEditor", () => { onCloseDeleteConfirmModal: jest.fn(), onMoveSectionDialog: jest.fn(), onCloseMoveSectionDialog: jest.fn(), - getValidationError: jest.fn(), }; const render = ({ ...props }) => @@ -170,6 +168,15 @@ describe("SectionEditor", () => { requiredCompleted: false, showOnHub: false, sectionSummary: true, + validationErrorInfo: { + errors: [ + { + type: "section", + field: "title", + errorCode: "ERR_REQUIRED_WHEN_SETTING", + }, + ], + }, questionnaire: { id: "2", navigation: false, @@ -177,20 +184,13 @@ describe("SectionEditor", () => { collapsibleSummary: false, }, }; - const getValidationError = jest.fn().mockReturnValue("Validation error"); - - const wrapper = render({ section, getValidationError }); + const wrapper = render({ section }); expect( wrapper .find("[testSelector='txt-section-title']") .prop("errorValidationMsg") - ).toBe("Validation error"); - - expect(getValidationError).toHaveBeenCalledWith({ - field: "title", - message: sectionErrors.SECTION_TITLE_NOT_ENTERED, - }); + ).toEqual(["Enter a section title"]); }); it("should not autofocus the section title when its empty and navigation has just been turned on", () => { @@ -224,24 +224,26 @@ describe("SectionEditor", () => { }); it("should show an error when there is a validation error", () => { - const getValidationError = jest.fn().mockReturnValue("Validation error"); const wrapper = render({ section: { ...section1, title: "", + validationErrorInfo: { + errors: [ + { + type: "section", + field: "title", + errorCode: "ERR_REQUIRED_WHEN_SETTING", + }, + ], + }, }, - getValidationError, }); expect( wrapper .find("[testSelector='txt-section-title']") .prop("errorValidationMsg") - ).toBe("Validation error"); - - expect(getValidationError).toHaveBeenCalledWith({ - field: "title", - message: sectionErrors.SECTION_TITLE_NOT_ENTERED, - }); + ).toEqual(["Enter a section title"]); }); describe("DeleteConfirmDialog", () => { diff --git a/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap b/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap index feda4e9a76..724d1e6a90 100644 --- a/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap +++ b/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap @@ -25,10 +25,13 @@ exports[`SectionEditor should render 1`] = ` controls={ Object { "emphasis": true, + "piping": true, } } disabled={false} + errorValidationMsg={null} id="section-title" + isRepeatingSection={false} label={ } + listId={null} maxHeight={12} multiline={false} name="title" @@ -48,6 +52,8 @@ exports[`SectionEditor should render 1`] = ` } + listId={null} maxHeight={12} multiline={false} name="title" @@ -170,6 +180,8 @@ exports[`SectionEditor should render 2`] = ` get(section, ["questionnaire", "navigation"]); +const getMultipleErrorsByField = (field, errorMessages, validationErrors) => { + const errorArray = validationErrors.filter((error) => error.field === field); + const errMsgArray = errorArray.map( + (error) => errorMessages[error?.errorCode] || error?.errorCode + ); + + if (!errMsgArray.length) { + return null; + } + return errMsgArray; +}; + export class SectionEditor extends React.Component { static propTypes = { section: propType(sectionFragment), @@ -164,36 +174,28 @@ export class SectionEditor extends React.Component { size="large" testSelector="txt-section-title" autoFocus={autoFocusTitle} - errorValidationMsg={ - section && - this.props.getValidationError({ - field: "title", - message: sectionErrors.SECTION_TITLE_NOT_ENTERED, - }) - } + listId={section.repeatingSectionListId} + isRepeatingSection={section.repeatingSection} + errorValidationMsg={getMultipleErrorsByField( + "title", + sectionErrors.TITLE, + section?.validationErrorInfo?.errors + )} /> { const [pickerContent, setPickerContent] = useState(ANSWER); const [contentTypes, setContentTypes] = useState([ANSWER]); @@ -60,7 +62,12 @@ const PipingMenu = ({ setPickerContent(pickerContent); const tempContentTypes = [pickerContent]; if (pickerContent === ANSWER) { - if (some(questionnaire?.collectionLists?.lists, { id: listId })) { + if (isRepeatingSection) { + tempContentTypes.push(LIST_ANSWER); + setPickerContent(LIST_ANSWER); + const answerContentTypeIndex = tempContentTypes.indexOf(ANSWER); + tempContentTypes.splice(answerContentTypeIndex, 1); + } else if (some(questionnaire?.collectionLists?.lists, { id: listId })) { tempContentTypes.push(LIST_ANSWER); } } diff --git a/eq-author/src/components/RichTextEditor/Toolbar.js b/eq-author/src/components/RichTextEditor/Toolbar.js index 4e924afb62..c37639a989 100644 --- a/eq-author/src/components/RichTextEditor/Toolbar.js +++ b/eq-author/src/components/RichTextEditor/Toolbar.js @@ -102,6 +102,7 @@ class ToolBar extends React.Component { linkLimit: PropTypes.number, allCalculatedSummaryPages: PropTypes.array, //eslint-disable-line listId: PropTypes.string, + isRepeatingSection: PropTypes.bool, }; renderButton = (button) => { @@ -138,6 +139,7 @@ class ToolBar extends React.Component { linkLimit, allCalculatedSummaryPages, listId, + isRepeatingSection, } = this.props; const isPipingDisabled = !(piping && selectionIsCollapsed); @@ -170,6 +172,7 @@ class ToolBar extends React.Component { defaultTab={defaultTab} allCalculatedSummaryPages={allCalculatedSummaryPages} listId={listId} + isRepeatingSection={isRepeatingSection} /> )} diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index 36bb479f1c..d595305eb8 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -177,6 +177,7 @@ class RichTextEditor extends React.Component { linkCount: PropTypes.number, linkLimit: PropTypes.number, withoutMargin: PropTypes.bool, + isRepeatingSection: PropTypes.bool, allCalculatedSummaryPages: PropTypes.array, //eslint-disable-line }; @@ -483,6 +484,7 @@ class RichTextEditor extends React.Component { linkLimit, withoutMargin, allCalculatedSummaryPages, + isRepeatingSection, ...otherProps } = this.props; @@ -527,6 +529,7 @@ class RichTextEditor extends React.Component { linkCount={linkCount} linkLimit={linkLimit} allCalculatedSummaryPages={allCalculatedSummaryPages} + isRepeatingSection={isRepeatingSection} {...otherProps} /> diff --git a/eq-author/src/constants/validationMessages.js b/eq-author/src/constants/validationMessages.js index 527a70816b..08af755f93 100644 --- a/eq-author/src/constants/validationMessages.js +++ b/eq-author/src/constants/validationMessages.js @@ -149,10 +149,19 @@ export const questionDefinitionErrors = { }; export const sectionErrors = { - SECTION_TITLE_NOT_ENTERED: "Enter a section title", - SUMMARY_TITLE_NOT_ENTERED: "Enter a summary title", - SECTION_INTRO_TITLE_NOT_ENTERED: "Enter an introduction title", - SECTION_INTRO_CONTENT_NOT_ENTERED: "Enter introduction content", + TITLE: { + ERR_REQUIRED_WHEN_SETTING: "Enter a section title", + SUMMARY_TITLE_NOT_ENTERED: "Enter a summary title", + PIPING_TITLE_DELETED: "The answer being piped has been deleted", + }, + INTRO_TITLE: { + ERR_VALID_REQUIRED: "Enter an introduction title", + PIPING_TITLE_DELETED: "The answer being piped has been deleted", + }, + INTRO_CONTENT: { + ERR_VALID_REQUIRED: "Enter introduction content", + PIPING_TITLE_DELETED: "The answer being piped has been deleted", + }, }; export const listErrors = {