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);