Skip to content

Commit

Permalink
Merge pull request #3084 from ONSdigital/EAR-1789-looping-questions-c…
Browse files Browse the repository at this point in the history
…alculated-summary

EAR-1789 - Looping questions calculated summary
  • Loading branch information
sudeepkunhis authored Dec 6, 2023
2 parents e7834ff + 59c6c95 commit 63bc36f
Show file tree
Hide file tree
Showing 18 changed files with 545 additions and 30 deletions.
2 changes: 2 additions & 0 deletions eq-author-api/constants/validationErrorCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ERR_COUNT_OF_GREATER_THAN_AVAILABLE_OPTIONS =
"ERR_COUNT_OF_GREATER_THAN_AVAILABLE_OPTIONS";
const ERR_VALID_PIPED_ANSWER_REQUIRED = "ERR_VALID_PIPED_ANSWER_REQUIRED";
const ERR_UNIQUE_PAGE_DESCRIPTION = "ERR_UNIQUE_PAGE_DESCRIPTION";
const ERR_NO_ANSWERS = "ERR_NO_ANSWERS";

module.exports = {
ERR_INVALID,
Expand Down Expand Up @@ -74,4 +75,5 @@ module.exports = {
ERR_COUNT_OF_GREATER_THAN_AVAILABLE_OPTIONS,
ERR_VALID_PIPED_ANSWER_REQUIRED,
ERR_UNIQUE_PAGE_DESCRIPTION,
ERR_NO_ANSWERS,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const createValidationError = require("../createValidationError");

const { ERR_NO_ANSWERS } = require("../../../constants/validationErrorCodes");
const { getPages, getFolders } = require("../../../schema/resolvers/utils");

module.exports = (ajv) =>
ajv.addKeyword({
keyword: "calculatedSummaryMinAnswers",
$data: true,
validate: function isValid(
schema,
_data,
_parentSchema,
{
instancePath,
rootData: questionnaire,
parentData,
parentDataProperty: fieldName,
}
) {
if (parentData.summaryAnswers.length > 1) {
return true;
} else {
const folders = getFolders({ questionnaire });
const pages = getPages({ questionnaire });

const allAnswers = pages.reduce(
(acc, page) => (page.answers ? [...acc, ...page.answers] : acc),
[]
);

const selectedAnswers = allAnswers.filter((answer) =>
parentData.summaryAnswers.includes(answer.id)
);

let selectedFolder;
folders.forEach((folder) => {
folder.pages.forEach((page) => {
if (page.id === selectedAnswers[0]?.questionPageId) {
selectedFolder = folder;
}
});
});

if (parentData.summaryAnswers.length === 1 && selectedFolder?.listId) {
return true;
} else {
isValid.errors = [
createValidationError(
instancePath,
fieldName,
ERR_NO_ANSWERS,
questionnaire
),
];
return false;
}
}
},
});
1 change: 1 addition & 0 deletions eq-author-api/src/validation/customKeywords/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ module.exports = (ajv) => {
require("./validateSecondaryCondition")(ajv);
require("./validatePageDescription")(ajv);
require("./requiredWhenIntroductionSetting")(ajv);
require("./calculatedSummaryMinAnswers")(ajv);
};
5 changes: 2 additions & 3 deletions eq-author-api/src/validation/schemas/page.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,8 @@
"calculatedSummaryPosition": {
"$data": "/sections"
},
"minItems": 2,
"errorMessage": {
"minItems": "ERR_NO_ANSWERS"
"calculatedSummaryMinAnswers": {
"$data": "/sections"
}
},
"totalTitle": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const Answers = ({ page, onUpdateCalculatedSummaryPage, onSelect }) => {
{selectedAnswers.map((answer) => (
<SelectedAnswer
key={answer.id}
insideListCollectorFolder={answer?.page?.folder?.listId != null}
{...answer}
onRemove={() => handleRemoveAnswers([answer])}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("Empty state", () => {
expect(selectBtn).toBeInTheDocument();

const errorMsg = getByText(
"Select at least two answers or calculated summary totals"
"Minimum selection requirements not met. Add an answer or calculated summary total."
);
expect(errorMsg).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const Title = styled(Truncated)`
const Chip = styled(MenuItemType)`
color: ${colors.text};
float: right;
font-size: 0.8em;
`;

const CloseButton = styled.button`
Expand Down Expand Up @@ -75,6 +76,7 @@ const SelectedAnswer = ({
displayName,
properties,
type: answerType,
insideListCollectorFolder,
onRemove,
}) => {
const unitType = properties.unit || false;
Expand All @@ -93,6 +95,7 @@ const SelectedAnswer = ({
<Wrapper>
<Title>{displayName}</Title>
{unitType && <Chip data-test="unit-type">{unitType}</Chip>}
{insideListCollectorFolder && <Chip>List collector follow-up</Chip>}
<Chip>{answerType}</Chip>
<CloseButton
data-test="remove-answer-button"
Expand All @@ -109,6 +112,7 @@ SelectedAnswer.propTypes = {
onRemove: PropType.func.isRequired,
displayName: PropType.string.isRequired,
type: PropType.string.isRequired,
insideListCollectorFolder: PropType.bool,
properties: PropType.object.isRequired, // eslint-disable-line
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ AnswerSelector.fragments = {
displayName
type
properties
page {
id
folder {
id
... on ListCollectorFolder {
listId
}
}
}
}
type
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe("Submit selected answers", () => {
);
expect(pickerHeader[1]).toBeInTheDocument();

const calculatedSummaryAnswers = getAllByText("CALCULATED SUMMARY");
const calculatedSummaryAnswers = getAllByText("Calculated summary");
expect(calculatedSummaryAnswers).toBeTruthy();
calculatedSummaryAnswers[0].click();
calculatedSummaryAnswers[1].click();
Expand All @@ -182,3 +182,50 @@ describe("Submit selected answers", () => {
expect(mockOnUpdateCalculatedSummaryPage).toHaveBeenCalledTimes(1);
});
});

describe("Select list collector follow-up answers", () => {
let mockOnUpdateCalculatedSummaryPage, mockUseQuestionnaire;

beforeEach(() => {
mockOnUpdateCalculatedSummaryPage = jest.fn();

mockUseQuestionnaire = jest.fn(() => ({
questionnaire: mockCalculatedSummary,
}));

questionnaireContext.useQuestionnaire = mockUseQuestionnaire; // eslint-disable-line import/namespace
});

it("should submit the selected list collector follow-up answers", () => {
const { getByText, getAllByText, getByTestId } = render(() => (
<AnswerSelector
page={mockCalculatedSummary.sections[0].folders[2].pages[0]}
onUpdateCalculatedSummaryPage={mockOnUpdateCalculatedSummaryPage}
/>
));

const btn = getByText("Select an answer or calculated summary total");

expect(btn).not.toBeDisabled();
btn.click();

const pickerHeader = getAllByText(
"Select an answer or calculated summary total"
);
expect(pickerHeader[1]).toBeInTheDocument();

const listCollectorFollowupAnswers = getAllByText(
"List collector follow-up"
);
expect(listCollectorFollowupAnswers).toBeTruthy();
listCollectorFollowupAnswers[0].click();
listCollectorFollowupAnswers[1].click();

const selectButton = getByTestId("select-summary-answers");
expect(selectButton).toBeTruthy();

selectButton.click();

expect(mockOnUpdateCalculatedSummaryPage).toHaveBeenCalledTimes(1);
});
});
80 changes: 78 additions & 2 deletions eq-author/src/App/page/Design/CalculatedSummaryPageEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import gql from "graphql-tag";
import { richTextEditorErrors } from "constants/validationMessages";
import { colors } from "constants/theme";

import { Field } from "components/Forms";
import Collapsible from "components/Collapsible";

import RichTextEditor from "components/RichTextEditor";
import withEntityEditor from "components/withEntityEditor";
import PageHeader from "../PageHeader";
Expand All @@ -21,7 +24,6 @@ import {
VARIABLES,
} from "components/ContentPickerSelectv3/content-types";

//new
import AnswerSelector from "./AnswerSelector";

import withPropRenamed from "enhancers/withPropRenamed";
Expand All @@ -42,12 +44,42 @@ const PageSegment = styled.div`
padding: 0 2em;
`;

const Content = styled.p``;

const ContentCustomMargin = styled.p`
margin-bottom: 0.2em;
`;

const StyledField = styled(Field)`
padding: 0 2em;
`;

const ContentContainer = styled.span``;

const Title = styled.h2`
display: block;
font-size: 1em;
margin-top: 1.33em;
margin-bottom: -0.5em;
margin-left: 0;
margin-right: 0;
font-weight: bold;
`;

const SelectorTitle = styled.h2`
font-size: 1em;
color: ${colors.black};
margin: 0 0 0.4em;
`;

const UnorderedList = styled.ul`
padding-left: 0;
margin-top: 0;
margin-left: 2em;
`;

const ListItem = styled.li``;

const HorizontalRule = styled.hr`
border: 0;
border-top: 0.0625em solid ${colors.lightMediumGrey};
Expand Down Expand Up @@ -134,6 +166,38 @@ export const CalculatedSummaryPageEditor = (props) => {
isMoveDisabled
isDuplicateDisabled
/>
<StyledField>
<Title>What is a calculated summary?</Title>
<ContentContainer>
<Content>
A calculated summary allows you to combine answers of the same type
from multiple questions to generate a total.
</Content>
<Content>
Answer types such as currency, number, percentage, and unit are
permitted, along with the totals of other calculated summaries. The
answers being totalled must belong to the same section, except for
the totals of other calculated summaries, which can come from
different sections.
</Content>
</ContentContainer>
<Collapsible title="Calculated summary for list collector follow-up questions">
<Content>
A calculated summary can total the answers for each list item&apos;s
follow-up question. If additional answers are selected, they must be
of the same answer type.
</Content>
<Content>
The list item will be displayed in the calculated summary table with
its corresponding value.
</Content>
<Content>
If 1 list collector follow-up question is selected for the
calculated summary and 1 list item is added by the respondent, the
summary will be shown.
</Content>
</Collapsible>
</StyledField>
<PageSegment>
<RichTextEditor
id="summary-title"
Expand Down Expand Up @@ -162,7 +226,19 @@ export const CalculatedSummaryPageEditor = (props) => {
/>
<HorizontalRule />
<div>
<SelectorTitle>Answers to calculate</SelectorTitle>
<SelectorTitle>
Select answers or calculated summaries to total
</SelectorTitle>
<ContentCustomMargin>
A calculated summary must include at least:
</ContentCustomMargin>
<UnorderedList>
<ListItem>2 answers of the same type</ListItem>
<ListItem>
or 1 answer from a list collector follow-up question
</ListItem>
<ListItem>or 2 calculated summary totals</ListItem>
</UnorderedList>
<AnswerSelector
onUpdateCalculatedSummaryPage={onUpdateCalculatedSummaryPage}
page={page}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,20 @@ const RoutingAnswerContentPicker = ({
[questionnaire, pageId, includeSelf]
);

const filteredPreviousAnswers = previousAnswers.map((answer) => {
return {
...answer,
folders: answer.folders.filter((folder) => folder.listId == null),
};
});

const metadata =
questionnaire?.metadata?.filter(
({ type }) => type === TEXT.value || type === TEXT_OPTIONAL.value
) || [];

const data = {
[ANSWER]: previousAnswers,
[ANSWER]: filteredPreviousAnswers,
[METADATA]: metadata,
};

Expand Down
Loading

0 comments on commit 63bc36f

Please sign in to comment.