From 07f0f28666efebead8ec5cd1cafeb25149c84271 Mon Sep 17 00:00:00 2001 From: Joshua Chapman <30293265+jchapman68@users.noreply.github.com> Date: Wed, 23 Aug 2023 11:28:26 +0100 Subject: [PATCH 01/16] Revert "Revert "EAR 1948 Option values for rad-chk-ans"" --- .../MainNavigation/index.js | 10 ++- eq-author/src/App/qcodes/QCodesTable/index.js | 82 ++++++++++++++++--- .../src/App/qcodes/QCodesTable/index.test.js | 26 +++--- eq-author/src/App/qcodes/QcodesPage.js | 25 +++++- eq-author/src/App/qcodes/QcodesPage.test.js | 7 +- eq-author/src/components/Panel/index.js | 2 +- .../src/components/QCodeContext/index.js | 69 +++++++++++++++- eq-author/src/constants/validationMessages.js | 8 +- .../src/graphql/lists/listAnswer.graphql | 1 + 9 files changed, 195 insertions(+), 35 deletions(-) diff --git a/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js b/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js index 90dc9c655b..55a63c011b 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 - + + QCodes 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 ab57c0a305..63ce0237f7 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.js @@ -47,6 +47,8 @@ import { DRIVING, ANOTHER } from "constants/list-answer-types"; import { QCODE_IS_NOT_UNIQUE, QCODE_REQUIRED, + VALUE_IS_NOT_UNIQUE, + VALUE_REQUIRED, } from "constants/validationMessages"; const SpacedTableColumn = styled(TableColumn)` @@ -73,7 +75,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 +117,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 +142,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 +179,13 @@ const Row = memo((props) => { ] ); + const handleBlurOptionValue = useCallback( + (value) => { + updateValue(mutationVariables({ id, value })); + }, + [id, updateValue] + ); + return ( {questionShortCode || questionTitle ? ( @@ -204,7 +220,7 @@ const Row = memo((props) => { aria-label="QCode input field" /> {errorMessage && ( - {errorMessage} + {errorMessage} )} ) @@ -222,9 +238,28 @@ const Row = memo((props) => { aria-label="QCode input field" /> {errorMessage && ( - {errorMessage} + {errorMessage} + )} + + )} + {[CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) && + !hideOptionValue ? ( + + setValue(e.value)} + onBlur={() => handleBlurOptionValue(value)} + hasError={Boolean(valueErrorMessage)} + aria-label="Option Value input field" + /> + {valueErrorMessage && ( + {valueErrorMessage} )} + ) : ( + )} ); @@ -237,35 +272,59 @@ 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 { 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 + Short code + Question + Answer Type + Answer label + Q code for answer type + + Value for checkbox, radio and select answer labels + {answerRows?.map((item, index) => { + if ( + ![CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(item.type) + ) { + currentQuestionId = item.id ? item.id : ""; + } + if ( + item.value && + [CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(item.type) + ) { + idValue = currentQuestionId.concat(item.value); + } if ( item.additionalAnswer && (dataVersion === "3" || item.type !== "CheckboxOption") @@ -277,12 +336,14 @@ export const QCodeTable = () => { dataVersion={dataVersion} {...item} errorMessage={getErrorMessage(item.qCode)} + valueErrorMessage={getValueErrorMessage(item.value, idValue)} /> ); @@ -293,8 +354,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..b1a39c65ff 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.test.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.test.js @@ -138,11 +138,13 @@ 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: { @@ -150,6 +152,7 @@ const optionsSetup = (dataVersion) => { label: "Mutually-exclusive-option-label", mutuallyExclusive: true, qCode: "mutually-exclusive-option", + value: "mutually-exclusive-option", }, }) ); @@ -189,9 +192,10 @@ describe("Qcode Table", () => { const fieldHeadings = [ "Short code", "Question", - "Type", + "Answer Type", "Answer label", - "Qcode", + "Q code for answer type", + "Value for checkbox, radio and select answer labels", ]; fieldHeadings.forEach((heading) => expect(getByText(heading)).toBeTruthy()); }); @@ -205,7 +209,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", () => { @@ -670,14 +674,14 @@ 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") @@ -707,7 +711,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 +744,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 +770,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..624639238f 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; + eidth: 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 a89185d8de..99f074ce95 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -10,6 +10,7 @@ import { RADIO, MUTUALLY_EXCLUSIVE, ANSWER_OPTION_TYPES, + SELECT, SELECT_OPTION, } from "constants/answer-types"; @@ -61,11 +62,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, @@ -79,11 +82,13 @@ const formatListCollector = (listCollectorPage) => [ label: listCollectorPage.anotherPositive, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, { label: listCollectorPage.anotherNegative, type: RADIO_OPTION, option: true, + hideOptionValue: true, }, ]; @@ -184,11 +189,51 @@ const getEmptyQCodes = (answerRows, dataVersion) => { else { return answerRows?.find( ({ qCode, type }) => - !qCode && ![CHECKBOX, RADIO_OPTION, SELECT_OPTION].includes(type) + !qCode && + ![CHECKBOX, CHECKBOX_OPTION, RADIO_OPTION, SELECT_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) => { + // acc - accumulator + let currentQuestionId = ""; + let idValue = ""; + const optionValueUsageMap = flattenedAnswers?.reduce( + (acc, { value, type, id }) => { + if ([RADIO, CHECKBOX, SELECT].includes(type)) { + currentQuestionId = id; + } + if ( + value && + [CHECKBOX_OPTION, RADIO_OPTION, SELECT_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].includes(type) && + !hideOptionValue + ); +}; + export const QCodeContextProvider = ({ questionnaire = {}, children }) => { const answerRows = useMemo( () => getFlattenedAnswerRows(questionnaire) ?? [], @@ -202,7 +247,16 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { const hasQCodeError = duplicatedQCodes?.length || - getEmptyQCodes(answerRows, questionnaire.dataVersion); + getEmptyQCodes(answerRows, questionnaire.dataVersion) || + getEmptyOptionValues(answerRows); + + const duplicatedOptionValues = useMemo( + () => getDuplicatedOptionValues(answerRows) ?? [], + [answerRows] + ); + + const hasOptionValueError = + duplicatedOptionValues?.length || getEmptyOptionValues(answerRows); const dataVersion = questionnaire?.dataVersion; @@ -212,8 +266,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 f5185a7a52..faab3d5495 100644 --- a/eq-author/src/constants/validationMessages.js +++ b/eq-author/src/constants/validationMessages.js @@ -202,8 +202,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 366c51fc72..e13e4e916c 100644 --- a/eq-author/src/graphql/lists/listAnswer.graphql +++ b/eq-author/src/graphql/lists/listAnswer.graphql @@ -79,6 +79,7 @@ fragment ListAnswer on Answer { mutuallyExclusive label description + value validationErrorInfo { ...ValidationErrorInfo } From 25ab5348bd4e4c2c8fbe945e5b479c1de9caa547 Mon Sep 17 00:00:00 2001 From: farres1 Date: Thu, 1 Feb 2024 12:26:55 +0000 Subject: [PATCH 02/16] Add space to Q Codes in main navigation bar --- .../src/App/QuestionnaireDesignPage/MainNavigation/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js b/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js index 55a63c011b..0b32242675 100644 --- a/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js +++ b/eq-author/src/App/QuestionnaireDesignPage/MainNavigation/index.js @@ -200,7 +200,7 @@ export const UnwrappedMainNavigation = ({ } > - QCodes and values + Q Codes and values {qcodesEnabled && hasQCodeError && ( From 88d9a0d8d9f6b11dc234bfe0804135ba1825476b Mon Sep 17 00:00:00 2001 From: farres1 Date: Thu, 1 Feb 2024 12:27:14 +0000 Subject: [PATCH 03/16] Fix width --- eq-author/src/App/qcodes/QcodesPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eq-author/src/App/qcodes/QcodesPage.js b/eq-author/src/App/qcodes/QcodesPage.js index 624639238f..38766ae8ba 100644 --- a/eq-author/src/App/qcodes/QcodesPage.js +++ b/eq-author/src/App/qcodes/QcodesPage.js @@ -34,7 +34,7 @@ const StyledMainCanvas = styled(MainCanvas)` const Padding = styled.div` margin: 2em auto 1em; - eidth: 100%; + width: 100%; padding: 0 0.5em 0 1em; max-width: 80em; `; From 3202e09d753a862fa3c1d1093a403d678a743ba8 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 11:51:11 +0000 Subject: [PATCH 04/16] add mutually exclusive answers in table rows Signed-off-by: sudeep --- eq-author/src/components/QCodeContext/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index 1d4a2bd722..c9cdc894e9 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -39,11 +39,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, From 70dcf4cd211e207dc2a0d35ff506f76ae7f881c9 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 11:51:40 +0000 Subject: [PATCH 05/16] display mutually exclusive options Signed-off-by: sudeep --- eq-author/src/App/qcodes/QCodesTable/index.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/eq-author/src/App/qcodes/QCodesTable/index.js b/eq-author/src/App/qcodes/QCodesTable/index.js index 63ce0237f7..7f4fce5358 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.js @@ -204,7 +204,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) ? ( ) : ( @@ -224,7 +229,12 @@ const Row = memo((props) => { )} ) - ) : [CHECKBOX, RADIO_OPTION, SELECT_OPTION].includes(type) ? ( + ) : [ + CHECKBOX, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ? ( ) : ( @@ -242,8 +252,12 @@ const Row = memo((props) => { )} )} - {[CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) && - !hideOptionValue ? ( + {[ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) && !hideOptionValue ? ( Date: Tue, 19 Mar 2024 11:52:45 +0000 Subject: [PATCH 06/16] update tests for mutually exclusive answers Signed-off-by: sudeep --- .../src/App/qcodes/QCodesTable/index.test.js | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/eq-author/src/App/qcodes/QCodesTable/index.test.js b/eq-author/src/App/qcodes/QCodesTable/index.test.js index b1a39c65ff..bede6eae6e 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

", @@ -147,13 +147,35 @@ const optionsSetup = (dataVersion) => { value: "option-2", }, ], - mutuallyExclusiveOption: { - id: "checkbox-option-3-id", - label: "Mutually-exclusive-option-label", - mutuallyExclusive: true, - qCode: "mutually-exclusive-option", - value: "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: "9d6d013d-64c0-4757-b8e3-f991e2dcfb92", + value: "m1-value", + }, + { + qCode: "", + description: null, + label: "OR2", + additionalAnswer: null, + id: "4b362d2e-1ec0-4cb2-b90d-572be5133ab2", + value: "m1-value", + }, + ], + id: "mutually-exclusive-option-id", }) ); @@ -605,15 +627,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", () => { @@ -624,8 +646,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", () => { @@ -647,17 +669,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", + }, }, }); }); @@ -688,8 +719,8 @@ describe("Qcode Table", () => { ).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", () => { From 05a5b4e9828f6f469e80723dd6ad89f686145269 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 17:41:52 +0000 Subject: [PATCH 07/16] added new utility function Signed-off-by: sudeep --- eq-author/src/utils/questionnaireUtils/index.js | 5 +++++ 1 file changed, 5 insertions(+) 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); From 3d172400e94d4e3ff74a4c46c4b326e55b4531a8 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 17:42:33 +0000 Subject: [PATCH 08/16] corrected version display and validation error Signed-off-by: sudeep --- eq-author/src/App/qcodes/QCodesTable/index.js | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/eq-author/src/App/qcodes/QCodesTable/index.js b/eq-author/src/App/qcodes/QCodesTable/index.js index 7f4fce5358..2cff0df929 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.js @@ -51,6 +51,12 @@ import { 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}; @@ -252,12 +258,14 @@ const Row = memo((props) => { )}
)} - {[ + {dataVersion === "3" && + [ CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION, MUTUALLY_EXCLUSIVE_OPTION, - ].includes(type) && !hideOptionValue ? ( + ].includes(type) && + !hideOptionValue ? ( { + const { questionnaire } = useQuestionnaire(); const { answerRows, duplicatedQCodes, dataVersion, duplicatedOptionValues } = useQCodeContext(); const getErrorMessage = (qCode) => @@ -315,27 +324,56 @@ export const QCodeTable = () => { return (
- - Short code - Question - Answer Type - Answer label - Q code for answer type - - Value for checkbox, radio and select answer labels - - + {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].includes(item.type) + ![ + 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].includes(item.type) + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(item.type) ) { idValue = currentQuestionId.concat(item.value); } From 8366e5a786c4b62d9ef8491b13aeb383e447b4df Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 17:43:19 +0000 Subject: [PATCH 09/16] corrected context to check duplicates Signed-off-by: sudeep --- .../src/components/QCodeContext/index.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index c9cdc894e9..b32527d00e 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, @@ -12,6 +12,7 @@ import { ANSWER_OPTION_TYPES, SELECT, SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, } from "constants/answer-types"; import { @@ -205,7 +206,7 @@ const getEmptyQCodes = (answerRows, dataVersion) => { // 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) => { +export const getDuplicatedOptionValues = (flattenedAnswers, questionnaire) => { // acc - accumulator let currentQuestionId = ""; let idValue = ""; @@ -214,9 +215,20 @@ export const getDuplicatedOptionValues = (flattenedAnswers) => { 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].includes(type) + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ) { idValue = currentQuestionId.concat(value); const currentValue = acc.get(idValue); @@ -259,8 +271,8 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { getEmptyOptionValues(answerRows); const duplicatedOptionValues = useMemo( - () => getDuplicatedOptionValues(answerRows) ?? [], - [answerRows] + () => getDuplicatedOptionValues(answerRows, questionnaire) ?? [], + [answerRows, questionnaire] ); const hasOptionValueError = From de0dfc4f15cc4919fc1b565fcced1e0d8c90fe74 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 19 Mar 2024 18:18:32 +0000 Subject: [PATCH 10/16] corrected test Signed-off-by: sudeep --- eq-author/src/App/qcodes/QCodesTable/index.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/eq-author/src/App/qcodes/QCodesTable/index.test.js b/eq-author/src/App/qcodes/QCodesTable/index.test.js index bede6eae6e..9946524a31 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.test.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.test.js @@ -217,7 +217,6 @@ describe("Qcode Table", () => { "Answer Type", "Answer label", "Q code for answer type", - "Value for checkbox, radio and select answer labels", ]; fieldHeadings.forEach((heading) => expect(getByText(heading)).toBeTruthy()); }); From 1927ec19f40f99e8e5d43ba69edfd6c2a325b5ed Mon Sep 17 00:00:00 2001 From: sudeep Date: Wed, 20 Mar 2024 12:32:09 +0000 Subject: [PATCH 11/16] solved qcodes error badge Signed-off-by: sudeep --- .../src/components/QCodeContext/index.js | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index b32527d00e..21a36e9575 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -190,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 @@ -199,7 +204,13 @@ const getEmptyQCodes = (answerRows, dataVersion) => { return answerRows?.find( ({ qCode, type }) => !qCode && - ![CHECKBOX, CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) + ![ + CHECKBOX, + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) ); } }; @@ -265,16 +276,17 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { [answerRows, questionnaire] ); - const hasQCodeError = - duplicatedQCodes?.length || - getEmptyQCodes(answerRows, questionnaire.dataVersion) || - getEmptyOptionValues(answerRows); - const duplicatedOptionValues = useMemo( () => getDuplicatedOptionValues(answerRows, questionnaire) ?? [], [answerRows, questionnaire] ); + const hasQCodeError = + duplicatedQCodes?.length || + duplicatedOptionValues?.length || + getEmptyQCodes(answerRows, questionnaire.dataVersion) || + getEmptyOptionValues(answerRows); + const hasOptionValueError = duplicatedOptionValues?.length || getEmptyOptionValues(answerRows); From b0ba23c83a682c1ffdd11c87da14b6f51d0c70b2 Mon Sep 17 00:00:00 2001 From: sudeep Date: Thu, 28 Mar 2024 09:52:43 +0000 Subject: [PATCH 12/16] updated tests Signed-off-by: sudeep --- eq-author/src/App/qcodes/QCodesTable/index.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eq-author/src/App/qcodes/QCodesTable/index.test.js b/eq-author/src/App/qcodes/QCodesTable/index.test.js index 9946524a31..7b51247d7b 100644 --- a/eq-author/src/App/qcodes/QCodesTable/index.test.js +++ b/eq-author/src/App/qcodes/QCodesTable/index.test.js @@ -163,7 +163,7 @@ const optionsSetup = (dataVersion) => { description: null, label: "OR1", additionalAnswer: null, - id: "9d6d013d-64c0-4757-b8e3-f991e2dcfb92", + id: "or1", value: "m1-value", }, { @@ -171,7 +171,7 @@ const optionsSetup = (dataVersion) => { description: null, label: "OR2", additionalAnswer: null, - id: "4b362d2e-1ec0-4cb2-b90d-572be5133ab2", + id: "or2", value: "m1-value", }, ], From b590583cd3345c43c9cfc18500f5fd802a4d0acb Mon Sep 17 00:00:00 2001 From: sudeep Date: Thu, 28 Mar 2024 11:00:50 +0000 Subject: [PATCH 13/16] empty commit From b2f74a04f791b36848709d2257e9083f58059d53 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 9 Apr 2024 13:42:09 +0100 Subject: [PATCH 14/16] solved qcode error badge for version 3 Signed-off-by: sudeep --- eq-author/src/components/QCodeContext/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index 21a36e9575..8a5eb96782 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -283,9 +283,9 @@ export const QCodeContextProvider = ({ questionnaire = {}, children }) => { const hasQCodeError = duplicatedQCodes?.length || - duplicatedOptionValues?.length || getEmptyQCodes(answerRows, questionnaire.dataVersion) || - getEmptyOptionValues(answerRows); + (questionnaire.dataVersion === "3" && duplicatedOptionValues?.length) || + (questionnaire.dataVersion === "3" && getEmptyOptionValues(answerRows)); const hasOptionValueError = duplicatedOptionValues?.length || getEmptyOptionValues(answerRows); From 6f74a673d7056c7aed38d507455582ff0c0ed79b Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 9 Apr 2024 13:54:15 +0100 Subject: [PATCH 15/16] solved empty qcode error badge in version 1 Signed-off-by: sudeep --- eq-author/src/components/QCodeContext/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index 8a5eb96782..65d279daef 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -206,7 +206,6 @@ const getEmptyQCodes = (answerRows, dataVersion) => { !qCode && ![ CHECKBOX, - CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION, MUTUALLY_EXCLUSIVE_OPTION, From c6de8e84619c9eab292e7b7d361107a3644a4538 Mon Sep 17 00:00:00 2001 From: sudeep Date: Tue, 9 Apr 2024 13:56:42 +0100 Subject: [PATCH 16/16] final solution for qcode error badge Signed-off-by: sudeep --- eq-author/src/components/QCodeContext/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eq-author/src/components/QCodeContext/index.js b/eq-author/src/components/QCodeContext/index.js index 65d279daef..3fd685858d 100644 --- a/eq-author/src/components/QCodeContext/index.js +++ b/eq-author/src/components/QCodeContext/index.js @@ -259,7 +259,12 @@ const getEmptyOptionValues = (answerRows) => { return answerRows?.find( ({ value, type, hideOptionValue }) => !value && - [CHECKBOX_OPTION, RADIO_OPTION, SELECT_OPTION].includes(type) && + [ + CHECKBOX_OPTION, + RADIO_OPTION, + SELECT_OPTION, + MUTUALLY_EXCLUSIVE_OPTION, + ].includes(type) && !hideOptionValue ); };