From 1ea6e0c575e6f4840391c6a1e032a71e57f40411 Mon Sep 17 00:00:00 2001 From: farres1 Date: Tue, 16 Jan 2024 12:00:21 +0000 Subject: [PATCH 01/10] Add piping to repeating section title --- eq-author/src/App/section/Design/SectionEditor/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eq-author/src/App/section/Design/SectionEditor/index.js b/eq-author/src/App/section/Design/SectionEditor/index.js index 5563db9483..08c755acc9 100644 --- a/eq-author/src/App/section/Design/SectionEditor/index.js +++ b/eq-author/src/App/section/Design/SectionEditor/index.js @@ -36,6 +36,7 @@ import { const titleControls = { emphasis: true, + piping: true, }; const Padding = styled.div` @@ -164,6 +165,7 @@ export class SectionEditor extends React.Component { size="large" testSelector="txt-section-title" autoFocus={autoFocusTitle} + listId={section.repeatingSectionListId} errorValidationMsg={ section && this.props.getValidationError({ From a51f66a107af240417fe658efa5d4775aaf3f6ad Mon Sep 17 00:00:00 2001 From: farres1 Date: Wed, 17 Jan 2024 08:24:52 +0000 Subject: [PATCH 02/10] Display error when piping an answer and section is not repeating --- eq-author-api/src/businessLogic/onSectionUpdated.js | 5 +++++ eq-author-api/src/validation/schemas/section.json | 6 ++++++ eq-author/src/App/section/Design/SectionEditor/index.js | 5 ++++- eq-author/src/constants/validationMessages.js | 3 ++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/eq-author-api/src/businessLogic/onSectionUpdated.js b/eq-author-api/src/businessLogic/onSectionUpdated.js index 3a24b7b588..6812d5df88 100644 --- a/eq-author-api/src/businessLogic/onSectionUpdated.js +++ b/eq-author-api/src/businessLogic/onSectionUpdated.js @@ -5,6 +5,8 @@ const { const { flatMap } = require("lodash"); const cheerio = require("cheerio"); +const { getSections } = require("../../schema/resolvers/utils/sectionGetters"); + const updatePipingValue = (htmlText, answerId, newValue) => { if (!htmlText) { return htmlText; @@ -71,6 +73,7 @@ module.exports = (ctx, section, oldSection) => { section?.repeatingSectionListId ); const pages = flatMap(section?.folders, "pages"); + const sections = getSections(ctx); if ( (!section.repeatingSection && oldSection.repeatingSection) || @@ -79,6 +82,7 @@ module.exports = (ctx, section, oldSection) => { ) { if (oldList) { deletePiping(oldList.answers, section, pages); + deletePiping(oldList.answers, section, sections); } } @@ -90,6 +94,7 @@ module.exports = (ctx, section, oldSection) => { ) { if (newList) { updatePiping(newList.answers, section, pages); + updatePiping(newList.answers, section, sections); } } }; 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/section/Design/SectionEditor/index.js b/eq-author/src/App/section/Design/SectionEditor/index.js index 08c755acc9..99fa3cf8dc 100644 --- a/eq-author/src/App/section/Design/SectionEditor/index.js +++ b/eq-author/src/App/section/Design/SectionEditor/index.js @@ -170,7 +170,10 @@ export class SectionEditor extends React.Component { section && this.props.getValidationError({ field: "title", - message: sectionErrors.SECTION_TITLE_NOT_ENTERED, + message: + sectionErrors[ + section?.validationErrorInfo?.errors[0]?.errorCode + ], }) } /> diff --git a/eq-author/src/constants/validationMessages.js b/eq-author/src/constants/validationMessages.js index 527a70816b..0bef110a87 100644 --- a/eq-author/src/constants/validationMessages.js +++ b/eq-author/src/constants/validationMessages.js @@ -149,10 +149,11 @@ export const questionDefinitionErrors = { }; export const sectionErrors = { - SECTION_TITLE_NOT_ENTERED: "Enter a section title", + ERR_REQUIRED_WHEN_SETTING: "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", + PIPING_TITLE_DELETED: "The answer being piped has been deleted", }; export const listErrors = { From 739f992855392725715d68f60cb52a74e786ece3 Mon Sep 17 00:00:00 2001 From: farres1 Date: Wed, 17 Jan 2024 09:59:06 +0000 Subject: [PATCH 03/10] Remove answer radio option when piping answers into repeating section title --- eq-author/src/App/section/Design/SectionEditor/index.js | 1 + eq-author/src/components/RichTextEditor/PipingMenu.js | 9 ++++++++- eq-author/src/components/RichTextEditor/Toolbar.js | 3 +++ eq-author/src/components/RichTextEditor/index.js | 3 +++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/eq-author/src/App/section/Design/SectionEditor/index.js b/eq-author/src/App/section/Design/SectionEditor/index.js index 99fa3cf8dc..aae2cbf808 100644 --- a/eq-author/src/App/section/Design/SectionEditor/index.js +++ b/eq-author/src/App/section/Design/SectionEditor/index.js @@ -166,6 +166,7 @@ export class SectionEditor extends React.Component { testSelector="txt-section-title" autoFocus={autoFocusTitle} listId={section.repeatingSectionListId} + isRepeatingSection={section.repeatingSection} errorValidationMsg={ section && this.props.getValidationError({ diff --git a/eq-author/src/components/RichTextEditor/PipingMenu.js b/eq-author/src/components/RichTextEditor/PipingMenu.js index 21a787ddef..cc69f1f354 100644 --- a/eq-author/src/components/RichTextEditor/PipingMenu.js +++ b/eq-author/src/components/RichTextEditor/PipingMenu.js @@ -38,6 +38,7 @@ const PipingMenuPropTypes = { allCalculatedSummaryPages: PropTypes.array, //eslint-disable-line listId: PropTypes.string, supplementaryDataId: PropTypes.string, + isRepeatingSection: PropTypes.bool, }; const PipingMenu = ({ @@ -48,6 +49,7 @@ const PipingMenu = ({ allowableTypes = [ANSWER, METADATA], allCalculatedSummaryPages = [], // Default array is empty to disable variable piping button listId, + isRepeatingSection, }) => { 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} /> From 68d6695cb49770ba44c3b9f2435c5352d70fb261 Mon Sep 17 00:00:00 2001 From: martyncolmer Date: Fri, 26 Jan 2024 15:52:08 +0000 Subject: [PATCH 04/10] corrected onSectionUpdated as deleting too much --- .../src/businessLogic/onSectionUpdated.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/eq-author-api/src/businessLogic/onSectionUpdated.js b/eq-author-api/src/businessLogic/onSectionUpdated.js index 6812d5df88..27535f77f1 100644 --- a/eq-author-api/src/businessLogic/onSectionUpdated.js +++ b/eq-author-api/src/businessLogic/onSectionUpdated.js @@ -5,8 +5,6 @@ const { const { flatMap } = require("lodash"); const cheerio = require("cheerio"); -const { getSections } = require("../../schema/resolvers/utils/sectionGetters"); - const updatePipingValue = (htmlText, answerId, newValue) => { if (!htmlText) { return htmlText; @@ -40,6 +38,11 @@ const deletePiping = (answers, section, pages) => { answer.id, "Deleted answer" ); + section.title = updatePipingValue( + section.title, + answer.id, + "Deleted answer" + ); }); }; @@ -56,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" + ); }); }; @@ -73,7 +81,6 @@ module.exports = (ctx, section, oldSection) => { section?.repeatingSectionListId ); const pages = flatMap(section?.folders, "pages"); - const sections = getSections(ctx); if ( (!section.repeatingSection && oldSection.repeatingSection) || @@ -82,7 +89,6 @@ module.exports = (ctx, section, oldSection) => { ) { if (oldList) { deletePiping(oldList.answers, section, pages); - deletePiping(oldList.answers, section, sections); } } @@ -94,7 +100,6 @@ module.exports = (ctx, section, oldSection) => { ) { if (newList) { updatePiping(newList.answers, section, pages); - updatePiping(newList.answers, section, sections); } } }; From 70981b73b393be7ca55c044d35b67b6757143ef0 Mon Sep 17 00:00:00 2001 From: martyncolmer Date: Fri, 26 Jan 2024 16:52:02 +0000 Subject: [PATCH 05/10] added validation message to title field --- .../App/section/Design/SectionEditor/index.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/eq-author/src/App/section/Design/SectionEditor/index.js b/eq-author/src/App/section/Design/SectionEditor/index.js index aae2cbf808..565463be07 100644 --- a/eq-author/src/App/section/Design/SectionEditor/index.js +++ b/eq-author/src/App/section/Design/SectionEditor/index.js @@ -56,6 +56,18 @@ const SectionCanvas = styled.div` const hasNavigation = (section) => get(section, ["questionnaire", "navigation"]); +const getMultipleErrorsByField = (field, validationErrors) => { + const errorArray = validationErrors.filter((error) => error.field === field); + const errMsgArray = errorArray.map( + (error) => sectionErrors[error?.errorCode] || error?.errorCode + ); + + if (!errMsgArray.length) { + return null; + } + return errMsgArray; +}; + export class SectionEditor extends React.Component { static propTypes = { section: propType(sectionFragment), @@ -167,16 +179,10 @@ export class SectionEditor extends React.Component { autoFocus={autoFocusTitle} listId={section.repeatingSectionListId} isRepeatingSection={section.repeatingSection} - errorValidationMsg={ - section && - this.props.getValidationError({ - field: "title", - message: - sectionErrors[ - section?.validationErrorInfo?.errors[0]?.errorCode - ], - }) - } + errorValidationMsg={getMultipleErrorsByField( + "title", + section?.validationErrorInfo?.errors + )} /> Date: Fri, 26 Jan 2024 17:18:17 +0000 Subject: [PATCH 06/10] Added validation messsages --- .../App/section/Design/SectionEditor/index.js | 33 ++++++++----------- eq-author/src/constants/validationMessages.js | 18 +++++++--- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/eq-author/src/App/section/Design/SectionEditor/index.js b/eq-author/src/App/section/Design/SectionEditor/index.js index 565463be07..1aec5a553b 100644 --- a/eq-author/src/App/section/Design/SectionEditor/index.js +++ b/eq-author/src/App/section/Design/SectionEditor/index.js @@ -56,10 +56,10 @@ const SectionCanvas = styled.div` const hasNavigation = (section) => get(section, ["questionnaire", "navigation"]); -const getMultipleErrorsByField = (field, validationErrors) => { +const getMultipleErrorsByField = (field, errorMessages, validationErrors) => { const errorArray = validationErrors.filter((error) => error.field === field); const errMsgArray = errorArray.map( - (error) => sectionErrors[error?.errorCode] || error?.errorCode + (error) => errorMessages[error?.errorCode] || error?.errorCode ); if (!errMsgArray.length) { @@ -181,6 +181,7 @@ export class SectionEditor extends React.Component { isRepeatingSection={section.repeatingSection} errorValidationMsg={getMultipleErrorsByField( "title", + sectionErrors.TITLE, section?.validationErrorInfo?.errors )} /> @@ -188,24 +189,16 @@ export class SectionEditor extends React.Component { Date: Fri, 26 Jan 2024 17:47:55 +0000 Subject: [PATCH 07/10] fixed tests --- .../SectionEditor/SectionEditor.test.js | 40 ++++++++++--------- .../__snapshots__/SectionEditor.test.js.snap | 12 ++++++ .../App/section/Design/SectionEditor/index.js | 5 +-- 3 files changed, 34 insertions(+), 23 deletions(-) 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`] = ` Date: Fri, 26 Jan 2024 18:17:55 +0000 Subject: [PATCH 08/10] updated code to removed deleted and updated list items --- .../src/businessLogic/onAnswerDeleted.js | 5 ++++ .../src/businessLogic/onAnswerUpdated.js | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/eq-author-api/src/businessLogic/onAnswerDeleted.js b/eq-author-api/src/businessLogic/onAnswerDeleted.js index 64e3ad9ea5..d0b7ebda37 100644 --- a/eq-author-api/src/businessLogic/onAnswerDeleted.js +++ b/eq-author-api/src/businessLogic/onAnswerDeleted.js @@ -86,6 +86,11 @@ const removeAnswerFromPiping = (ctx, deletedAnswer, pages) => { 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); }; From a53cc769fccd1b94f9f46284c048d861264aef79 Mon Sep 17 00:00:00 2001 From: martyncolmer Date: Mon, 29 Jan 2024 12:13:57 +0000 Subject: [PATCH 09/10] Added refetch for list deletion --- eq-author/src/App/collectionLists/collectionListsPage.js | 2 ++ 1 file changed, 2 insertions(+) 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"], }); }; From c6b03e0e88b01552c5665816b8dfbe5908870123 Mon Sep 17 00:00:00 2001 From: martyncolmer Date: Mon, 29 Jan 2024 12:42:14 +0000 Subject: [PATCH 10/10] fix for repeating label and answer piping when list deleted --- eq-author-api/src/businessLogic/onAnswerDeleted.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eq-author-api/src/businessLogic/onAnswerDeleted.js b/eq-author-api/src/businessLogic/onAnswerDeleted.js index d0b7ebda37..a12341225d 100644 --- a/eq-author-api/src/businessLogic/onAnswerDeleted.js +++ b/eq-author-api/src/businessLogic/onAnswerDeleted.js @@ -81,6 +81,14 @@ 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);