diff --git a/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js b/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js index 90dc9c655b..0b32242675 100644 --- a/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js +++ b/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js @@ -61,6 +61,10 @@ export const UtilityBtns = styled.div` } `; +const StyledIconText = styled(IconText)` + line-height: 1.2; +`; + export const UnwrappedMainNavigation = ({ hasQuestionnaire, totalErrorCount, @@ -195,9 +199,9 @@ export const UnwrappedMainNavigation = ({ title === "QCodes" || totalErrorCount > 0 || !qcodesEnabled } > - - QCodes - + + Q Codes and values + {qcodesEnabled && hasQCodeError && ( )} diff --git a/eq-author/src/App/qcodes/QCodesTable/index.js b/eq-author/src/App/qcodes/QCodesTable/index.js index caf84bed32..b9238cdd24 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.js @@ -47,8 +47,16 @@ import { DRIVING, ANOTHER } from "constants/list-answer-types"; import { QCODE_IS_NOT_UNIQUE, QCODE_REQUIRED, + VALUE_IS_NOT_UNIQUE, + VALUE_REQUIRED, } from "constants/validationMessages"; +import { + getPageByAnswerId, + getAnswerByOptionId, +} from "utils/questionnaireUtils"; +import { useQuestionnaire } from "components/QuestionnaireContext"; + const SpacedTableColumn = styled(TableColumn)` padding: 0.5em 0.5em 0.2em; color: ${colors.text}; @@ -73,7 +81,7 @@ const StyledTableBody = styled(TableBody)` background-color: white; `; -const QcodeValidationError = styled(ValidationError)` +const StyledValidationError = styled(ValidationError)` justify-content: unset; margin: 0; padding-top: 0.2em; @@ -115,13 +123,16 @@ const Row = memo((props) => { questionShortCode, label, qCode: initialQcode, + value: initialValue, type, errorMessage, + valueErrorMessage, option, secondary, listAnswerType, drivingQCode, anotherQCode, + hideOptionValue, } = props; // Uses different initial QCode depending on the QCode defined in the props @@ -137,6 +148,10 @@ const Row = memo((props) => { const [updateListCollector] = useMutation(UPDATE_LIST_COLLECTOR_PAGE, { refetchQueries: ["GetQuestionnaire"], }); + const [value, setValue] = useState(initialValue); + const [updateValue] = useMutation(UPDATE_OPTION_QCODE, { + refetchQueries: ["GetQuestionnaire"], + }); const handleBlur = useCallback( (qCode) => { @@ -170,6 +185,13 @@ const Row = memo((props) => { ] ); + const handleBlurOptionValue = useCallback( + (value) => { + updateValue(mutationVariables({ id, value })); + }, + [id, updateValue] + ); + return ( {questionShortCode || questionTitle ? ( @@ -188,7 +210,12 @@ const Row = memo((props) => { {TYPE_TO_DESCRIPTION[type]} {stripHtmlToText(label)} {dataVersion === "3" ? ( - [CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) ? ( + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ? ( ) : ( @@ -204,11 +231,16 @@ const Row = memo((props) => { aria-label="QCode input field" /> {errorMessage && ( - {errorMessage} + {errorMessage} )} ) - ) : [CHECKBOX, RADIO_OPTION, SELECT_OPTION].includes(type) ? ( + ) : [ + CHECKBOX, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ? ( ) : ( @@ -222,9 +254,34 @@ const Row = memo((props) => { aria-label="QCode input field" /> {errorMessage && ( - {errorMessage} + {errorMessage} + )} + + )} + {dataVersion === "3" && + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) && + !hideOptionValue ? ( + + setValue(e.value)} + onBlur={() => handleBlurOptionValue(value)} + hasError={Boolean(valueErrorMessage)} + aria-label="Option Value input field" + /> + {valueErrorMessage && ( + {valueErrorMessage} )} + ) : ( + )} ); @@ -237,35 +294,89 @@ Row.propTypes = { questionShortCode: PropTypes.string, label: PropTypes.string, qCode: PropTypes.string, + value: PropTypes.string, type: PropTypes.string, qCodeCheck: PropTypes.func, errorMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + valueErrorMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), secondary: PropTypes.bool, option: PropTypes.bool, listAnswerType: PropTypes.string, drivingQCode: PropTypes.string, anotherQCode: PropTypes.string, + hideOptionValue: PropTypes.bool, }; export const QCodeTable = () => { - const { answerRows, duplicatedQCodes, dataVersion } = useQCodeContext(); + const { questionnaire } = useQuestionnaire(); + const { answerRows, duplicatedQCodes, dataVersion, duplicatedOptionValues } = + useQCodeContext(); const getErrorMessage = (qCode) => (!qCode && QCODE_REQUIRED) || (duplicatedQCodes.includes(qCode) && QCODE_IS_NOT_UNIQUE); + const getValueErrorMessage = (value, idValue) => + (!value && VALUE_REQUIRED) || + (duplicatedOptionValues.includes(idValue) && VALUE_IS_NOT_UNIQUE); + + let currentQuestionId = ""; + let idValue = ""; return ( - - Short code - Question - Type - Answer label - Qcode - + {dataVersion === "3" ? ( + + Short code + Question + Answer Type + Answer label + + Q code for answer type + + + Value for checkbox, radio and select answer labels + + + ) : ( + + Short code + Question + Answer Type + Answer label + + Q code for answer type + + + )} {answerRows?.map((item, index) => { + if ( + ![ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(item.type) + ) { + currentQuestionId = item.id ? item.id : ""; + } + if ([MUTUALLY_EXCLUSIVE_OPTION].includes(item.type)) { + const answer = getAnswerByOptionId(questionnaire, item.id); + const page = getPageByAnswerId(questionnaire, answer.id); + currentQuestionId = page.answers[0]?.id; + } + if ( + item.value && + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(item.type) + ) { + idValue = currentQuestionId.concat(item.value); + } if ( item.additionalAnswer && (dataVersion === "3" || item.type !== "CheckboxOption") @@ -277,12 +388,14 @@ export const QCodeTable = () => { dataVersion={dataVersion} {...item} errorMessage={getErrorMessage(item.qCode)} + valueErrorMessage={getValueErrorMessage(item.value, idValue)} /> ); @@ -293,8 +406,9 @@ export const QCodeTable = () => { dataVersion={dataVersion} {...item} errorMessage={getErrorMessage( - item.qCode ?? item.drivingQCode ?? item.anotherQCode // Uses a different QCode depending on the QCode defined in item + item.qCode ?? item.drivingQCode ?? item.anotherQCode )} + valueErrorMessage={getValueErrorMessage(item.value, idValue)} /> ); } diff --git a/eq-author/src/App/qcodes/QCodesTable/index.test.js b/eq-author/src/App/qcodes/QCodesTable/index.test.js index 0b0f974879..7b51247d7b 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.test.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.test.js @@ -105,7 +105,7 @@ const textSetup = () => { }; const optionsSetup = (dataVersion) => { - const questionnaire = buildQuestionnaire({ answerCount: 2 }); + const questionnaire = buildQuestionnaire({ answerCount: 3 }); Object.assign(questionnaire.sections[0].folders[0].pages[0], { alias: "multiple-choice-answer-types-alias", title: "

Multiple choice answer types

", @@ -138,19 +138,44 @@ const optionsSetup = (dataVersion) => { id: "checkbox-option-1-id", label: "checkbox-option-1-label", qCode: "option-1", + value: "option-1", }, { id: "checkbox-option-2-id", label: "checkbox-option-2-label", qCode: "option-2", + value: "option-2", }, ], - mutuallyExclusiveOption: { - id: "checkbox-option-3-id", - label: "Mutually-exclusive-option-label", - mutuallyExclusive: true, - qCode: "mutually-exclusive-option", - }, + }) + ); + + Object.assign( + questionnaire.sections[0].folders[0].pages[0].answers[2], + (questionnaire.dataVersion = dataVersion), + generateAnswer({ + qCode: "mutually-exclusive-1", + label: "mutually-exclusive-1-label", + type: "MutuallyExclusive", + options: [ + { + qCode: "", + description: null, + label: "OR1", + additionalAnswer: null, + id: "or1", + value: "m1-value", + }, + { + qCode: "", + description: null, + label: "OR2", + additionalAnswer: null, + id: "or2", + value: "m1-value", + }, + ], + id: "mutually-exclusive-option-id", }) ); @@ -189,9 +214,9 @@ describe("Qcode Table", () => { const fieldHeadings = [ "Short code", "Question", - "Type", + "Answer Type", "Answer label", - "Qcode", + "Q code for answer type", ]; fieldHeadings.forEach((heading) => expect(getByText(heading)).toBeTruthy()); }); @@ -205,7 +230,7 @@ describe("Qcode Table", () => { const questionnaire = buildQuestionnaire({ answerCount: 1 }); questionnaire.sections[0].folders[0].pages[0].answers[0].qCode = ""; const { getAllByText } = renderWithContext({ questionnaire }); - expect(getAllByText("Qcode required")).toBeTruthy(); + expect(getAllByText("Q code required")).toBeTruthy(); }); it("should not save qCode if it is the same as the initial qCode", () => { @@ -601,15 +626,15 @@ describe("Qcode Table", () => { describe("options", () => { it("should display type", () => { expect(utils.getAllByText(/Checkbox option/)).toHaveLength(2); - expect(utils.getByText(/Mutually exclusive/)).toBeVisible(); + expect( + utils.getAllByText(/Mutually exclusive option/) + ).toHaveLength(2); }); it("should display answer label", () => { expect(utils.getByText(/checkbox-option-1-label/)).toBeVisible(); expect(utils.getByText(/checkbox-option-2-label/)).toBeVisible(); - expect( - utils.getByText(/Mutually-exclusive-option-label/) - ).toBeVisible(); + expect(utils.getByText(/mutually-exclusive-1-label/)).toBeVisible(); }); it("should display answer qCode", () => { @@ -620,8 +645,8 @@ describe("Qcode Table", () => { utils.getByTestId("checkbox-option-2-id-test-input").value ).toEqual("option-2"); expect( - utils.getByTestId("checkbox-option-3-id-test-input").value - ).toEqual("mutually-exclusive-option"); + utils.getByTestId("mutually-exclusive-option-id-test-input").value + ).toEqual("mutually-exclusive-1"); }); it("should save qCode for option", () => { @@ -643,17 +668,26 @@ describe("Qcode Table", () => { it("should save qCode for mutually exclusive option", () => { fireEvent.change( - utils.getByTestId("checkbox-option-3-id-test-input"), + utils.getByTestId("mutually-exclusive-option-id-test-input"), { target: { value: "187" }, } ); + fireEvent.change( + utils.getByTestId("mutually-exclusive-option-id-test-input"), + { + target: { value: "mutually-exclusive-new-1" }, + } + ); fireEvent.blur( - utils.getByTestId("checkbox-option-3-id-test-input") + utils.getByTestId("mutually-exclusive-option-id-test-input") ); expect(mock).toHaveBeenCalledWith({ variables: { - input: { id: "checkbox-option-3-id", qCode: "187" }, + input: { + id: "mutually-exclusive-option-id", + qCode: "mutually-exclusive-new-1", + }, }, }); }); @@ -670,22 +704,22 @@ describe("Qcode Table", () => { utils = optionsSetup("3"); }); - it("should display answer qCodes without option qCodes for checkbox answers in data version 3", () => { + it("should display answer qCodes and option values for checkbox answers in data version 3", () => { expect( - utils.queryByTestId("checkbox-option-1-id-test-input") - ).not.toBeInTheDocument(); + utils.queryByTestId("checkbox-option-1-id-value-test-input") + ).toBeInTheDocument(); expect( - utils.queryByTestId("checkbox-option-2-id-test-input") - ).not.toBeInTheDocument(); + utils.queryByTestId("checkbox-option-2-id-value-test-input") + ).toBeInTheDocument(); expect( utils.getByTestId("checkbox-answer-id-test-input") ).toBeInTheDocument(); expect( - utils.getByTestId("checkbox-option-3-id-test-input").value - ).toEqual("mutually-exclusive-option"); + utils.getByTestId("mutually-exclusive-option-id-test-input").value + ).toEqual("mutually-exclusive-1"); }); it("should save qCode for checkbox answer", () => { @@ -707,7 +741,7 @@ describe("Qcode Table", () => { questionnaire.sections[0].folders[0].pages[0].answers[0].qCode = ""; questionnaire.dataVersion = "3"; const { getAllByText } = renderWithContext({ questionnaire }); - expect(getAllByText("Qcode required")).toBeTruthy(); + expect(getAllByText("Q code required")).toBeTruthy(); }); it("should render a validation error when duplicate qCodes are present in data version 3", () => { @@ -740,7 +774,7 @@ describe("Qcode Table", () => { questionnaire.sections[0].folders[0].pages[0].answers[0].options[0] = option; const { getAllByText } = renderWithContext({ questionnaire }); - expect(getAllByText("Qcode required")).toBeTruthy(); + expect(getAllByText("Q code required")).toBeTruthy(); }); it("should map qCode rows when additional answer is set to true and answer type is not checkbox option", () => { @@ -766,7 +800,7 @@ describe("Qcode Table", () => { questionnaire.sections[0].folders[0].pages[0].answers[0].options[0] = option; const { getAllByText } = renderWithContext({ questionnaire }); - expect(getAllByText("Qcode required")).toBeTruthy(); + expect(getAllByText("Q code required")).toBeTruthy(); }); describe("List collector questions", () => { diff --git a/eq-author/src/App/qcodes/QcodesPage.js b/eq-author/src/App/qcodes/QcodesPage.js index a1bc220d1e..38766ae8ba 100644 --- a/eq-author/src/App/qcodes/QcodesPage.js +++ b/eq-author/src/App/qcodes/QcodesPage.js @@ -7,6 +7,8 @@ import { Grid } from "components/Grid"; import { colors } from "constants/theme"; import MainCanvas from "components/MainCanvas"; import QcodesTable from "./QCodesTable"; +import { InformationPanel } from "components/Panel"; +import Panel from "components-themed/panels"; const Container = styled.div` display: flex; @@ -17,7 +19,7 @@ const Container = styled.div` const StyledGrid = styled(Grid)` overflow: hidden; - padding-top: 2em; + padding-top: 0em; &:focus-visible { border: 3px solid ${colors.focus}; margin: 0; @@ -30,9 +32,28 @@ const StyledMainCanvas = styled(MainCanvas)` max-width: 80em; `; +const Padding = styled.div` + margin: 2em auto 1em; + width: 100%; + padding: 0 0.5em 0 1em; + max-width: 80em; +`; + const QcodesPage = () => ( -
+
+ + + Unique Q codes must be assigned to each answer type. +

+ Unique values must be assigned to allow downstream processing of + checkbox, radio and select answer labels. +
+ + For live or ongoing surveys, only change the Q code or value if the + context of the question or answer label has changed. + +
diff --git a/eq-author/src/App/qcodes/QcodesPage.test.js b/eq-author/src/App/qcodes/QcodesPage.test.js index fa8f6787d2..42786a9bac 100644 --- a/eq-author/src/App/qcodes/QcodesPage.test.js +++ b/eq-author/src/App/qcodes/QcodesPage.test.js @@ -4,6 +4,7 @@ import { useMutation } from "@apollo/react-hooks"; import QcodesPage from "./QcodesPage"; import { QCodeContextProvider } from "components/QCodeContext"; import { buildQuestionnaire } from "tests/utils/createMockQuestionnaire"; +import Theme from "contexts/themeContext"; jest.mock("@apollo/react-hooks", () => ({ useMutation: jest.fn(), @@ -14,14 +15,14 @@ useMutation.mockImplementation(jest.fn(() => [jest.fn()])); describe("Qcodes Page", () => { const questionnaire = buildQuestionnaire({ answerCount: 1 }); - const renderQcodesPage = () => + const renderQcodesPage = (component) => render( - + {component} ); it("should render Qcodes page", () => { - const { getByTestId } = renderQcodesPage(); + const { getByTestId } = renderQcodesPage(); expect(getByTestId("qcodes-page-container")).toBeInTheDocument(); }); }); diff --git a/eq-author/src/components/Panel/index.js b/eq-author/src/components/Panel/index.js index c7db01b58a..2b5f2cb072 100644 --- a/eq-author/src/components/Panel/index.js +++ b/eq-author/src/components/Panel/index.js @@ -31,7 +31,7 @@ const InformationPanel = ({ children }) => { ); }; InformationPanel.propTypes = { - children: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, }; export { Panel, InformationPanel }; diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index 2c34299e78..3fd685858d 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -1,7 +1,7 @@ import React, { createContext, useContext, useMemo } from "react"; import PropTypes from "prop-types"; import CustomPropTypes from "custom-prop-types"; -import { getPages } from "utils/questionnaireUtils"; +import { getPages, getPageByAnswerId } from "utils/questionnaireUtils"; import { RADIO_OPTION, @@ -10,7 +10,9 @@ import { RADIO, MUTUALLY_EXCLUSIVE, ANSWER_OPTION_TYPES, + SELECT, SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, } from "constants/answer-types"; import { @@ -38,11 +40,14 @@ export const flattenAnswer = (answer) => option: true, } ) ?? []), - answer.mutuallyExclusiveOption && { - ...answer.mutuallyExclusiveOption, - type: "MutuallyExclusiveOption", - option: true, - }, + ...(answer.options?.map( + (option) => + answer.type === MUTUALLY_EXCLUSIVE && { + ...option, + type: "MutuallyExclusiveOption", + option: true, + } + ) ?? []), answer.secondaryLabel && { ...answer, label: answer.secondaryLabel, @@ -64,11 +69,13 @@ const formatListCollector = (listCollectorPage) => [ label: listCollectorPage.drivingPositive, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, { label: listCollectorPage.drivingNegative, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, { id: listCollectorPage.id, @@ -82,11 +89,13 @@ const formatListCollector = (listCollectorPage) => [ label: listCollectorPage.anotherPositive, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, { label: listCollectorPage.anotherNegative, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, ]; @@ -181,7 +190,12 @@ const getEmptyQCodes = (answerRows, dataVersion) => { return answerRows?.find( ({ qCode, drivingQCode, anotherQCode, type }) => !(qCode || drivingQCode || anotherQCode) && - ![CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) + ![ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ); } // If dataVersion is not 3, checkbox answers and radio options do not have QCodes, and therefore these can be empty @@ -189,11 +203,72 @@ const getEmptyQCodes = (answerRows, dataVersion) => { else { return answerRows?.find( ({ qCode, type }) => - !qCode && ![CHECKBOX, RADIO_OPTION, SELECT_OPTION].includes(type) + !qCode && + ![ + CHECKBOX, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ); } }; +// getDuplicatedOptionValues :: [AnswerRow] -> [Value] +// Return an array of Values which are duplicated within an answer in the given list of answer rows +export const getDuplicatedOptionValues = (flattenedAnswers, questionnaire) => { + // acc - accumulator + let currentQuestionId = ""; + let idValue = ""; + const optionValueUsageMap = flattenedAnswers?.reduce( + (acc, { value, type, id }) => { + if ([RADIO, CHECKBOX, SELECT].includes(type)) { + currentQuestionId = id; + } + + if ([MUTUALLY_EXCLUSIVE].includes(type)) { + const page = getPageByAnswerId(questionnaire, id); + currentQuestionId = page.answers[0]?.id; + } + + if ( + value && + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) + ) { + idValue = currentQuestionId.concat(value); + const currentValue = acc.get(idValue); + acc.set(idValue, currentValue ? currentValue + 1 : 1); + } + return acc; + }, + new Map() + ); + + return Array.from(optionValueUsageMap).reduce( + (acc, [value, count]) => (count > 1 ? [...acc, value] : acc), + [] + ); +}; + +const getEmptyOptionValues = (answerRows) => { + return answerRows?.find( + ({ value, type, hideOptionValue }) => + !value && + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) && + !hideOptionValue + ); +}; + export const QCodeContextProvider = ({ questionnaire = {}, children }) => { const answerRows = useMemo( () => getFlattenedAnswerRows(questionnaire) ?? [], @@ -205,9 +280,19 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { [answerRows, questionnaire] ); + const duplicatedOptionValues = useMemo( + () => getDuplicatedOptionValues(answerRows, questionnaire) ?? [], + [answerRows, questionnaire] + ); + const hasQCodeError = duplicatedQCodes?.length || - getEmptyQCodes(answerRows, questionnaire.dataVersion); + getEmptyQCodes(answerRows, questionnaire.dataVersion) || + (questionnaire.dataVersion === "3" && duplicatedOptionValues?.length) || + (questionnaire.dataVersion === "3" && getEmptyOptionValues(answerRows)); + + const hasOptionValueError = + duplicatedOptionValues?.length || getEmptyOptionValues(answerRows); const dataVersion = questionnaire?.dataVersion; @@ -217,8 +302,17 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { duplicatedQCodes, dataVersion, hasQCodeError, + duplicatedOptionValues, + hasOptionValueError, }), - [answerRows, duplicatedQCodes, dataVersion, hasQCodeError] + [ + answerRows, + duplicatedQCodes, + dataVersion, + hasQCodeError, + duplicatedOptionValues, + hasOptionValueError, + ] ); return ( diff --git a/eq-author/src/constants/validationMessages.js b/eq-author/src/constants/validationMessages.js index c305e700b6..a0941bc935 100644 --- a/eq-author/src/constants/validationMessages.js +++ b/eq-author/src/constants/validationMessages.js @@ -224,8 +224,12 @@ export const dynamicAnswer = { ERR_REFERENCE_MOVED: "Answer must be from a previous question", }; -export const QCODE_IS_NOT_UNIQUE = "Qcode must be unique"; -export const QCODE_REQUIRED = "Qcode required"; +export const QCODE_IS_NOT_UNIQUE = + "This Q code has been assigned to another answer type. Enter a unique Q code."; +export const QCODE_REQUIRED = "Q code required"; +export const VALUE_IS_NOT_UNIQUE = + "This value has been assigned to another option for this answer type. Enter a unique value."; +export const VALUE_REQUIRED = "Value required"; export const QUESTION_ANSWER_NOT_SELECTED = "Answer required"; export const CALCSUM_ANSWER_NOT_SELECTED = "Select at least two answers to be calculated"; diff --git a/eq-author/src/graphql/lists/listAnswer.graphql b/eq-author/src/graphql/lists/listAnswer.graphql index 76be5d2416..2ab419ec51 100644 --- a/eq-author/src/graphql/lists/listAnswer.graphql +++ b/eq-author/src/graphql/lists/listAnswer.graphql @@ -80,6 +80,7 @@ fragment ListAnswer on Answer { mutuallyExclusive label description + value validationErrorInfo { ...ValidationErrorInfo } diff --git a/eq-author/src/utils/questionnaireUtils/index.js b/eq-author/src/utils/questionnaireUtils/index.js index 26929a1a44..3bc75394a0 100644 --- a/eq-author/src/utils/questionnaireUtils/index.js +++ b/eq-author/src/utils/questionnaireUtils/index.js @@ -36,6 +36,11 @@ export const getPageByAnswerId = (questionnaire, id) => getPages(questionnaire)?.find(({ answers }) => answers?.some((answer) => answer.id === id) ); +export const getAnswerByOptionId = (questionnaire, id) => + getAnswers(questionnaire)?.find( + (answer) => + answer.options && answer.options?.some((option) => option.id === id) + ); export const getPageByConfirmationId = (questionnaire, id) => getPages(questionnaire)?.find(({ confirmation }) => confirmation.id === id);