diff --git a/eq-author-api/constants/legalBases.js b/eq-author-api/constants/legalBases.js
new file mode 100644
index 0000000000..fde2957322
--- /dev/null
+++ b/eq-author-api/constants/legalBases.js
@@ -0,0 +1,3 @@
+module.exports.NOTICE_1 = "NOTICE_1";
+module.exports.NOTICE_2 = "NOTICE_2";
+module.exports.VOLUNTARY = "VOLUNTARY";
diff --git a/eq-author-api/db/models/DynamoDB.js b/eq-author-api/db/models/DynamoDB.js
index 7e40865200..0ebfeb6afc 100644
--- a/eq-author-api/db/models/DynamoDB.js
+++ b/eq-author-api/db/models/DynamoDB.js
@@ -42,6 +42,9 @@ const baseQuestionnaireSchema = {
shortTitle: {
type: String,
},
+ introduction: {
+ type: Object,
+ },
};
const questionnanaireSchema = new dynamoose.Schema(
diff --git a/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.js b/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.js
new file mode 100644
index 0000000000..41a0d06c2c
--- /dev/null
+++ b/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.js
@@ -0,0 +1,32 @@
+//This is an auto-generated file. Do NOT modify the method signature.
+
+const { find } = require("lodash");
+const { BUSINESS } = require("../constants/questionnaireTypes");
+const {
+ createDefaultBusinessSurveyMetadata,
+} = require("../utils/defaultMetadata");
+const {
+ createQuestionnaireIntroduction,
+} = require("../schema/resolvers/questionnaireIntroduction");
+
+module.exports = function addBusinessQuestionnaireIntroduction(questionnaire) {
+ if (questionnaire.type !== BUSINESS) {
+ return questionnaire;
+ }
+
+ const defaultMetadata = createDefaultBusinessSurveyMetadata();
+ const metadataToAdd = defaultMetadata
+ .filter(({ key }) => !find(questionnaire.metadata, { key }))
+ .map((md, index) => ({
+ ...md,
+ id: `migrated-md-${index}`,
+ }));
+ questionnaire.metadata = [...questionnaire.metadata, ...metadataToAdd];
+
+ questionnaire.introduction = createQuestionnaireIntroduction(
+ questionnaire.metadata
+ );
+ questionnaire.introduction.id = "questionnaire-introduction";
+
+ return questionnaire;
+};
diff --git a/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.test.js b/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.test.js
new file mode 100644
index 0000000000..8a731eac4f
--- /dev/null
+++ b/eq-author-api/migrations/addBusinessQuestionnaireIntroduction.test.js
@@ -0,0 +1,57 @@
+const { cloneDeep } = require("lodash");
+
+const { SOCIAL, BUSINESS } = require("../constants/questionnaireTypes");
+
+const addBusinessQuestionnaireIntroduction = require("./addBusinessQuestionnaireIntroduction.js");
+
+describe("addBusinessQuestionnaireIntroduction", () => {
+ it("should be deterministic", () => {
+ const questionnaire = {
+ type: BUSINESS,
+ metadata: [],
+ };
+
+ expect(
+ addBusinessQuestionnaireIntroduction(cloneDeep(questionnaire))
+ ).toMatchObject(
+ addBusinessQuestionnaireIntroduction(cloneDeep(questionnaire))
+ );
+ });
+
+ it("should not touch social", () => {
+ const questionnaire = {
+ type: SOCIAL,
+ };
+
+ expect(addBusinessQuestionnaireIntroduction(questionnaire)).toEqual(
+ questionnaire
+ );
+ });
+
+ it("should add missing default metadata", () => {
+ const questionnaire = {
+ type: BUSINESS,
+ metadata: [
+ {
+ key: "ru_name",
+ alias: "Ru Name",
+ type: "Text",
+ value: "ESSENTIAL ENTERPRISE LTD.",
+ },
+ ],
+ };
+
+ expect(
+ addBusinessQuestionnaireIntroduction(questionnaire).metadata.map(
+ md => md.key
+ )
+ ).toEqual([
+ "ru_name",
+ "trad_as",
+ "period_id",
+ "ref_p_start_date",
+ "ref_p_end_date",
+ "employmentDate",
+ ]);
+ });
+});
diff --git a/eq-author-api/migrations/index.js b/eq-author-api/migrations/index.js
index 5116d6b226..bf178f2b20 100644
--- a/eq-author-api/migrations/index.js
+++ b/eq-author-api/migrations/index.js
@@ -2,12 +2,14 @@ const addVersion = require("./addVersion");
const addOptionalFieldProperties = require("./addOptionalFieldProperties");
const addQuestionnaireType = require("./addQuestionnaireType");
const updateMetadataValue = require("./updateMetadataValue");
+const addBusinessQuestionnaireIntroduction = require("./addBusinessQuestionnaireIntroduction");
const migrations = [
addVersion,
addOptionalFieldProperties,
addQuestionnaireType,
updateMetadataValue,
+ addBusinessQuestionnaireIntroduction,
];
const currentVersion = migrations.length;
diff --git a/eq-author-api/schema/resolvers/base.js b/eq-author-api/schema/resolvers/base.js
index 5af7b458d1..4b569b6925 100644
--- a/eq-author-api/schema/resolvers/base.js
+++ b/eq-author-api/schema/resolvers/base.js
@@ -55,7 +55,7 @@ const {
} = require("../../utils/datastore");
const {
- defaultBusinessSurveyMetadata,
+ createDefaultBusinessSurveyMetadata,
} = require("../../utils/defaultMetadata");
const { listQuestionnaires } = require("../../utils/datastore");
@@ -64,6 +64,11 @@ const { DATE, DATE_RANGE } = require("../../constants/answerTypes");
const { DATE: METADATA_DATE } = require("../../constants/metadataTypes");
const { VALIDATION_TYPES } = require("../../constants/validationTypes");
+const {
+ createQuestionnaireIntroduction,
+} = require("./questionnaireIntroduction");
+
+
const getQuestionnaireList = () => {
return listQuestionnaires();
};
@@ -131,22 +136,39 @@ const createSection = (input = {}) => ({
...input,
});
-const createNewQuestionnaire = input => ({
- id: uuid.v4(),
- title: null,
- description: null,
- theme: "default",
- legalBasis: "Voluntary",
- navigation: false,
- surveyId: "",
- createdAt: new Date(),
- metadata: input.type === BUSINESS ? defaultBusinessSurveyMetadata : [],
- sections: [createSection()],
- summary: false,
- version: currentVersion,
- shortTitle: "",
- ...input,
-});
+const createNewQuestionnaire = input => {
+ const defaultQuestionnaire = {
+ id: uuid.v4(),
+ title: null,
+ description: null,
+ theme: "default",
+ legalBasis: "Voluntary",
+ navigation: false,
+ surveyId: "",
+ createdAt: new Date(),
+ metadata: [],
+ sections: [createSection()],
+ summary: false,
+ version: currentVersion,
+ shortTitle: "",
+ introduction: null,
+ };
+
+ let changes = {};
+ if (input.type === BUSINESS) {
+ const metadata = createDefaultBusinessSurveyMetadata();
+ changes = {
+ metadata,
+ introduction: createQuestionnaireIntroduction(metadata),
+ };
+ }
+
+ return {
+ ...defaultQuestionnaire,
+ ...changes,
+ ...input,
+ };
+};
const Resolvers = {
Query: {
diff --git a/eq-author-api/schema/resolvers/index.js b/eq-author-api/schema/resolvers/index.js
index 4984e95346..df2a65eab1 100644
--- a/eq-author-api/schema/resolvers/index.js
+++ b/eq-author-api/schema/resolvers/index.js
@@ -1,5 +1,6 @@
const base = require("./base");
const routing2 = require("./routing2");
const page = require("./pages");
+const questionnaireIntroduction = require("./questionnaireIntroduction");
-module.exports = [base, ...routing2, ...page];
+module.exports = [base, ...routing2, ...page, ...questionnaireIntroduction];
diff --git a/eq-author-api/schema/resolvers/questionnaireIntroduction.js b/eq-author-api/schema/resolvers/questionnaireIntroduction.js
new file mode 100644
index 0000000000..b88d00cfca
--- /dev/null
+++ b/eq-author-api/schema/resolvers/questionnaireIntroduction.js
@@ -0,0 +1,83 @@
+const { find, omit, first, remove } = require("lodash");
+const uuid = require("uuid").v4;
+
+const { saveQuestionnaire } = require("../../utils/datastore");
+const { NOTICE_1 } = require("../../constants/legalBases");
+
+const createCollapsible = options => ({
+ id: uuid(),
+ title: "",
+ description: "",
+ ...options,
+});
+
+const Resolvers = {};
+Resolvers.Query = {
+ questionnaireIntroduction: (root, args, ctx) =>
+ ctx.questionnaire.introduction,
+};
+Resolvers.Mutation = {
+ updateQuestionnaireIntroduction: async (_, { input }, ctx) => {
+ const introduction = ctx.questionnaire.introduction;
+ Object.assign(introduction, omit(input, "id"));
+ await saveQuestionnaire(ctx.questionnaire);
+ return introduction;
+ },
+ createCollapsible: async (_, { input }, ctx) => {
+ const collapsible = createCollapsible(omit(input, "introductionId"));
+ ctx.questionnaire.introduction.collapsibles.push(collapsible);
+ await saveQuestionnaire(ctx.questionnaire);
+ return collapsible;
+ },
+ updateCollapsible: async (_, { input: { id, ...rest } }, ctx) => {
+ const collapsible = ctx.questionnaire.introduction.collapsibles.find(
+ c => c.id === id
+ );
+ Object.assign(collapsible, rest);
+ await saveQuestionnaire(ctx.questionnaire);
+ return collapsible;
+ },
+ moveCollapsible: async (_, { input: { id, position } }, ctx) => {
+ const introduction = ctx.questionnaire.introduction;
+ const collapsibleMoving = first(remove(introduction.collapsibles, { id }));
+ introduction.collapsibles.splice(position, 0, collapsibleMoving);
+ await saveQuestionnaire(ctx.questionnaire);
+ return collapsibleMoving;
+ },
+ deleteCollapsible: async (_, { input: { id } }, ctx) => {
+ const introduction = ctx.questionnaire.introduction;
+ remove(introduction.collapsibles, { id });
+ await saveQuestionnaire(ctx.questionnaire);
+ return introduction;
+ },
+};
+Resolvers.QuestionnaireIntroduction = {
+ availablePipingMetadata: (root, args, ctx) => ctx.questionnaire.metadata,
+ availablePipingAnswers: () => [],
+};
+
+Resolvers.Collapsible = {
+ introduction: (root, args, ctx) => ctx.questionnaire.introduction,
+};
+
+module.exports = [Resolvers];
+module.exports.createQuestionnaireIntroduction = metadata => {
+ return {
+ id: uuid.v4(),
+ title: `
You are completing this for trad_as (ru_name)
`,
+ description:
+ "- Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated.
- You can provide info estimates if actual figures aren’t available.
- We will treat your data securely and confidentially.
",
+ legalBasis: NOTICE_1,
+ secondaryTitle: "Information you need
",
+ secondaryDescription:
+ "You can select the dates of the period you are reporting for, if the given dates are not appropriate.
",
+ collapsibles: [],
+ tertiaryTitle: "How we use your data
",
+ tertiaryDescription:
+ "- You cannot appeal your selection. Your business was selected to give us a comprehensive view of the UK economy.
- The information you provide contributes to Gross Domestic Product (GDP).
",
+ };
+};
diff --git a/eq-author-api/schema/tests/collapsibles.test.js b/eq-author-api/schema/tests/collapsibles.test.js
new file mode 100644
index 0000000000..4b680b2fc4
--- /dev/null
+++ b/eq-author-api/schema/tests/collapsibles.test.js
@@ -0,0 +1,174 @@
+const {
+ buildQuestionnaire,
+} = require("../../tests/utils/questionnaireBuilder");
+const {
+ deleteQuestionnaire,
+} = require("../../tests/utils/questionnaireBuilder/questionnaire");
+const {
+ queryCollapsible,
+ createCollapsible,
+ updateCollapsible,
+ moveCollapsible,
+ deleteCollapsible,
+} = require("../../tests/utils/questionnaireBuilder/collapsible");
+
+const { BUSINESS } = require("../../constants/questionnaireTypes");
+
+describe("questionnaire", () => {
+ let questionnaire;
+
+ afterEach(async () => {
+ await deleteQuestionnaire(questionnaire.id);
+ questionnaire = null;
+ });
+
+ describe("read", () => {
+ it("should return the collapsibles on the introduction", async () => {
+ questionnaire = await buildQuestionnaire({
+ type: BUSINESS,
+ introduction: {
+ collapsibles: [
+ {
+ title: "Collapsible title",
+ description: "Collapsible description",
+ },
+ ],
+ },
+ });
+
+ const collapsibles = await queryCollapsible(
+ questionnaire,
+ questionnaire.introduction.id
+ );
+ expect(collapsibles).toEqual([
+ {
+ id: expect.any(String),
+ title: "Collapsible title",
+ description: "Collapsible description",
+ introduction: {
+ id: questionnaire.introduction.id,
+ },
+ },
+ ]);
+ });
+ });
+
+ describe("create", () => {
+ it("should add a collapsible", async () => {
+ questionnaire = await buildQuestionnaire({ type: BUSINESS });
+ const collapsible = await createCollapsible(questionnaire, {
+ introductionId: questionnaire.introduction.id,
+ title: "Some title",
+ description: "Some description",
+ });
+ expect(collapsible).toEqual({
+ id: expect.any(String),
+ title: "Some title",
+ description: "Some description",
+ introduction: {
+ id: questionnaire.introduction.id,
+ },
+ });
+ });
+ });
+
+ describe("update", () => {
+ it("should update the properties", async () => {
+ questionnaire = await buildQuestionnaire({
+ type: BUSINESS,
+ introduction: {
+ collapsibles: [
+ {
+ title: "Collapsible title",
+ description: "Collapsible description",
+ },
+ ],
+ },
+ });
+
+ const collapsible = await updateCollapsible(questionnaire, {
+ id: questionnaire.introduction.collapsibles[0].id,
+ title: "Some title",
+ description: "Some description",
+ });
+
+ expect(collapsible).toEqual({
+ id: questionnaire.introduction.collapsibles[0].id,
+ title: "Some title",
+ description: "Some description",
+ });
+ });
+ });
+
+ describe("move", () => {
+ it("should move the collapsible forward", async () => {
+ questionnaire = await buildQuestionnaire({
+ type: BUSINESS,
+ introduction: {
+ collapsibles: [{}, {}],
+ },
+ });
+
+ const collapsibleIds = questionnaire.introduction.collapsibles.map(
+ c => c.id
+ );
+ const [collapsible1Id, collapsible2Id] = collapsibleIds;
+
+ const result = await moveCollapsible(questionnaire, {
+ id: collapsible2Id,
+ position: 0,
+ });
+ expect(result.introduction.collapsibles.map(c => c.id)).toEqual([
+ collapsible2Id,
+ collapsible1Id,
+ ]);
+ });
+
+ it("should move the collapsible backward", async () => {
+ questionnaire = await buildQuestionnaire({
+ type: BUSINESS,
+ introduction: {
+ collapsibles: [{}, {}],
+ },
+ });
+
+ const collapsibleIds = questionnaire.introduction.collapsibles.map(
+ c => c.id
+ );
+ const [collapsible1Id, collapsible2Id] = collapsibleIds;
+
+ const result = await moveCollapsible(questionnaire, {
+ id: collapsible1Id,
+ position: 1,
+ });
+ expect(result.introduction.collapsibles.map(c => c.id)).toEqual([
+ collapsible2Id,
+ collapsible1Id,
+ ]);
+ });
+ });
+
+ describe("delete", () => {
+ it("should remove the collapsible", async () => {
+ questionnaire = await buildQuestionnaire({
+ type: BUSINESS,
+ introduction: {
+ collapsibles: [{}, {}],
+ },
+ });
+
+ const collapsibleIds = questionnaire.introduction.collapsibles.map(
+ c => c.id
+ );
+ const [collapsible1Id, collapsible2Id] = collapsibleIds;
+
+ const introduction = await deleteCollapsible(questionnaire, {
+ id: collapsible1Id,
+ });
+
+ expect(introduction.collapsibles.map(c => c.id)).toEqual([
+ collapsible2Id,
+ ]);
+ });
+ });
+});
diff --git a/eq-author-api/schema/tests/questionnaire.test.js b/eq-author-api/schema/tests/questionnaire.test.js
index 582bfe46f6..fdd854cbf8 100644
--- a/eq-author-api/schema/tests/questionnaire.test.js
+++ b/eq-author-api/schema/tests/questionnaire.test.js
@@ -5,10 +5,8 @@ const { SOCIAL, BUSINESS } = require("../../constants/questionnaireTypes");
const {
buildQuestionnaire,
} = require("../../tests/utils/questionnaireBuilder");
-const executeQuery = require("../../tests/utils/executeQuery");
const {
createQuestionnaire,
- createQuestionnaireMutation,
queryQuestionnaire,
updateQuestionnaire,
deleteQuestionnaire,
@@ -34,22 +32,15 @@ describe("questionnaire", () => {
description: "Description",
surveyId: "1",
theme: "default",
- legalBasis: "Voluntary",
navigation: false,
summary: false,
type: SOCIAL,
shortTitle: "short title",
};
- questionnaire = await createQuestionnaire(config);
});
it("should create a questionnaire with a section and page", async () => {
- const result = await executeQuery(
- createQuestionnaireMutation,
- { input: config },
- {}
- );
- const questionnaire = result.data.createQuestionnaire;
+ const questionnaire = await createQuestionnaire(config);
expect(questionnaire).toEqual(
expect.objectContaining({ ...config, displayName: "short title" })
);
@@ -58,24 +49,29 @@ describe("questionnaire", () => {
});
it("should create a questionnaire with no metadata when creating a social survey", async () => {
- const result = await executeQuery(
- createQuestionnaireMutation,
- { input: config },
- {}
- );
- const questionnaire = result.data.createQuestionnaire;
+ const questionnaire = await createQuestionnaire(config);
expect(questionnaire.metadata).toEqual([]);
});
it("should create a questionnaire with default business metadata when creating a business survey", async () => {
- const result = await executeQuery(
- createQuestionnaireMutation,
- { input: { ...config, type: BUSINESS } },
- {}
- );
- const questionnaire = result.data.createQuestionnaire;
+ const questionnaire = await createQuestionnaire({
+ ...config,
+ type: BUSINESS,
+ });
expect(questionnaire.metadata).toHaveLength(6);
});
+
+ it("should create a questionnaire introduction for business surveys", async () => {
+ const questionnaire = await createQuestionnaire({
+ ...config,
+ type: BUSINESS,
+ });
+ expect(questionnaire.introduction).toMatchObject({
+ id: expect.any(String),
+ title: expect.any(String),
+ collapsibles: [],
+ });
+ });
});
describe("mutate", () => {
@@ -85,7 +81,6 @@ describe("questionnaire", () => {
description: "Description",
surveyId: "1",
theme: "default",
- legalBasis: "Voluntary",
navigation: false,
summary: false,
metadata: [{}],
@@ -96,7 +91,6 @@ describe("questionnaire", () => {
title: "Questionnaire-updated",
description: "Description-updated",
theme: "census",
- legalBasis: "StatisticsOfTradeAct",
navigation: true,
surveyId: "2-updated",
summary: true,
@@ -147,7 +141,6 @@ describe("questionnaire", () => {
displayName: expect.any(String),
description: expect.any(String),
theme: expect.any(String),
- legalBasis: expect.any(String),
navigation: expect.any(Boolean),
surveyId: expect.any(String),
createdAt: expect.any(String),
diff --git a/eq-author-api/schema/tests/questionnaireIntroduction.test.js b/eq-author-api/schema/tests/questionnaireIntroduction.test.js
new file mode 100644
index 0000000000..04b9d0401f
--- /dev/null
+++ b/eq-author-api/schema/tests/questionnaireIntroduction.test.js
@@ -0,0 +1,89 @@
+const {
+ buildQuestionnaire,
+} = require("../../tests/utils/questionnaireBuilder");
+const {
+ deleteQuestionnaire,
+} = require("../../tests/utils/questionnaireBuilder/questionnaire");
+const {
+ queryQuestionnaireIntroduction,
+ updateQuestionnaireIntroduction,
+} = require("../../tests/utils/questionnaireBuilder/questionnaireIntroduction");
+
+const { BUSINESS } = require("../../constants/questionnaireTypes");
+const { NOTICE_2 } = require("../../constants/legalBases");
+
+describe("questionnaire", () => {
+ let questionnaire;
+
+ beforeEach(async () => {
+ questionnaire = await buildQuestionnaire({ type: BUSINESS });
+ });
+
+ afterEach(async () => {
+ await deleteQuestionnaire(questionnaire.id);
+ questionnaire = null;
+ });
+
+ describe("read", () => {
+ it("should return the questionnaire introduction", async () => {
+ const introduction = await queryQuestionnaireIntroduction(
+ questionnaire,
+ questionnaire.introduction.id
+ );
+ expect(introduction).toEqual({
+ id: expect.any(String),
+ title: expect.any(String),
+ description: expect.any(String),
+ secondaryTitle: expect.any(String),
+ secondaryDescription: expect.any(String),
+ collapsibles: expect.any(Array),
+ legalBasis: expect.any(String),
+ tertiaryTitle: expect.any(String),
+ tertiaryDescription: expect.any(String),
+ availablePipingAnswers: expect.any(Array),
+ availablePipingMetadata: expect.any(Array),
+ });
+ });
+
+ it("should return the available piping metadata but no answers", async () => {
+ const introduction = await queryQuestionnaireIntroduction(
+ questionnaire,
+ questionnaire.introduction.id
+ );
+
+ expect(introduction.availablePipingAnswers).toEqual([]);
+ expect(introduction.availablePipingMetadata).not.toHaveLength(0);
+ expect(introduction.availablePipingMetadata.map(md => md.id)).toEqual(
+ questionnaire.metadata.map(md => md.id)
+ );
+ });
+ });
+
+ describe("update", () => {
+ it("should update the properties", async () => {
+ const changes = {
+ title: "new title",
+ description: "new description",
+ secondaryTitle: "new secondaryTitle",
+ secondaryDescription: "new secondaryDescription",
+ legalBasis: NOTICE_2,
+ tertiaryTitle: "new tertiaryTitle",
+ tertiaryDescription: "new tertiaryDescription",
+ };
+
+ const updatedIntroduction = await updateQuestionnaireIntroduction(
+ questionnaire,
+ {
+ id: questionnaire.introduction.id,
+ ...changes,
+ }
+ );
+
+ expect(updatedIntroduction).toEqual({
+ id: questionnaire.introduction.id,
+ collapsibles: expect.any(Array),
+ ...changes,
+ });
+ });
+ });
+});
diff --git a/eq-author-api/schema/typeDefs.js b/eq-author-api/schema/typeDefs.js
index f8adc970d6..98e5f2ab6a 100644
--- a/eq-author-api/schema/typeDefs.js
+++ b/eq-author-api/schema/typeDefs.js
@@ -29,7 +29,6 @@ type Questionnaire {
title: String
description: String
theme: Theme
- legalBasis: LegalBasis
navigation: Boolean
surveyId: String
createdAt: Date
@@ -41,6 +40,7 @@ type Questionnaire {
type: QuestionnaireType!
shortTitle: String
displayName: String!
+ introduction: QuestionnaireIntroduction
}
type Section {
@@ -321,11 +321,6 @@ enum AnswerType {
Relationship
}
-enum LegalBasis {
- Voluntary
- StatisticsOfTradeAct
-}
-
enum Theme {
default
census
@@ -441,6 +436,33 @@ type BinaryExpression2 {
expressionGroup: ExpressionGroup2!
}
+enum LegalBasis {
+ NOTICE_1
+ NOTICE_2
+ VOLUNTARY
+}
+
+type Collapsible {
+ id: ID!
+ title: String!
+ description: String!
+ introduction: QuestionnaireIntroduction!
+}
+
+type QuestionnaireIntroduction {
+ id: ID!
+ title: String!
+ description: String!
+ legalBasis: LegalBasis!
+ secondaryTitle: String!
+ secondaryDescription: String!
+ collapsibles: [Collapsible!]!
+ tertiaryTitle: String!
+ tertiaryDescription: String!
+ availablePipingAnswers: [Answer!]!
+ availablePipingMetadata: [Metadata!]!
+}
+
type Query {
questionnaires: [Questionnaire]
questionnaire(input: QueryInput!): Questionnaire
@@ -451,6 +473,7 @@ type Query {
option(input: QueryInput!): Option
pagesAffectedByDeletion(pageId: ID!): [Page]! @deprecated(reason: "Not implemented")
questionConfirmation(id: ID!): QuestionConfirmation
+ questionnaireIntroduction(id: ID!): QuestionnaireIntroduction
me: User!
}
@@ -508,6 +531,11 @@ type Mutation {
updateLeftSide2(input: UpdateLeftSide2Input!): BinaryExpression2!
updateRightSide2(input: UpdateRightSide2Input!): BinaryExpression2!
deleteBinaryExpression2(input: DeleteBinaryExpression2Input!): ExpressionGroup2!
+ updateQuestionnaireIntroduction(input: UpdateQuestionnaireIntroductionInput): QuestionnaireIntroduction!
+ createCollapsible(input: CreateCollapsibleInput!): Collapsible!
+ updateCollapsible(input: UpdateCollapsibleInput!): Collapsible!
+ moveCollapsible(input: MoveCollapsibleInput!): Collapsible!
+ deleteCollapsible(input: DeleteCollapsibleInput!): QuestionnaireIntroduction!
}
input CreateRouting2Input {
@@ -575,7 +603,6 @@ input CreateQuestionnaireInput {
title: String!
description: String
theme: String!
- legalBasis: LegalBasis!
navigation: Boolean
surveyId: String!
summary: Boolean
@@ -864,4 +891,35 @@ input DeleteQuestionConfirmationInput {
id: ID!
}
+input UpdateQuestionnaireIntroductionInput {
+ id: ID!
+ title: String!
+ description: String!
+ legalBasis: LegalBasis!
+ secondaryTitle: String!
+ secondaryDescription: String!
+ tertiaryTitle: String!
+ tertiaryDescription: String!
+}
+
+input CreateCollapsibleInput {
+ introductionId: ID!
+ title: String
+ description: String
+}
+
+input UpdateCollapsibleInput {
+ id: ID!
+ title: String!
+ description: String!
+}
+
+input MoveCollapsibleInput {
+ id: ID!
+ position: Int!
+}
+
+input DeleteCollapsibleInput {
+ id: ID!
+}
`;
diff --git a/eq-author-api/scripts/createMigration.js b/eq-author-api/scripts/createMigration.js
index 89e0e6b02a..2a8a91cd80 100644
--- a/eq-author-api/scripts/createMigration.js
+++ b/eq-author-api/scripts/createMigration.js
@@ -24,9 +24,16 @@ module.exports = function ${name}(questionnaire) {
};
`;
-const testTemplate = `const ${name} = require("./${filename}");
+const testTemplate = `const { cloneDeep } = require("lodash");
+const ${name} = require("./${filename}");
describe("${name}", () => {
+ // This test must remain for your migration to always work
+ it("should be deterministic", () => {
+ const questionnaire = {}; // Fill in the structure of the questionnaire here
+ expect(${name}(cloneDeep(questionnaire))).toEqual(${name}(cloneDeep(questionnaire)));
+ });
+
it.todo("should...");
});
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/create.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/create.js
new file mode 100644
index 0000000000..fb97978bde
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/create.js
@@ -0,0 +1,42 @@
+const { filter } = require("graphql-anywhere");
+const gql = require("graphql-tag");
+
+const executeQuery = require("../../executeQuery");
+
+const createCollapsibleMutation = `
+ mutation CreateCollapsible($input: CreateCollapsibleInput!) {
+ createCollapsible(input: $input) {
+ id
+ title
+ description
+ introduction {
+ id
+ }
+ }
+ }
+`;
+
+const createCollapsible = async (questionnaire, input) => {
+ const result = await executeQuery(
+ createCollapsibleMutation,
+ {
+ input: filter(
+ gql`
+ {
+ introductionId
+ title
+ description
+ }
+ `,
+ input
+ ),
+ },
+ { questionnaire }
+ );
+ return result.data.createCollapsible;
+};
+
+module.exports = {
+ createCollapsibleMutation,
+ createCollapsible,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/delete.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/delete.js
new file mode 100644
index 0000000000..f85f0f2a3b
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/delete.js
@@ -0,0 +1,26 @@
+const executeQuery = require("../../executeQuery");
+
+const deleteCollapsibleMutation = `
+ mutation DeleteCollapsible($input: DeleteCollapsibleInput!) {
+ deleteCollapsible(input: $input) {
+ id
+ collapsibles {
+ id
+ }
+ }
+ }
+`;
+
+const deleteCollapsible = async (questionnaire, input) => {
+ const result = await executeQuery(
+ deleteCollapsibleMutation,
+ { input },
+ { questionnaire }
+ );
+ return result.data.deleteCollapsible;
+};
+
+module.exports = {
+ deleteCollapsibleMutation,
+ deleteCollapsible,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/index.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/index.js
new file mode 100644
index 0000000000..51150d8bc7
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/index.js
@@ -0,0 +1,7 @@
+module.exports = {
+ ...require("./create"),
+ ...require("./query"),
+ ...require("./update"),
+ ...require("./move"),
+ ...require("./delete"),
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/move.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/move.js
new file mode 100644
index 0000000000..f92d2674c9
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/move.js
@@ -0,0 +1,29 @@
+const executeQuery = require("../../executeQuery");
+
+const moveCollapsibleMutation = `
+ mutation MoveCollapsible($input: MoveCollapsibleInput!) {
+ moveCollapsible(input: $input) {
+ id
+ introduction {
+ id
+ collapsibles {
+ id
+ }
+ }
+ }
+ }
+`;
+
+const moveCollapsible = async (questionnaire, input) => {
+ const result = await executeQuery(
+ moveCollapsibleMutation,
+ { input },
+ { questionnaire }
+ );
+ return result.data.moveCollapsible;
+};
+
+module.exports = {
+ moveCollapsibleMutation,
+ moveCollapsible,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/query.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/query.js
new file mode 100644
index 0000000000..ed019748d9
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/query.js
@@ -0,0 +1,31 @@
+const executeQuery = require("../../executeQuery");
+
+const queryCollapsibleQuery = `
+ query GetIntroductionCollapsibles($introductionId: ID!) {
+ questionnaireIntroduction(id: $introductionId) {
+ id
+ collapsibles {
+ id
+ title
+ description
+ introduction {
+ id
+ }
+ }
+ }
+ }
+`;
+
+const queryCollapsible = async (questionnaire, introductionId) => {
+ const result = await executeQuery(
+ queryCollapsibleQuery,
+ { introductionId },
+ { questionnaire }
+ );
+ return result.data.questionnaireIntroduction.collapsibles;
+};
+
+module.exports = {
+ queryCollapsibleQuery,
+ queryCollapsible,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/collapsible/update.js b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/update.js
new file mode 100644
index 0000000000..2b3fcf8391
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/collapsible/update.js
@@ -0,0 +1,25 @@
+const executeQuery = require("../../executeQuery");
+
+const updateCollapsibleMutation = `
+ mutation UpdateCollapsible($input: UpdateCollapsibleInput!) {
+ updateCollapsible(input: $input) {
+ id
+ title
+ description
+ }
+ }
+`;
+
+const updateCollapsible = async (questionnaire, input) => {
+ const result = await executeQuery(
+ updateCollapsibleMutation,
+ { input },
+ { questionnaire }
+ );
+ return result.data.updateCollapsible;
+};
+
+module.exports = {
+ updateCollapsibleMutation,
+ updateCollapsible,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/index.js b/eq-author-api/tests/utils/questionnaireBuilder/index.js
index 6ce9ca1dc7..f7bd023050 100644
--- a/eq-author-api/tests/utils/questionnaireBuilder/index.js
+++ b/eq-author-api/tests/utils/questionnaireBuilder/index.js
@@ -15,22 +15,29 @@ const {
deleteOption,
} = require("./option");
const { createCalculatedSummaryPage } = require("./page/calculatedSummary");
-
const {
createQuestionConfirmation,
updateQuestionConfirmation,
} = require("./questionConfirmation");
+const {
+ updateQuestionnaireIntroduction,
+} = require("./questionnaireIntroduction");
+const { createCollapsible } = require("./collapsible");
const buildRouting = require("./buildRouting");
//@todo - Split into smaller functions to avoid deeply nested chaining
const buildQuestionnaire = async questionnaireConfig => {
- const { sections, metadata, ...questionnaireProps } = questionnaireConfig;
+ const {
+ sections,
+ metadata,
+ introduction,
+ ...questionnaireProps
+ } = questionnaireConfig;
const questionnaire = await createQuestionnaireReturningPersisted({
title: "Questionnaire",
surveyId: "1",
theme: "default",
- legalBasis: "Voluntary",
navigation: false,
type: SOCIAL,
...questionnaireProps,
@@ -126,6 +133,24 @@ const buildQuestionnaire = async questionnaireConfig => {
}
}
+ if (introduction) {
+ const { collapsibles, ...introductionProps } = introduction;
+ if (Object.keys(introductionProps).length > 0) {
+ await updateQuestionnaireIntroduction(questionnaire, {
+ id: questionnaire.introduction.id,
+ ...introductionProps,
+ });
+ }
+ if (Array.isArray(collapsibles)) {
+ for (let i = 0; i < collapsibles.length; ++i) {
+ await createCollapsible(questionnaire, {
+ introductionId: questionnaire.introduction.id,
+ ...collapsibles[i],
+ });
+ }
+ }
+ }
+
await buildRouting(questionnaire, questionnaireConfig);
const getResult = await getQuestionnaire(questionnaire.id);
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/createQuestionnaire.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/createQuestionnaire.js
index 670b9c89d2..94f5ecf66d 100644
--- a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/createQuestionnaire.js
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/createQuestionnaire.js
@@ -9,7 +9,6 @@ const createQuestionnaireMutation = `
displayName
description
theme
- legalBasis
navigation
type
surveyId
@@ -30,6 +29,13 @@ const createQuestionnaireMutation = `
metadata {
id
}
+ introduction {
+ id
+ title
+ collapsibles {
+ id
+ }
+ }
}
}
`;
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/duplicateQuestionnaire.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/duplicateQuestionnaire.js
index 81994c6e51..b18ce865c2 100644
--- a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/duplicateQuestionnaire.js
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/duplicateQuestionnaire.js
@@ -9,7 +9,6 @@ const duplicateQuestionnaireMutation = `
displayName
description
theme
- legalBasis
navigation
surveyId
createdAt
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/queryQuestionnaire.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/queryQuestionnaire.js
index 3bb3e76431..1dd14e170c 100644
--- a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/queryQuestionnaire.js
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/queryQuestionnaire.js
@@ -7,7 +7,6 @@ const getQuestionnaireQuery = `
title
description
theme
- legalBasis
navigation
surveyId
createdAt
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/updateQuestionnaire.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/updateQuestionnaire.js
index 2188d33161..c10dc3f2c2 100644
--- a/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/updateQuestionnaire.js
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaire/updateQuestionnaire.js
@@ -7,7 +7,6 @@ const updateQuestionnaireMutation = `
title
description
theme
- legalBasis
navigation
surveyId
summary
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/index.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/index.js
new file mode 100644
index 0000000000..f3a2dfc15b
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/index.js
@@ -0,0 +1,4 @@
+module.exports = {
+ ...require("./query"),
+ ...require("./update"),
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/query.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/query.js
new file mode 100644
index 0000000000..a759f19a6b
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/query.js
@@ -0,0 +1,42 @@
+const executeQuery = require("../../executeQuery");
+
+const queryIntroductionQuery = `
+ query GetIntroduction($introductionId: ID!) {
+ questionnaireIntroduction(id: $introductionId) {
+ id
+ title
+ description
+ secondaryTitle
+ secondaryDescription
+ collapsibles {
+ id
+ }
+ legalBasis
+ tertiaryTitle
+ tertiaryDescription
+ availablePipingAnswers {
+ id
+ }
+ availablePipingMetadata {
+ id
+ }
+ }
+ }
+`;
+
+const queryQuestionnaireIntroduction = async (
+ questionnaire,
+ introductionId
+) => {
+ const result = await executeQuery(
+ queryIntroductionQuery,
+ { introductionId },
+ { questionnaire }
+ );
+ return result.data.questionnaireIntroduction;
+};
+
+module.exports = {
+ queryIntroductionQuery,
+ queryQuestionnaireIntroduction,
+};
diff --git a/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/update.js b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/update.js
new file mode 100644
index 0000000000..0265169fb8
--- /dev/null
+++ b/eq-author-api/tests/utils/questionnaireBuilder/questionnaireIntroduction/update.js
@@ -0,0 +1,33 @@
+const executeQuery = require("../../executeQuery");
+
+const updateQuestionnaireIntroductionMutation = `
+ mutation UpdateQuestionnaireIntroduction($input: UpdateQuestionnaireIntroductionInput!) {
+ updateQuestionnaireIntroduction(input: $input) {
+ id
+ title
+ description
+ secondaryTitle
+ secondaryDescription
+ collapsibles {
+ id
+ }
+ legalBasis
+ tertiaryTitle
+ tertiaryDescription
+ }
+ }
+`;
+
+const updateQuestionnaireIntroduction = async (questionnaire, input) => {
+ const result = await executeQuery(
+ updateQuestionnaireIntroductionMutation,
+ { input },
+ { questionnaire }
+ );
+ return result.data.updateQuestionnaireIntroduction;
+};
+
+module.exports = {
+ updateQuestionnaireIntroductionMutation,
+ updateQuestionnaireIntroduction,
+};
diff --git a/eq-author-api/utils/datastoreDynamo.js b/eq-author-api/utils/datastoreDynamo.js
index f21631032e..6071b1a4dc 100644
--- a/eq-author-api/utils/datastoreDynamo.js
+++ b/eq-author-api/utils/datastoreDynamo.js
@@ -123,6 +123,16 @@ const saveQuestionnaire = async (questionnaireModel, count = 0, patch) => {
);
const latestQuestionnaire = await getQuestionnaire(questionnaireModel.id);
+
+ // We are done if we match the latest questionnaire
+ const diffToLatest = diffPatcher.diff(
+ omitTimestamps(latestQuestionnaire),
+ omitTimestamps(questionnaireModel)
+ );
+ if (!diffToLatest) {
+ return;
+ }
+
diffPatcher.patch(latestQuestionnaire, patchToApply);
await saveQuestionnaire(latestQuestionnaire, ++count, patchToApply);
}
diff --git a/eq-author-api/utils/defaultMetadata.js b/eq-author-api/utils/defaultMetadata.js
index 9ba2beadfa..1e785d4693 100644
--- a/eq-author-api/utils/defaultMetadata.js
+++ b/eq-author-api/utils/defaultMetadata.js
@@ -1,6 +1,5 @@
const uuid = require("uuid");
-const { includes } = require("lodash");
-const { filter, flow, map } = require("lodash/fp");
+const { includes, filter } = require("lodash");
const defaultTypeValueNames = {
Date: "dateValue",
@@ -97,29 +96,29 @@ const defaultValues = [
},
];
-const defaultBusinessSurveyMetadata = flow(
- filter(({ key }) =>
- includes(
- [
- "ru_name",
- "trad_as",
- "ref_p_start_date",
- "ref_p_end_date",
- "period_id",
- "employmentDate",
- ],
- key
- )
- ),
- map(metadata => ({
+const DEFAULT_BUSINESS_SURVEY_METADATA = filter(defaultValues, ({ key }) =>
+ includes(
+ [
+ "ru_name",
+ "trad_as",
+ "ref_p_start_date",
+ "ref_p_end_date",
+ "period_id",
+ "employmentDate",
+ ],
+ key
+ )
+);
+
+const createDefaultBusinessSurveyMetadata = () =>
+ DEFAULT_BUSINESS_SURVEY_METADATA.map(metadata => ({
id: uuid.v4(),
...metadata,
- }))
-)(defaultValues);
+ }));
Object.assign(module.exports, {
defaultTypeValueNames,
defaultTypeValues,
defaultValues,
- defaultBusinessSurveyMetadata,
+ createDefaultBusinessSurveyMetadata,
});
diff --git a/eq-author/cypress/builders/metadata.js b/eq-author/cypress/builders/metadata.js
index 0686c4790a..99c6455202 100644
--- a/eq-author/cypress/builders/metadata.js
+++ b/eq-author/cypress/builders/metadata.js
@@ -1,6 +1,6 @@
import { selectOptionByLabel, testId } from "../utils";
-export const addMetadata = (metadataKey, type) => {
+export const addMetadata = (metadataKey, type, existingCount = 0) => {
cy.get(testId("metadata-btn")).as("metadataBtn");
cy.get("@metadataBtn").should("be.visible");
cy.get("@metadataBtn").click();
@@ -9,10 +9,14 @@ export const addMetadata = (metadataKey, type) => {
cy.get("@addMetadataBtn").should("be.visible");
cy.get("@addMetadataBtn").click();
- cy.get(testId("metadata-table-row")).within(() => {
- cy.get("[name='key']").as("metadataKey");
- cy.get("[name='type']").within(() => selectOptionByLabel(type));
- });
+ cy.get(testId("metadata-table-row")).should("have.length", existingCount + 1);
+
+ cy.get(testId("metadata-table-row"))
+ .last()
+ .within(() => {
+ cy.get("[name='key']").as("metadataKey");
+ cy.get("[name='type']").within(() => selectOptionByLabel(type));
+ });
cy.get("@metadataKey").type(metadataKey);
cy.get("@metadataKey").should("have.value", metadataKey);
diff --git a/eq-author/cypress/builders/questionnaire.js b/eq-author/cypress/builders/questionnaire.js
index 5f92068ebc..e379e4b77a 100644
--- a/eq-author/cypress/builders/questionnaire.js
+++ b/eq-author/cypress/builders/questionnaire.js
@@ -2,13 +2,13 @@ import { testId, idRegex } from "../utils";
const checkIsOnDesignPage = () => cy.hash().should("match", /\/design$/);
-const updateDetails = ({ title }) => {
+const updateDetails = ({ title, type = "Social" }) => {
cy.get(testId("questionnaire-settings-modal")).within(() => {
cy.get(testId("txt-questionnaire-title"))
.clear()
.type(title);
cy.get("label[for='navigation']").click();
- cy.get(testId("select-questionnaire-type")).select("Social");
+ cy.get(testId("select-questionnaire-type")).select(type);
cy.get("form").submit();
});
diff --git a/eq-author/cypress/fixtures/publisher.json b/eq-author/cypress/fixtures/publisher.json
index 6ab32c9ef1..775ea0bb5f 100644
--- a/eq-author/cypress/fixtures/publisher.json
+++ b/eq-author/cypress/fixtures/publisher.json
@@ -1,36 +1,75 @@
{
- "eq_id": "1",
- "form_type": "1",
+ "eq_id": "3e947e58-bc66-439f-834d-82d80a45c53b",
+ "form_type": "3e947e58-bc66-439f-834d-82d80a45c53b",
"mime_type": "application/json/ons/eq",
"schema_version": "0.0.1",
"data_version": "0.0.2",
- "survey_id": "1",
+ "survey_id": "ukis",
"title": "UKIS",
- "view_submitted_response": {
- "duration": 900,
- "enabled": true
- },
"sections": [
{
- "id": "section1",
+ "id": "section584466ca-2485-4e32-b5ac-352bd7a26d2a",
"title": "General Business Information",
"groups": [
{
- "id": "group1",
+ "id": "group584466ca-2485-4e32-b5ac-352bd7a26d2a",
"title": "General Business Information",
"blocks": [
{
- "id": "block1",
+ "type": "Introduction",
+ "id": "introduction-block",
+ "primary_content": [
+ {
+ "type": "Basic",
+ "id": "primary",
+ "content": [
+ {
+ "list": [
+ "Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated.",
+ "You can provide info estimates if actual figures aren’t available.",
+ "We will treat your data securely and confidentially."
+ ]
+ }
+ ]
+ }
+ ],
+ "preview_content": {
+ "id": "preview",
+ "title": "Information you need",
+ "content": [
+ {
+ "description": "You can select the dates of the period you are reporting for, if the given dates are not appropriate."
+ }
+ ],
+ "questions": []
+ },
+ "secondary_content": [
+ {
+ "id": "secondary-content",
+ "title": "How we use your data",
+ "content": [
+ {
+ "list": [
+ "You cannot appeal your selection. Your business was selected to give us a comprehensive view of the UK economy.",
+ "The information you provide contributes to Gross Domestic Product (GDP)."
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "blockcecdd089-eac6-4cef-a6a5-98e829307a7a",
"type": "Question",
"questions": [
{
- "id": "question1",
+ "id": "questioncecdd089-eac6-4cef-a6a5-98e829307a7a",
"title": "In which geographic markets did Enterprise Ltd offer goods and / or services?",
"description": "Here is the question description
",
"type": "General",
"answers": [
{
- "id": "answer1",
+ "id": "answerdeafc370-5c27-4d21-943a-6d19c6443146",
"mandatory": false,
"type": "Checkbox",
"label": "",
@@ -92,8 +131,8 @@
]
}
],
- "theme": "social",
- "legal_basis": "StatisticsOfTradeAct",
+ "theme": "default",
+ "legal_basis": "Notice is given under section 1 of the Statistics of Trade Act 1947.",
"navigation": {
"visible": true
},
@@ -109,6 +148,26 @@
{
"name": "ru_name",
"validator": "string"
+ },
+ {
+ "name": "trad_as",
+ "validator": "string"
+ },
+ {
+ "name": "ref_p_start_date",
+ "validator": "date"
+ },
+ {
+ "name": "ref_p_end_date",
+ "validator": "date"
+ },
+ {
+ "name": "employmentDate",
+ "validator": "date"
}
- ]
-}
+ ],
+ "view_submitted_response": {
+ "enabled": true,
+ "duration": 900
+ }
+}
\ No newline at end of file
diff --git a/eq-author/cypress/integration/authenticated/introduction_spec.js b/eq-author/cypress/integration/authenticated/introduction_spec.js
new file mode 100644
index 0000000000..db307a7ab7
--- /dev/null
+++ b/eq-author/cypress/integration/authenticated/introduction_spec.js
@@ -0,0 +1,135 @@
+import { idRegex, testId, findInputByLabel } from "../../utils";
+import { questionnaire } from "../../builders";
+
+describe("Questionnaire Introduction", () => {
+ const questionnaireTitle = "Questionnaire Introduction Test";
+
+ afterEach(() => {
+ cy.deleteQuestionnaire(questionnaireTitle);
+ });
+
+ beforeEach(() => {
+ cy.visit("/");
+ cy.login();
+ questionnaire.add({ title: questionnaireTitle, type: "Business" });
+ });
+
+ it("should be the default launch page", () => {
+ const pattern = new RegExp(`/q/${idRegex}/introduction/${idRegex}/design`);
+ cy.hash().should("match", pattern);
+
+ cy.get(testId("logo")).click();
+ cy.contains(questionnaireTitle).click();
+
+ cy.hash().should("match", pattern);
+ });
+
+ it("should allow modification of the properties", () => {
+ cy.get(testId("txt-intro-description", "testid")).should("to.not.be.empty");
+ cy.get(testId("txt-intro-description", "testid"))
+ .clear()
+ .type("Introduction description")
+ .blur();
+
+ cy.get(testId("txt-intro-secondary-title", "testid")).should(
+ "to.not.be.empty"
+ );
+ cy.get(testId("txt-intro-secondary-title", "testid"))
+ .clear()
+ .type("Secondary title")
+ .blur();
+
+ cy.get(testId("txt-intro-secondary-description", "testid")).should(
+ "to.not.be.empty"
+ );
+ cy.get(testId("txt-intro-secondary-description", "testid"))
+ .clear()
+ .type("Secondary description")
+ .blur();
+
+ findInputByLabel("Notice 2").click();
+ cy.get(`${testId("intro-legal-basis")} [value='NOTICE_2']`).should(
+ "be.checked"
+ );
+
+ cy.get(testId("txt-intro-tertiary-title", "testid")).should(
+ "to.not.be.empty"
+ );
+ cy.get(testId("txt-intro-tertiary-title", "testid"))
+ .clear()
+ .type("tertiary title")
+ .blur();
+
+ cy.get(testId("txt-intro-tertiary-description", "testid")).should(
+ "to.not.be.empty"
+ );
+ cy.get(testId("txt-intro-tertiary-description", "testid"))
+ .clear()
+ .type("tertiary description")
+ .blur();
+ });
+
+ it("should be able to add and modify collapsibles", () => {
+ cy.get(testId("collapsible-editor")).should("have.length", 0);
+ cy.get(testId("add-collapsible-btn")).click();
+ cy.get(testId("collapsible-editor")).should("have.length", 1);
+
+ cy.get(testId("txt-collapsible-title"))
+ .type("Collapsible title")
+ .blur();
+
+ cy.get(testId("txt-collapsible-description", "testid"))
+ .type("Collapsible description")
+ .blur();
+ });
+
+ it("should be able to re-order collapsibles", () => {
+ cy.get(testId("collapsible-editor")).should("have.length", 0);
+
+ cy.get(testId("add-collapsible-btn")).click();
+ cy.get(testId("collapsible-editor")).should("have.length", 1);
+ cy.get(testId("txt-collapsible-title"))
+ .eq(0)
+ .type("Title 1")
+ .blur();
+
+ cy.get(testId("add-collapsible-btn")).click();
+ cy.get(testId("collapsible-editor")).should("have.length", 2);
+ cy.get(testId("txt-collapsible-title"))
+ .eq(1)
+ .type("Title 2")
+ .blur();
+
+ cy.get(testId("move-up-btn"))
+ .eq(1)
+ .click();
+
+ cy.contains("Untitled Section").click();
+ cy.hash().should("match", new RegExp(`/q/${idRegex}/section`));
+ cy.get(testId("side-nav")).within(() => {
+ cy.contains("Introduction").click();
+ });
+ cy.hash().should("match", new RegExp(`/q/${idRegex}/introduction`));
+
+ cy.get(testId("txt-collapsible-title")).should("have.length", 2);
+
+ cy.get(testId("txt-collapsible-title"))
+ .eq(0)
+ .should("contain", "Title 2");
+ });
+
+ it("should be able to delete collapsibles", () => {
+ cy.get(testId("collapsible-editor")).should("have.length", 0);
+ cy.get(testId("add-collapsible-btn")).click();
+ cy.get(testId("collapsible-editor")).should("have.length", 1);
+ cy.get(testId("delete-collapsible-btn")).click();
+ cy.get(testId("collapsible-editor")).should("have.length", 0);
+ });
+
+ it("should be previewable", () => {
+ cy.get(testId("preview"))
+ .contains("Preview")
+ .click();
+ cy.hash().should("match", /\/preview$/);
+ });
+});
diff --git a/eq-author/cypress/integration/authenticated/piping_spec.js b/eq-author/cypress/integration/authenticated/piping_spec.js
index 4d3cee9ee7..63a8d85526 100644
--- a/eq-author/cypress/integration/authenticated/piping_spec.js
+++ b/eq-author/cypress/integration/authenticated/piping_spec.js
@@ -5,7 +5,6 @@ import {
addSection,
testId,
selectFirstAnswerFromContentPicker,
- selectFirstMetadataContentPicker,
enableDescription,
enableGuidance,
} from "../../utils";
@@ -61,13 +60,23 @@ const clickLastSection = () =>
.last()
.click({ force: true }); //Metadata modal transition is sometimes too slow
+const selectMetadata = () => {
+ cy.get(testId("picker-option"))
+ .contains(METADATA)
+ .click();
+
+ cy.get(testId("submit-button")).click();
+};
+
const canPipeMetadata = ({ selector }) => {
cy.get(testId(selector, "testid")).click();
cy.focused()
.should("have.attr", "data-testid")
.and("eq", selector);
clickPipingButton(selector);
- selectFirstMetadataContentPicker();
+
+ selectMetadata();
+
cy.get(testId(selector, "testid")).should("contain", `[${METADATA}]`);
};
@@ -76,11 +85,12 @@ describe("Piping", () => {
beforeEach(() => {
cy.visit("/");
cy.login();
- addQuestionnaire(questionnaireTitle);
+ addQuestionnaire(questionnaireTitle, "Business");
});
describe("Answers", () => {
beforeEach(() => {
+ clickFirstPage();
addAnswerType("Number");
cy.get(testId("txt-answer-label")).type(ANSWER);
addSection();
@@ -155,9 +165,12 @@ describe("Piping", () => {
describe("Metadata", () => {
beforeEach(() => {
- addMetadata(METADATA, "Text");
+ addMetadata(METADATA, "Text", 6);
});
describe("Page", () => {
+ beforeEach(() => {
+ clickFirstPage();
+ });
it("Can pipe metadata into page title", () => {
cy.get(testId("txt-question-title", "testid")).type("title");
canPipeMetadata({ selector: "txt-question-title" });
@@ -200,6 +213,7 @@ describe("Piping", () => {
describe("Question Confirmation", () => {
beforeEach(() => {
+ clickFirstPage();
questionConfirmation.add();
});
@@ -210,6 +224,15 @@ describe("Piping", () => {
canPipeMetadata({ selector: "txt-confirmation-title" });
});
});
+
+ describe("Questionnaire Introduction", () => {
+ it("Can pipe metadata into description", () => {
+ cy.get(testId("txt-intro-description", "testid"))
+ .clear()
+ .type("intro description");
+ canPipeMetadata({ selector: "txt-intro-description" });
+ });
+ });
});
afterEach(() => {
diff --git a/eq-author/cypress/integration/authenticated/routing_spec.js b/eq-author/cypress/integration/authenticated/routing_spec.js
index 6d2ed8cf2c..b188270290 100644
--- a/eq-author/cypress/integration/authenticated/routing_spec.js
+++ b/eq-author/cypress/integration/authenticated/routing_spec.js
@@ -50,6 +50,7 @@ describe("Routing", () => {
title = "Test no routing rules";
cy.createQuestionnaire(title);
+ cy.contains("Untitled Page").click();
typeIntoDraftEditor(testId("txt-question-title", "testid"), "Question 1");
buildMultipleChoiceAnswer(["A", "B", "C"]);
@@ -67,6 +68,7 @@ describe("Routing", () => {
title = "Test routing destination";
cy.createQuestionnaire(title);
+ cy.contains("Untitled Page").click();
typeIntoDraftEditor(testId("txt-question-title", "testid"), "Question 1");
buildMultipleChoiceAnswer(["A", "B", "C"]);
@@ -99,6 +101,7 @@ describe("Routing", () => {
it(`should be able to add a ${type} routing rule and edit the inputs`, () => {
title = `Test add ${type}`;
cy.createQuestionnaire(title);
+ cy.contains("Untitled Page").click();
typeIntoDraftEditor(testId("txt-question-title", "testid"), "Question 1");
addAnswerType(type);
@@ -116,6 +119,7 @@ describe("Routing", () => {
it("follows the link to add an answer and routing updates with the new answer", () => {
title = "Test no answer";
cy.createQuestionnaire(title);
+ cy.contains("Untitled Page").click();
typeIntoDraftEditor(testId("txt-question-title", "testid"), "Question 1");
@@ -150,9 +154,7 @@ describe("Routing", () => {
title = "Test OR rules";
cy.createQuestionnaire(title);
- cy.get(testId("nav-section-link"))
- .first()
- .click();
+ cy.contains("Untitled Section").click();
typeIntoDraftEditor(testId("txt-section-title", "testid"), "Section 1");
cy.get(testId("nav-page-link"))
@@ -253,6 +255,7 @@ describe("Routing", () => {
title = "Test future question";
cy.createQuestionnaire(title);
+ cy.contains("Untitled Page").click();
typeIntoDraftEditor(testId("txt-question-title", "testid"), "Question 1");
buildMultipleChoiceAnswer(["A", "B", "C"]);
diff --git a/eq-author/cypress/integration/e2e/e2e_spec.js b/eq-author/cypress/integration/e2e/e2e_spec.js
index bfa3e03704..71dbb4c7b8 100644
--- a/eq-author/cypress/integration/e2e/e2e_spec.js
+++ b/eq-author/cypress/integration/e2e/e2e_spec.js
@@ -29,7 +29,7 @@ describe("End to end", () => {
it("Can create a questionnaire", () => {
cy.visit("/");
cy.login();
- questionnaire.add({ title: "UKIS" }).then(({ id }) => {
+ questionnaire.add({ title: "UKIS", type: "Business" }).then(({ id }) => {
questionnaireId = id;
});
section.updateInitial({
diff --git a/eq-author/cypress/utils/index.js b/eq-author/cypress/utils/index.js
index 005072b017..83f1abd85d 100644
--- a/eq-author/cypress/utils/index.js
+++ b/eq-author/cypress/utils/index.js
@@ -44,9 +44,9 @@ export function setQuestionnaireSettings({ title, type, shortTitle }) {
});
}
-export const addQuestionnaire = title => {
+export const addQuestionnaire = (title, type = "Social") => {
cy.get(testId("create-questionnaire")).click();
- setQuestionnaireSettings({ title, type: "Social" });
+ setQuestionnaireSettings({ title, type });
};
export const addSection = (initialNumberOfSections = 1) => {
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/IntroductionNavItem.js b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/IntroductionNavItem.js
new file mode 100644
index 0000000000..b01e142f91
--- /dev/null
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/IntroductionNavItem.js
@@ -0,0 +1,60 @@
+import React from "react";
+import styled from "styled-components";
+import { withRouter } from "react-router-dom";
+import CustomPropTypes from "custom-prop-types";
+import gql from "graphql-tag";
+import { propType } from "graphql-anywhere";
+
+import { buildIntroductionPath } from "utils/UrlUtils";
+import NavLink from "./NavLink";
+import PageIcon from "./icon-survey-intro.svg?inline";
+
+const StyledItem = styled.li`
+ padding: 0;
+ margin: 0;
+ position: relative;
+ display: flex;
+ align-items: center;
+ font-weight: bold;
+`;
+
+export const UnwrappedIntroductionNavItem = ({
+ questionnaire,
+ match,
+ ...otherProps
+}) => (
+
+
+ Introduction
+
+
+);
+
+UnwrappedIntroductionNavItem.fragments = {
+ IntroductionNavItem: gql`
+ fragment IntroductionNavItem on Questionnaire {
+ id
+ introduction {
+ id
+ }
+ }
+ `,
+};
+
+UnwrappedIntroductionNavItem.propTypes = {
+ questionnaire: propType(
+ UnwrappedIntroductionNavItem.fragments.IntroductionNavItem
+ ),
+ match: CustomPropTypes.match,
+};
+
+export default withRouter(UnwrappedIntroductionNavItem);
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/__snapshots__/index.test.js.snap b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/__snapshots__/index.test.js.snap
index e64e540c5e..2a61a76333 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/__snapshots__/index.test.js.snap
@@ -39,27 +39,31 @@ exports[`NavigationSidebar should render 1`] = `
}
/>
-
+
+
+ "title": "Questionnaire",
+ }
+ }
+ />
+
+
`;
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/icon-survey-intro.svg b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/icon-survey-intro.svg
new file mode 100644
index 0000000000..eb3ae86c8c
--- /dev/null
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/icon-survey-intro.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.js b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.js
index bc7ecf7451..2bd11ed6a2 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.js
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.js
@@ -12,6 +12,7 @@ import withUpdateQuestionnaire from "./withUpdateQuestionnaire";
import SectionNav from "./SectionNav";
import NavigationHeader from "./NavigationHeader";
+import IntroductionNavItem from "./IntroductionNavItem";
const Container = styled.div`
background: ${colors.darkBlue};
@@ -29,6 +30,12 @@ const NavigationScrollPane = styled(ScrollPane)`
}
`;
+const NavList = styled.ol`
+ margin: 0;
+ padding: 0;
+ list-style: none;
+`;
+
export class UnwrappedNavigationSidebar extends Component {
static propTypes = {
questionnaire: CustomPropTypes.questionnaire,
@@ -71,7 +78,17 @@ export class UnwrappedNavigationSidebar extends Component {
data-test="nav-section-header"
/>
-
+
+ {questionnaire.introduction && (
+
+ )}
+
+
+
+
)}
@@ -86,10 +103,12 @@ UnwrappedNavigationSidebar.fragments = {
id
...SectionNav
...NavigationHeader
+ ...IntroductionNavItem
}
${NavigationHeader.fragments.NavigationHeader}
${SectionNav.fragments.SectionNav}
+ ${IntroductionNavItem.fragments.IntroductionNavItem}
`,
};
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.test.js b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.test.js
index efa7408566..f322f1e1ff 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.test.js
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/index.test.js
@@ -4,63 +4,57 @@ import { UnwrappedNavigationSidebar as NavigationSidebar } from "./";
import { SynchronousPromise } from "synchronous-promise";
describe("NavigationSidebar", () => {
- let wrapper,
- handleAddSection,
- handleAddQuestionPage,
- handleUpdateQuestionnaire,
- handleAddCalculatedSummaryPage,
- handleAddQuestionConfirmation;
-
- const page = { id: "2", title: "Page", position: 0 };
- const section = { id: "3", title: "Section", pages: [page] };
- const questionnaire = {
- id: "1",
- title: "Questionnaire",
- sections: [section],
- };
-
+ let props;
beforeEach(() => {
- handleAddSection = jest.fn(() => SynchronousPromise.resolve(questionnaire));
- handleAddQuestionPage = jest.fn(() =>
- SynchronousPromise.resolve({ section })
- );
- handleUpdateQuestionnaire = jest.fn();
- handleAddQuestionConfirmation = jest.fn();
- handleAddCalculatedSummaryPage = jest.fn();
-
- wrapper = shallow(
-
- );
+ const page = { id: "2", title: "Page", position: 0 };
+ const section = { id: "3", title: "Section", pages: [page] };
+ const questionnaire = {
+ id: "1",
+ title: "Questionnaire",
+ sections: [section],
+ };
+ props = {
+ questionnaire,
+ onAddSection: jest.fn(() => SynchronousPromise.resolve(questionnaire)),
+ onAddQuestionPage: jest.fn(() => SynchronousPromise.resolve({ section })),
+ onAddCalculatedSummaryPage: jest.fn(),
+ onUpdateQuestionnaire: jest.fn(),
+ onAddQuestionConfirmation: jest.fn(),
+ canAddQuestionConfirmation: true,
+ loading: false,
+ };
});
it("should render", () => {
+ const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
});
it("should only render container if loading", () => {
- wrapper.setProps({ loading: true });
+ const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
});
it("should allow sections to be added", () => {
+ const wrapper = shallow();
wrapper.find("[data-test='nav-section-header']").simulate("addSection");
- expect(handleAddSection).toHaveBeenCalledWith(questionnaire.id);
+ expect(props.onAddSection).toHaveBeenCalledWith(props.questionnaire.id);
});
it("should allow pages to be added", () => {
+ const wrapper = shallow();
wrapper
.find("[data-test='nav-section-header']")
.simulate("addQuestionPage");
- expect(handleAddQuestionPage).toHaveBeenCalledWith();
+ expect(props.onAddQuestionPage).toHaveBeenCalledWith();
+ });
+
+ it("should render an introduction nav item when the questionnaire has one", () => {
+ props.questionnaire.introduction = {
+ id: "1",
+ };
+ const wrapper = shallow();
+ expect(wrapper.find("[data-test='nav-introduction']")).toHaveLength(1);
});
});
diff --git a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/withUpdateQuestionnaire.js b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/withUpdateQuestionnaire.js
index b546bbd71a..73e8b4a3b2 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/withUpdateQuestionnaire.js
+++ b/eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/withUpdateQuestionnaire.js
@@ -11,7 +11,6 @@ const inputStructure = gql`
title
description
theme
- legalBasis
navigation
surveyId
summary
diff --git a/eq-author/src/App/QuestionnaireDesignPage/__snapshots__/index.test.js.snap b/eq-author/src/App/QuestionnaireDesignPage/__snapshots__/index.test.js.snap
index 2a9c07ac4f..7d7902f92a 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/QuestionnaireDesignPage/__snapshots__/index.test.js.snap
@@ -4,6 +4,13 @@ exports[`QuestionnaireDesignPage getTitle should display existing title if loadi
exports[`QuestionnaireDesignPage getTitle should display questionnaire title if no longer loading 1`] = `"hello world - foo"`;
+exports[`QuestionnaireDesignPage should redirect to the introduction if it has one 1`] = `
+
+`;
+
exports[`QuestionnaireDesignPage should render 1`] = `
+
+
+ );
+ }
+
return (
@@ -217,6 +230,9 @@ const withMutations = flowRight(
const QUESTIONNAIRE_QUERY = gql`
query GetQuestionnaire($input: QueryInput!) {
questionnaire(input: $input) {
+ introduction {
+ id
+ }
...NavigationSidebar
}
}
diff --git a/eq-author/src/App/QuestionnaireDesignPage/index.test.js b/eq-author/src/App/QuestionnaireDesignPage/index.test.js
index 5c841869b2..5b49cca7a7 100644
--- a/eq-author/src/App/QuestionnaireDesignPage/index.test.js
+++ b/eq-author/src/App/QuestionnaireDesignPage/index.test.js
@@ -91,6 +91,20 @@ describe("QuestionnaireDesignPage", () => {
expect(wrapper.instance().renderRedirect()).toMatchSnapshot();
});
+ it("should redirect to the introduction if it has one", () => {
+ wrapper.setProps({
+ data: {
+ questionnaire: {
+ ...questionnaire,
+ introduction: {
+ id: "1",
+ },
+ },
+ },
+ });
+ expect(wrapper.instance().renderRedirect()).toMatchSnapshot();
+ });
+
describe("onAddQuestionPage", () => {
it("should add new page below current page", () => {
wrapper.find(NavigationSidebar).simulate("addQuestionPage");
diff --git a/eq-author/src/App/QuestionnaireSettingsModal/QuestionnaireMeta/QuestionnaireMeta.test.js b/eq-author/src/App/QuestionnaireSettingsModal/QuestionnaireMeta/QuestionnaireMeta.test.js
index 73a6c35dcf..2b89ae02e5 100644
--- a/eq-author/src/App/QuestionnaireSettingsModal/QuestionnaireMeta/QuestionnaireMeta.test.js
+++ b/eq-author/src/App/QuestionnaireSettingsModal/QuestionnaireMeta/QuestionnaireMeta.test.js
@@ -13,7 +13,6 @@ const handleUpdate = jest.fn();
const questionnaire = {
shortTitle: "I am the shortTitle",
- legalBasis: "StatisticsOfTradeAct",
type: "",
title: "I am the title",
id: "123",
diff --git a/eq-author/src/App/QuestionnaireSettingsModal/index.js b/eq-author/src/App/QuestionnaireSettingsModal/index.js
index c0d4338c23..c8113d1837 100644
--- a/eq-author/src/App/QuestionnaireSettingsModal/index.js
+++ b/eq-author/src/App/QuestionnaireSettingsModal/index.js
@@ -14,7 +14,6 @@ const defaultQuestionnaire = {
description: "",
surveyId: "",
theme: "default",
- legalBasis: "StatisticsOfTradeAct",
navigation: false,
};
diff --git a/eq-author/src/App/QuestionnairesPage/__snapshots__/index.test.js.snap b/eq-author/src/App/QuestionnairesPage/__snapshots__/index.test.js.snap
index 7dc7523df5..5015286207 100644
--- a/eq-author/src/App/QuestionnairesPage/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/QuestionnairesPage/__snapshots__/index.test.js.snap
@@ -31,7 +31,6 @@ exports[`components/QuestionnairesPage should not render table whilst data is lo
questionnaire={
Object {
"description": "",
- "legalBasis": "StatisticsOfTradeAct",
"navigation": false,
"surveyId": "",
"theme": "default",
@@ -233,7 +232,6 @@ exports[`components/QuestionnairesPage should render table when there are questi
questionnaire={
Object {
"description": "",
- "legalBasis": "StatisticsOfTradeAct",
"navigation": false,
"surveyId": "",
"theme": "default",
@@ -450,7 +448,6 @@ exports[`components/QuestionnairesPage should render when there are no questionn
questionnaire={
Object {
"description": "",
- "legalBasis": "StatisticsOfTradeAct",
"navigation": false,
"surveyId": "",
"theme": "default",
diff --git a/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.js b/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.js
index c301ee0457..601a22676b 100644
--- a/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.js
+++ b/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.js
@@ -1,17 +1,27 @@
import { graphql } from "react-apollo";
import createQuestionnaireQuery from "graphql/createQuestionnaire.graphql";
import getQuestionnaireList from "graphql/getQuestionnaireList.graphql";
-import { buildPagePath } from "utils/UrlUtils";
+import { buildPagePath, buildIntroductionPath } from "utils/UrlUtils";
export const redirectToDesigner = history => ({ data }) => {
const questionnaire = data.createQuestionnaire;
+
+ if (questionnaire.introduction) {
+ history.push(
+ buildIntroductionPath({
+ questionnaireId: questionnaire.id,
+ introductionId: questionnaire.introduction.id,
+ })
+ );
+ return;
+ }
+
const section = questionnaire.sections[0];
const page = section.pages[0];
history.push(
buildPagePath({
questionnaireId: questionnaire.id,
- sectionId: section.id,
pageId: page.id,
})
);
diff --git a/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.test.js b/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.test.js
index d44ee6767b..ff10734ace 100644
--- a/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.test.js
+++ b/eq-author/src/App/QuestionnairesPage/withCreateQuestionnaire.test.js
@@ -3,17 +3,17 @@ import {
mapMutateToProps,
updateQuestionnaireList,
} from "App/QuestionnairesPage/withCreateQuestionnaire";
-import { buildPagePath } from "utils/UrlUtils";
+import { buildPagePath, buildIntroductionPath } from "utils/UrlUtils";
import getQuestionnaireList from "graphql/getQuestionnaireList.graphql";
describe("withCreateQuestionnaire", () => {
- let history, mutate, results, user;
-
- const page = { id: "3" };
- const section = { id: "2", pages: [page] };
- const questionnaire = { id: "1", sections: [section] };
+ let history, mutate, results, user, page, section, questionnaire;
beforeEach(() => {
+ page = { id: "3" };
+ section = { id: "2", pages: [page] };
+ questionnaire = { id: "1", sections: [section] };
+
results = {
data: { createQuestionnaire: questionnaire },
};
@@ -30,13 +30,26 @@ describe("withCreateQuestionnaire", () => {
});
describe("redirectToDesigner", () => {
+ it("should redirect to the introduction if it has one", () => {
+ results.data.createQuestionnaire.introduction = {
+ id: "4",
+ };
+ redirectToDesigner(history)(results);
+
+ expect(history.push).toHaveBeenCalledWith(
+ buildIntroductionPath({
+ questionnaireId: questionnaire.id,
+ introductionId: "4",
+ })
+ );
+ });
+
it("should redirect to correct location", () => {
redirectToDesigner(history)(results);
expect(history.push).toHaveBeenCalledWith(
buildPagePath({
questionnaireId: questionnaire.id,
- sectionId: section.id,
pageId: page.id,
})
);
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000..672223cd14
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/__snapshots__/index.test.js.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CollapsibleEditor should render 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.js
new file mode 100644
index 0000000000..577100e5c1
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.js
@@ -0,0 +1,142 @@
+import React from "react";
+import { flowRight } from "lodash";
+import styled from "styled-components";
+import gql from "graphql-tag";
+import PropTypes from "prop-types";
+import { propType } from "graphql-anywhere";
+
+import DeleteButton from "components/buttons/DeleteButton";
+import MoveButton, { IconUp, IconDown } from "components/buttons/MoveButton";
+import RichTextEditor from "components/RichTextEditor";
+import { colors } from "constants/theme";
+import { Field, Label } from "components/Forms";
+import WrappingInput from "components/Forms/WrappingInput";
+
+import withChangeUpdate from "enhancers/withChangeUpdate";
+import withPropRenamed from "enhancers/withPropRenamed";
+import withEntityEditor from "components/withEntityEditor";
+
+import withUpdateCollapsible from "./withUpdateCollapsible";
+import withDeleteCollapsible from "./withDeleteCollapsible";
+
+const Detail = styled.div`
+ border: 1px solid ${colors.bordersLight};
+ padding: 2em 1em 0;
+ border-radius: 4px;
+ margin-bottom: 1em;
+ background: white;
+ position: relative;
+`;
+
+const DetailHeader = styled.div`
+ position: absolute;
+ top: 0.5em;
+ right: 0.5em;
+ display: flex;
+ justify-content: space-around;
+ z-index: 2;
+ width: 7em;
+`;
+
+const DetailDeleteButton = styled(DeleteButton)`
+ position: relative;
+`;
+
+export const CollapsibleEditor = ({
+ collapsible,
+ onChangeUpdate,
+ onChange,
+ onUpdate,
+ onMoveUp,
+ onMoveDown,
+ canMoveUp,
+ canMoveDown,
+ isMoving,
+ deleteCollapsible,
+}) => {
+ const { id, title, description } = collapsible;
+ return (
+
+
+
+
+
+
+
+
+ deleteCollapsible(collapsible)}
+ data-test="delete-collapsible-btn"
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+const fragment = gql`
+ fragment CollapsibleEditor on Collapsible {
+ id
+ title
+ description
+ }
+`;
+
+CollapsibleEditor.fragments = [fragment];
+
+CollapsibleEditor.propTypes = {
+ collapsible: propType(fragment).isRequired,
+ onChange: PropTypes.func.isRequired,
+ onUpdate: PropTypes.func.isRequired,
+ onChangeUpdate: PropTypes.func.isRequired,
+ onMoveUp: PropTypes.func.isRequired,
+ onMoveDown: PropTypes.func.isRequired,
+ canMoveUp: PropTypes.bool.isRequired,
+ canMoveDown: PropTypes.bool.isRequired,
+ isMoving: PropTypes.bool.isRequired,
+ deleteCollapsible: PropTypes.func.isRequired,
+};
+
+export default flowRight(
+ withUpdateCollapsible,
+ withDeleteCollapsible,
+ withPropRenamed("updateCollapsible", "onUpdate"),
+ withEntityEditor("collapsible"),
+ withChangeUpdate
+)(CollapsibleEditor);
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.test.js
new file mode 100644
index 0000000000..ed006f6da9
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/index.test.js
@@ -0,0 +1,107 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import { CollapsibleEditor } from "./";
+
+describe("CollapsibleEditor", () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ collapsible: { id: "1", title: "title", description: "description" },
+ onChangeUpdate: jest.fn(),
+ onChange: jest.fn(),
+ onUpdate: jest.fn(),
+ onMoveUp: jest.fn(),
+ onMoveDown: jest.fn(),
+ canMoveUp: false,
+ canMoveDown: false,
+ isMoving: false,
+ deleteCollapsible: jest.fn(),
+ };
+ });
+
+ it("should render", () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("should disable buttons whilst moving", () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find("[data-test='move-up-btn']").prop("disabled")).toEqual(
+ true
+ );
+ expect(
+ wrapper.find("[data-test='move-down-btn']").prop("disabled")
+ ).toEqual(true);
+ expect(
+ wrapper.find("[data-test='delete-collapsible-btn']").prop("disabled")
+ ).toEqual(true);
+ });
+
+ it("should trigger changeUpdate when description is changed", () => {
+ const wrapper = shallow();
+ wrapper
+ .find("[label='Description']")
+ .simulate("update", { name: "description", value: "description change" });
+ expect(props.onChangeUpdate).toHaveBeenCalledWith({
+ name: "description",
+ value: "description change",
+ });
+ });
+
+ it("should trigger onMoveUp when move up button is clicked", () => {
+ shallow()
+ .find("[data-test='move-up-btn']")
+ .simulate("click");
+
+ expect(props.onMoveUp).toHaveBeenCalled();
+ });
+
+ it("should disable the move up button when canMoveUp is false", () => {
+ expect(
+ shallow()
+ .find("[data-test='move-up-btn']")
+ .prop("disabled")
+ ).toEqual(true);
+ });
+
+ it("should trigger onMoveDown when move down button is clicked", () => {
+ shallow()
+ .find("[data-test='move-down-btn']")
+ .simulate("click");
+
+ expect(props.onMoveDown).toHaveBeenCalled();
+ });
+
+ it("should disable the move down button when canMoveDown is false", () => {
+ expect(
+ shallow()
+ .find("[data-test='move-down-btn']")
+ .prop("disabled")
+ ).toEqual(true);
+ });
+
+ it("should trigger deleteCollapsible when the delete button is clicked", () => {
+ shallow()
+ .find("[data-test='delete-collapsible-btn']")
+ .simulate("click");
+
+ expect(props.deleteCollapsible).toHaveBeenCalled();
+ });
+
+ it("should trigger onChange when the title is changed", () => {
+ shallow()
+ .find("[data-test='txt-collapsible-title']")
+ .simulate("change", { e: "change" });
+ expect(props.onChange).toHaveBeenCalledWith({ e: "change" });
+ });
+
+ it("should trigger onUpdate when the title is blurred", () => {
+ shallow()
+ .find("[data-test='txt-collapsible-title']")
+ .simulate("blur", { e: "blur" });
+ expect(props.onUpdate).toHaveBeenCalledWith({ e: "blur" });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.js
new file mode 100644
index 0000000000..04e1705161
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.js
@@ -0,0 +1,32 @@
+import { graphql } from "react-apollo";
+import gql from "graphql-tag";
+import { filter } from "graphql-anywhere";
+
+const mutation = gql`
+ mutation deleteCollapsible($input: DeleteCollapsibleInput!) {
+ deleteCollapsible(input: $input) {
+ id
+ collapsibles {
+ id
+ }
+ }
+ }
+`;
+const inputFilter = gql`
+ {
+ id
+ }
+`;
+
+export const mapMutateToProps = ({ mutate }) => ({
+ deleteCollapsible: collapsible => {
+ const data = filter(inputFilter, collapsible);
+ return mutate({
+ variables: { input: data },
+ });
+ },
+});
+
+export default graphql(mutation, {
+ props: mapMutateToProps,
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.test.js
new file mode 100644
index 0000000000..f7828f081b
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withDeleteCollapsible.test.js
@@ -0,0 +1,29 @@
+import { mapMutateToProps } from "./withDeleteCollapsible";
+
+describe("withDeleteCollapsible", () => {
+ let mutate;
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ it("should return a deleteCollapsible func", () => {
+ expect(mapMutateToProps({ mutate }).deleteCollapsible).toBeInstanceOf(
+ Function
+ );
+ });
+
+ it("should filter the args to what is allowed and call mutate", () => {
+ mapMutateToProps({ mutate }).deleteCollapsible({
+ id: "id",
+ title: "title",
+ description: "description",
+ });
+ expect(mutate).toHaveBeenCalledWith({
+ variables: {
+ input: {
+ id: "id",
+ },
+ },
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.js
new file mode 100644
index 0000000000..6634d41ab7
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.js
@@ -0,0 +1,40 @@
+import { graphql } from "react-apollo";
+import gql from "graphql-tag";
+import { filter } from "graphql-anywhere";
+
+const mutation = gql`
+ mutation updateCollapsible($input: UpdateCollapsibleInput!) {
+ updateCollapsible(input: $input) {
+ id
+ title
+ description
+ }
+ }
+`;
+const inputFilter = gql`
+ {
+ id
+ title
+ description
+ }
+`;
+
+export const mapMutateToProps = ({ mutate }) => ({
+ updateCollapsible: collapsible => {
+ const data = filter(inputFilter, collapsible);
+ return mutate({
+ variables: { input: data },
+ optimisticResponse: {
+ updateCollapsible: {
+ ...collapsible,
+ ...data,
+ __typename: "Collapsible",
+ },
+ },
+ });
+ },
+});
+
+export default graphql(mutation, {
+ props: mapMutateToProps,
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.test.js
new file mode 100644
index 0000000000..2f4afefc70
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/CollapsibleEditor/withUpdateCollapsible.test.js
@@ -0,0 +1,41 @@
+import { mapMutateToProps } from "./withUpdateCollapsible";
+
+describe("withUpdateCollapsible", () => {
+ let mutate;
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ it("should return a updateCollapsible func", () => {
+ expect(mapMutateToProps({ mutate }).updateCollapsible).toBeInstanceOf(
+ Function
+ );
+ });
+
+ it("should filter the args, create an optimistic response and call mutate", () => {
+ mapMutateToProps({ mutate }).updateCollapsible({
+ id: "id",
+ title: "title",
+ description: "description",
+ foo: "foo",
+ });
+ expect(mutate).toHaveBeenCalledWith({
+ optimisticResponse: {
+ updateCollapsible: {
+ id: "id",
+ title: "title",
+ description: "description",
+ foo: "foo",
+ __typename: "Collapsible",
+ },
+ },
+ variables: {
+ input: {
+ id: "id",
+ title: "title",
+ description: "description",
+ },
+ },
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.js
new file mode 100644
index 0000000000..64a152121b
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.js
@@ -0,0 +1,58 @@
+import { CSSTransition } from "react-transition-group";
+import PropTypes from "prop-types";
+import styled from "styled-components";
+
+const timeout = props => props.timeout;
+const halfTimeout = props => props.timeout / 2;
+
+const handleExit = node => {
+ const { height } = node.getBoundingClientRect();
+ node.style.height = `${height}px`;
+};
+
+const classNames = "transition";
+
+const Transition = styled(CSSTransition).attrs({
+ classNames,
+ onExit: () => handleExit,
+})`
+ position: relative;
+
+ &.${classNames}-enter {
+ opacity: 0;
+ transform: scale(0.9);
+ z-index: 200;
+ }
+
+ &.${classNames}-enter-active {
+ opacity: 1;
+ transform: scale(1);
+ transition: opacity ${timeout}ms ease-out,
+ transform ${timeout}ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ }
+
+ &.${classNames}-exit {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ &.${classNames}-exit-active {
+ opacity: 0;
+ height: 0 !important;
+ transform: scale(0.9);
+ transition: opacity ${halfTimeout}ms ease-out,
+ height ${halfTimeout}ms ease-in ${halfTimeout}ms,
+ transform ${halfTimeout}ms ease-in;
+ }
+`;
+
+Transition.propTypes = {
+ timeout: PropTypes.number,
+ children: PropTypes.element,
+};
+
+Transition.defaultProps = {
+ timeout: 200,
+};
+
+export default Transition;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.test.js
new file mode 100644
index 0000000000..904ceef679
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/Transition.test.js
@@ -0,0 +1,33 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import Transition from "./Transition";
+
+describe("Transition", () => {
+ it("should render", () => {
+ expect(
+ shallow(
+
+
+
+ )
+ ).toMatchSnapshot();
+ });
+
+ it("should set the height on exit so it can animate it out", () => {
+ const onExit = shallow(
+
+
+
+ ).prop("onExit");
+
+ const getBoundingClientRectStub = jest.fn().mockReturnValue({ height: 10 });
+ const node = {
+ style: { height: 0 },
+ getBoundingClientRect: getBoundingClientRectStub,
+ };
+
+ onExit(node);
+ expect(node.style.height).toEqual("10px");
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/Transition.test.js.snap b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/Transition.test.js.snap
new file mode 100644
index 0000000000..1feba9921b
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/Transition.test.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Transition should render 1`] = `
+.c0 {
+ position: relative;
+}
+
+.c0.transition-enter {
+ opacity: 0;
+ -webkit-transform: scale(0.9);
+ -ms-transform: scale(0.9);
+ transform: scale(0.9);
+ z-index: 200;
+}
+
+.c0.transition-enter-active {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+ -webkit-transition: opacity 200ms ease-out,-webkit-transform 200ms cubic-bezier(0.175,0.885,0.32,1.275);
+ -webkit-transition: opacity 200ms ease-out,transform 200ms cubic-bezier(0.175,0.885,0.32,1.275);
+ transition: opacity 200ms ease-out,transform 200ms cubic-bezier(0.175,0.885,0.32,1.275);
+}
+
+.c0.transition-exit {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+}
+
+.c0.transition-exit-active {
+ opacity: 0;
+ height: 0 !important;
+ -webkit-transform: scale(0.9);
+ -ms-transform: scale(0.9);
+ transform: scale(0.9);
+ -webkit-transition: opacity 100ms ease-out,height 100ms ease-in 100ms,-webkit-transform 100ms ease-in;
+ -webkit-transition: opacity 100ms ease-out,height 100ms ease-in 100ms,transform 100ms ease-in;
+ transition: opacity 100ms ease-out,height 100ms ease-in 100ms,transform 100ms ease-in;
+}
+
+
+
+
+`;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000..51d22326f7
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/__snapshots__/index.test.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CollapsiblesEditor should render 1`] = `
+
+
+
+
+
+ Add collapsible
+
+
+`;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.js
new file mode 100644
index 0000000000..7e0cf0f3bc
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.js
@@ -0,0 +1,63 @@
+import React from "react";
+import styled from "styled-components";
+import { propType } from "graphql-anywhere";
+import PropTypes from "prop-types";
+import { flowRight } from "lodash";
+
+import Button from "components/buttons/Button";
+import Reorder from "components/Reorder";
+
+import Transition from "./Transition";
+import withCreateCollapsible from "./withCreateCollapsible";
+import CollapsibleEditor from "./CollapsibleEditor";
+import withMoveCollapsible from "./withMoveCollapsible";
+
+const DetailList = styled.div`
+ margin-bottom: 2em;
+`;
+
+const AddButton = styled(Button)`
+ width: 100%;
+`;
+
+export const CollapsiblesEditor = ({
+ collapsibles,
+ createCollapsible,
+ moveCollapsible,
+ introductionId,
+}) => (
+
+
+ {(props, collapsible) => (
+
+ )}
+
+ createCollapsible({ introductionId })}
+ data-test="add-collapsible-btn"
+ >
+ Add collapsible
+
+
+);
+
+CollapsiblesEditor.fragments = [...CollapsibleEditor.fragments];
+
+CollapsiblesEditor.propTypes = {
+ collapsibles: PropTypes.arrayOf(propType(CollapsibleEditor.fragments[0]))
+ .isRequired,
+ createCollapsible: PropTypes.func.isRequired,
+ moveCollapsible: PropTypes.func.isRequired,
+ introductionId: PropTypes.string.isRequired,
+};
+
+export default flowRight(
+ withCreateCollapsible,
+ withMoveCollapsible
+)(CollapsiblesEditor);
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.test.js
new file mode 100644
index 0000000000..8bcd5a033e
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/index.test.js
@@ -0,0 +1,60 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import { CollapsiblesEditor } from "./";
+
+describe("CollapsiblesEditor", () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ collapsibles: [
+ {
+ id: "1",
+ title: "title",
+ description: "description",
+ },
+ ],
+ createCollapsible: jest.fn(),
+ moveCollapsible: jest.fn(),
+ introductionId: "introId",
+ };
+ });
+
+ it("should render", () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("should render the collapsible editor and additional props for the collapsible given", () => {
+ const wrapper = shallow()
+ .find("[data-test='collapsibles-list']")
+ .renderProp("children")({ prop1: "1", prop2: 2 }, props.collapsibles[0]);
+
+ expect(wrapper.props()).toMatchObject({
+ collapsible: props.collapsibles[0],
+ prop1: "1",
+ prop2: 2,
+ });
+ });
+
+ it("should create the collapsible when the add button is clicked", () => {
+ shallow()
+ .find("[data-test='add-collapsible-btn']")
+ .simulate("click");
+
+ expect(props.createCollapsible).toHaveBeenCalledWith({
+ introductionId: "introId",
+ });
+ });
+
+ it("should move the collapsible when move is triggered", () => {
+ shallow()
+ .find("[data-test='collapsibles-list']")
+ .simulate("move", { id: "1", position: 2 });
+
+ expect(props.moveCollapsible).toHaveBeenCalledWith({
+ id: "1",
+ position: 2,
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.js
new file mode 100644
index 0000000000..c0e5dc71be
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.js
@@ -0,0 +1,39 @@
+import { graphql } from "react-apollo";
+import gql from "graphql-tag";
+import { filter } from "graphql-anywhere";
+
+const mutation = gql`
+ mutation createCollapsible($input: CreateCollapsibleInput!) {
+ createCollapsible(input: $input) {
+ id
+ title
+ description
+ introduction {
+ id
+ collapsibles {
+ id
+ }
+ }
+ }
+ }
+`;
+const inputFilter = gql`
+ {
+ introductionId
+ title
+ description
+ }
+`;
+
+export const mapMutateToProps = ({ mutate }) => ({
+ createCollapsible: collapsible => {
+ const data = filter(inputFilter, collapsible);
+ return mutate({
+ variables: { input: data },
+ });
+ },
+});
+
+export default graphql(mutation, {
+ props: mapMutateToProps,
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.test.js
new file mode 100644
index 0000000000..721c69caa3
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withCreateCollapsible.test.js
@@ -0,0 +1,32 @@
+import { mapMutateToProps } from "./withCreateCollapsible";
+
+describe("withCreateCollapsible", () => {
+ let mutate;
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ it("should return a createCollapsible func", () => {
+ expect(mapMutateToProps({ mutate }).createCollapsible).toBeInstanceOf(
+ Function
+ );
+ });
+
+ it("should filter the args to what is allowed and call mutate", () => {
+ mapMutateToProps({ mutate }).createCollapsible({
+ introductionId: "introId",
+ title: "title",
+ description: "description",
+ foo: "foo",
+ });
+ expect(mutate).toHaveBeenCalledWith({
+ variables: {
+ input: {
+ introductionId: "introId",
+ title: "title",
+ description: "description",
+ },
+ },
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.js
new file mode 100644
index 0000000000..7d8f5ecd6d
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.js
@@ -0,0 +1,36 @@
+import { graphql } from "react-apollo";
+import gql from "graphql-tag";
+import { filter } from "graphql-anywhere";
+
+const mutation = gql`
+ mutation moveCollapsible($input: MoveCollapsibleInput!) {
+ moveCollapsible(input: $input) {
+ id
+ introduction {
+ id
+ collapsibles {
+ id
+ }
+ }
+ }
+ }
+`;
+const inputFilter = gql`
+ {
+ id
+ position
+ }
+`;
+
+export const mapMutateToProps = ({ mutate }) => ({
+ moveCollapsible: collapsible => {
+ const data = filter(inputFilter, collapsible);
+ return mutate({
+ variables: { input: data },
+ });
+ },
+});
+
+export default graphql(mutation, {
+ props: mapMutateToProps,
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.test.js
new file mode 100644
index 0000000000..6766296258
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/CollapsiblesEditor/withMoveCollapsible.test.js
@@ -0,0 +1,30 @@
+import { mapMutateToProps } from "./withMoveCollapsible";
+
+describe("withMoveCollapsible", () => {
+ let mutate;
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ it("should return a moveCollapsible func", () => {
+ expect(mapMutateToProps({ mutate }).moveCollapsible).toBeInstanceOf(
+ Function
+ );
+ });
+
+ it("should filter the args to what is allowed and call mutate", () => {
+ mapMutateToProps({ mutate }).moveCollapsible({
+ id: "id",
+ position: 1,
+ foo: "foo",
+ });
+ expect(mutate).toHaveBeenCalledWith({
+ variables: {
+ input: {
+ id: "id",
+ position: 1,
+ },
+ },
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.js b/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.js
new file mode 100644
index 0000000000..1bd2cb2236
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.js
@@ -0,0 +1,172 @@
+import React from "react";
+import styled, { css } from "styled-components";
+import PropTypes from "prop-types";
+
+import { Input } from "components/Forms";
+import { colors } from "constants/theme";
+
+import iconCheck from "./icon-check.svg";
+
+const LegalField = styled.div`
+ display: flex;
+ margin-bottom: 2em;
+`;
+
+const LegalInput = styled(Input)`
+ position: absolute;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ width: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ &:hover,
+ &:focus {
+ border: none;
+ outline: none;
+ box-shadow: none;
+ }
+`;
+
+const LegalLabel = styled.label`
+ padding: 2.5em 1.5em;
+ border-radius: 4px;
+ border: 1px solid ${colors.bordersLight};
+ flex: 1 1 33.3333333%;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: #fff;
+ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.18);
+ color: ${colors.textLight};
+ position: relative;
+ transition: padding 200ms ease-in-out;
+
+ &:not(:last-of-type) {
+ margin-right: 1em;
+ }
+
+ &:focus-within {
+ border-color: ${colors.blue};
+ outline-color: ${colors.blue};
+ box-shadow: 0 0 0 3px ${colors.tertiary};
+ }
+
+ &::before {
+ content: url(${iconCheck});
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin: 0 auto;
+ z-index: 1;
+ transform: scale(0);
+ opacity: 0;
+ transition: all 100ms ease-out 100ms;
+ margin-top: -1.5em;
+ margin-bottom: 0.5em;
+ }
+
+ ${props =>
+ props.selected &&
+ css`
+ border: 1px solid ${colors.primary};
+ padding: 3em 1.5em 2em;
+ &::before {
+ transform: scale(1);
+ opacity: 1;
+ }
+ `};
+`;
+
+const LegalTitle = styled.span`
+ font-size: 0.85em;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 0 1em;
+ color: ${colors.text};
+`;
+
+const LegalNotice = styled.span`
+ font-weight: bold;
+ margin-bottom: 1em;
+ width: 8em;
+`;
+
+const LegalDescription = styled.span`
+ font-size: 1em;
+`;
+
+export const LegalOption = ({ name, value, children, onChange, selected }) => (
+
+
+ {children}
+
+);
+LegalOption.propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ onChange: PropTypes.func.isRequired,
+ selected: PropTypes.bool.isRequired,
+};
+
+const OPTIONS = [
+ {
+ title: "Notice 1",
+ notice: true,
+ description:
+ "Notice is given under section 1 of the Statistics of Trade Act 1947.",
+ value: "NOTICE_1",
+ },
+ {
+ title: "Notice 2",
+ notice: true,
+ description:
+ "Notice is given under sections 3 and 4 of the Statistics of Trade Act 1947.",
+ value: "NOTICE_2",
+ },
+ {
+ title: "Voluntary",
+ description: "No legal notice will be displayed.",
+ value: "VOLUNTARY",
+ },
+];
+
+const LegalBasisField = ({ name, value, onChange, ...rest }) => (
+
+ {OPTIONS.map(option => (
+
+ {option.title}
+ {option.notice && (
+ Your response is legally required.
+ )}
+ {option.description}
+
+ ))}
+
+);
+
+LegalBasisField.propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
+
+export default LegalBasisField;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.test.js
new file mode 100644
index 0000000000..9df6e0e133
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/LegalBasisField.test.js
@@ -0,0 +1,51 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import LegalBasisField, { LegalOption } from "./LegalBasisField";
+
+describe("LegalBasisField", () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ name: "legalBasis",
+ value: "NOTICE_1",
+ onChange: jest.fn(),
+ };
+ });
+ it("should render", () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+
+ it("should show the option matching the value as selected", () => {
+ const option = shallow().find(
+ "[selected=true]"
+ );
+ expect(option.prop("value")).toEqual(props.value);
+ });
+
+ it("picking the option should trigger onChange", () => {
+ shallow()
+ .find("[value='NOTICE_2']")
+ .simulate("change", { name: props.name, value: "NOTICE_2" });
+
+ expect(props.onChange).toHaveBeenCalledWith({
+ name: props.name,
+ value: "NOTICE_2",
+ });
+ });
+
+ describe("LegalOption", () => {
+ it("should render", () => {
+ const props = {
+ name: "legalBasis",
+ value: "NOTICE_1",
+ onChange: jest.fn(),
+ selected: false,
+ };
+ expect(
+ shallow(hello world)
+ ).toMatchSnapshot();
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/LegalBasisField.test.js.snap b/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/LegalBasisField.test.js.snap
new file mode 100644
index 0000000000..a1cfa7b7ce
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/LegalBasisField.test.js.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LegalBasisField LegalOption should render 1`] = `
+
+
+ hello world
+
+`;
+
+exports[`LegalBasisField should render 1`] = `
+
+
+
+ Notice 1
+
+
+ Your response is legally required.
+
+
+ Notice is given under section 1 of the Statistics of Trade Act 1947.
+
+
+
+
+ Notice 2
+
+
+ Your response is legally required.
+
+
+ Notice is given under sections 3 and 4 of the Statistics of Trade Act 1947.
+
+
+
+
+ Voluntary
+
+
+ No legal notice will be displayed.
+
+
+
+`;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000..f00d44ed83
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/__snapshots__/index.test.js.snap
@@ -0,0 +1,181 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IntroductionEditor should render 1`] = `
+
+
+
+
+ Introduction content
+
+
+ This content is displayed above the “start survey” button. The title is not editable.
+
+
+
+
+ Legal basis
+
+
+
+
+
+
+
+ Secondary content
+
+
+ This content is displayed below the “start survey” button.
+
+
+
+
+ Collapsibles
+
+
+ Information which is displayed in a collapsible “twistie”.
+
+
+
+
+
+
+
+ Tertiary content
+
+
+
+
+
+
+`;
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/icon-check.svg b/eq-author/src/App/introduction/Design/IntroductionEditor/icon-check.svg
new file mode 100644
index 0000000000..c95e0e66dc
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/icon-check.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/index.js b/eq-author/src/App/introduction/Design/IntroductionEditor/index.js
new file mode 100644
index 0000000000..8396b5d1ba
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/index.js
@@ -0,0 +1,208 @@
+import React from "react";
+import gql from "graphql-tag";
+import styled from "styled-components";
+import { flowRight, noop } from "lodash/fp";
+import { propType } from "graphql-anywhere";
+import PropTypes from "prop-types";
+
+import withPropRenamed from "enhancers/withPropRenamed";
+import withChangeUpdate from "enhancers/withChangeUpdate";
+
+import RichTextEditor from "components/RichTextEditor";
+import withEntityEditor from "components/withEntityEditor";
+
+import { colors } from "constants/theme";
+
+import transformNestedFragments from "utils/transformNestedFragments";
+
+import LegalBasisField from "./LegalBasisField";
+import CollapsiblesEditor from "./CollapsiblesEditor";
+
+import withUpdateQuestionnaireIntroduction from "./withUpdateQuestionnaireIntroduction";
+
+const Section = styled.section`
+ &:not(:last-of-type) {
+ border-bottom: 1px solid #e0e0e0;
+ margin-bottom: 2em;
+ }
+`;
+
+const Padding = styled.div`
+ padding: 0 2em;
+`;
+
+const SectionTitle = styled.h2`
+ font-size: 1.1em;
+ font-weight: bold;
+ color: ${colors.text};
+ margin: 0 0 1em;
+`;
+
+const SectionDescription = styled.p`
+ margin: 0.1em 0 1em;
+ color: ${colors.textLight};
+`;
+
+const titleControls = {
+ emphasis: true,
+ piping: true,
+};
+
+const descriptionControls = {
+ bold: true,
+ emphasis: true,
+ piping: true,
+ list: true,
+};
+
+export const IntroductionEditor = ({ introduction, onChangeUpdate }) => {
+ const {
+ id,
+ collapsibles,
+ title,
+ description,
+ secondaryTitle,
+ secondaryDescription,
+ tertiaryTitle,
+ tertiaryDescription,
+ legalBasis,
+ } = introduction;
+
+ return (
+ <>
+
+
+
+ Introduction content
+
+
+ This content is displayed above the “start survey” button. The title
+ is not editable.
+
+
+
+ Legal basis
+
+
+
+
+
+
+ Secondary content
+
+
+ This content is displayed below the “start survey” button.
+
+
+
+
+ Collapsibles
+
+
+ Information which is displayed in a collapsible “twistie”.
+
+
+
+
+
+
+ Tertiary content
+
+
+
+
+ >
+ );
+};
+const fragment = gql`
+ fragment IntroductionEditor on QuestionnaireIntroduction {
+ id
+ title
+ description
+ secondaryTitle
+ secondaryDescription
+ collapsibles {
+ ...CollapsibleEditor
+ }
+ tertiaryTitle
+ tertiaryDescription
+ legalBasis
+ }
+`;
+
+IntroductionEditor.fragments = [fragment, ...CollapsiblesEditor.fragments];
+
+IntroductionEditor.propTypes = {
+ introduction: propType(
+ transformNestedFragments(fragment, CollapsiblesEditor.fragments)
+ ).isRequired,
+ onChangeUpdate: PropTypes.func.isRequired,
+};
+
+const withWrappers = flowRight(
+ withUpdateQuestionnaireIntroduction,
+ withPropRenamed("updateQuestionnaireIntroduction", "onUpdate"),
+ withEntityEditor("introduction"),
+ withChangeUpdate
+);
+
+export default withWrappers(IntroductionEditor);
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/index.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/index.test.js
new file mode 100644
index 0000000000..039ce963ee
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/index.test.js
@@ -0,0 +1,28 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import { IntroductionEditor } from "./";
+
+describe("IntroductionEditor", () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ introduction: {
+ id: "1",
+ title: "title",
+ description: "description",
+ secondaryTitle: "secondary title",
+ secondaryDescription: "secondary description",
+ collapsibles: [],
+ tertiaryTitle: "tertiary title",
+ tertiaryDescription: "tertiary description",
+ legalBasis: "VOLUNTARY",
+ },
+ onChangeUpdate: jest.fn(),
+ };
+ });
+
+ it("should render", () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.js b/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.js
new file mode 100644
index 0000000000..ec86546068
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.js
@@ -0,0 +1,52 @@
+import { graphql } from "react-apollo";
+import gql from "graphql-tag";
+import { filter } from "graphql-anywhere";
+
+const mutation = gql`
+ mutation UpdateQuestionnaireIntroduction(
+ $input: UpdateQuestionnaireIntroductionInput!
+ ) {
+ updateQuestionnaireIntroduction(input: $input) {
+ id
+ title
+ description
+ legalBasis
+ secondaryTitle
+ secondaryDescription
+ tertiaryTitle
+ tertiaryDescription
+ }
+ }
+`;
+const inputFilter = gql`
+ {
+ id
+ title
+ description
+ legalBasis
+ secondaryTitle
+ secondaryDescription
+ tertiaryTitle
+ tertiaryDescription
+ }
+`;
+
+export const mapMutateToProps = ({ mutate }) => ({
+ updateQuestionnaireIntroduction: introduction => {
+ const data = filter(inputFilter, introduction);
+ return mutate({
+ variables: { input: data },
+ optimisticResponse: {
+ updateQuestionnaireIntroduction: {
+ ...introduction,
+ ...data,
+ __typename: "QuestionnaireIntroduction",
+ },
+ },
+ });
+ },
+});
+
+export default graphql(mutation, {
+ props: mapMutateToProps,
+});
diff --git a/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.test.js b/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.test.js
new file mode 100644
index 0000000000..e0becdb651
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/IntroductionEditor/withUpdateQuestionnaireIntroduction.test.js
@@ -0,0 +1,40 @@
+import { mapMutateToProps } from "./withUpdateQuestionnaireIntroduction";
+
+describe("withUpdateQuestionnaireIntroduction", () => {
+ let mutate;
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ it("should return a updateQuestionnaireIntroduction func", () => {
+ expect(
+ mapMutateToProps({ mutate }).updateQuestionnaireIntroduction
+ ).toBeInstanceOf(Function);
+ });
+
+ it("should filter the args to what is allowed and call mutate", () => {
+ mapMutateToProps({ mutate }).updateQuestionnaireIntroduction({
+ introductionId: "introId",
+ title: "title",
+ description: "description",
+ foo: "foo",
+ });
+ expect(mutate).toHaveBeenCalledWith({
+ optimisticResponse: {
+ updateQuestionnaireIntroduction: {
+ introductionId: "introId",
+ title: "title",
+ description: "description",
+ foo: "foo",
+ __typename: "QuestionnaireIntroduction",
+ },
+ },
+ variables: {
+ input: {
+ title: "title",
+ description: "description",
+ },
+ },
+ });
+ });
+});
diff --git a/eq-author/src/App/introduction/Design/index.js b/eq-author/src/App/introduction/Design/index.js
new file mode 100644
index 0000000000..cf6b284278
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/index.js
@@ -0,0 +1,78 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Query } from "react-apollo";
+import gql from "graphql-tag";
+import { get, isEmpty } from "lodash/fp";
+import { propType } from "graphql-anywhere";
+
+import Loading from "components/Loading";
+import Error from "components/Error";
+
+import transformNestedFragments from "utils/transformNestedFragments";
+
+import IntroductionLayout from "../IntroductionLayout";
+
+import IntroductionEditor from "./IntroductionEditor";
+
+export const IntroductionDesign = ({ loading, error, data }) => {
+ if (loading) {
+ return (
+
+ Page loading…
+
+ );
+ }
+
+ const introduction = get("questionnaireIntroduction", data);
+ if (error || isEmpty(introduction)) {
+ return Something went wrong;
+ }
+
+ return (
+
+
+
+ );
+};
+
+IntroductionDesign.propTypes = {
+ loading: PropTypes.bool.isRequired,
+ error: PropTypes.object, // eslint-disable-line
+ data: PropTypes.shape({
+ introduction: propType(...IntroductionEditor.fragments),
+ }),
+};
+
+const query = gql`
+ query GetQuestionnaireIntroduction($id: ID!) {
+ questionnaireIntroduction(id: $id) {
+ id
+ ...IntroductionEditor
+ }
+ }
+`;
+
+const INTRODUCTION_QUERY = transformNestedFragments(
+ query,
+ IntroductionEditor.fragments
+);
+
+const IntroductionDesignWithData = props => (
+
+ {queryProps => }
+
+);
+IntroductionDesignWithData.propTypes = {
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ introductionId: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default IntroductionDesignWithData;
diff --git a/eq-author/src/App/introduction/Design/index.test.js b/eq-author/src/App/introduction/Design/index.test.js
new file mode 100644
index 0000000000..21fc805c0d
--- /dev/null
+++ b/eq-author/src/App/introduction/Design/index.test.js
@@ -0,0 +1,50 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import Loading from "components/Loading";
+import { default as ErrorComponent } from "components/Error";
+
+import { IntroductionDesign } from "./";
+import IntroductionEditor from "./IntroductionEditor";
+
+describe("Introduction Design", () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ loading: false,
+ error: null,
+ data: {
+ questionnaireIntroduction: {
+ id: "1",
+ },
+ },
+ };
+ });
+
+ it("should render the editor when loaded", () => {
+ expect(
+ shallow().find(IntroductionEditor)
+ ).toHaveLength(1);
+ });
+
+ it("should render loading whilst loading", () => {
+ expect(
+ shallow().find(Loading)
+ ).toHaveLength(1);
+ });
+
+ it("should render error when there is no data but it is loaded", () => {
+ props.data = null;
+ expect(
+ shallow().find(ErrorComponent)
+ ).toHaveLength(1);
+ });
+
+ it("should render error when there an error and it has loaded", () => {
+ props.error = new Error("It broke");
+ expect(
+ shallow().find(ErrorComponent)
+ ).toHaveLength(1);
+ });
+});
diff --git a/eq-author/src/App/introduction/IntroductionLayout.js b/eq-author/src/App/introduction/IntroductionLayout.js
new file mode 100644
index 0000000000..00537bb268
--- /dev/null
+++ b/eq-author/src/App/introduction/IntroductionLayout.js
@@ -0,0 +1,28 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Titled } from "react-titled";
+import styled from "styled-components";
+
+import EditorLayout from "App/page/Design/EditorLayout";
+
+const Padding = styled.div`
+ padding: 2em 0 0;
+ position: relative;
+ width: 100%;
+`;
+
+const setTitle = title => `Introduction — ${title}`;
+
+const IntroductionLayout = ({ children }) => (
+
+
+ {children}
+
+
+);
+
+IntroductionLayout.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export default IntroductionLayout;
diff --git a/eq-author/src/App/introduction/IntroductionLayout.test.js b/eq-author/src/App/introduction/IntroductionLayout.test.js
new file mode 100644
index 0000000000..c233dc1dad
--- /dev/null
+++ b/eq-author/src/App/introduction/IntroductionLayout.test.js
@@ -0,0 +1,29 @@
+import React from "react";
+import { shallow } from "enzyme";
+import { Titled } from "react-titled";
+
+import IntroductionLayout from "./IntroductionLayout";
+
+describe("IntroductionLayout", () => {
+ it("should render", () => {
+ expect(
+ shallow(
+
+
+
+ )
+ ).toMatchSnapshot();
+ });
+
+ it("should apply introduction to the title", () => {
+ const titleFunc = shallow(
+
+
+
+ )
+ .find(Titled)
+ .prop("title");
+
+ expect(titleFunc("Something")).toEqual("Introduction — Something");
+ });
+});
diff --git a/eq-author/src/App/introduction/Preview/__snapshots__/index.test.js.snap b/eq-author/src/App/introduction/Preview/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000..eccdab8aad
--- /dev/null
+++ b/eq-author/src/App/introduction/Preview/__snapshots__/index.test.js.snap
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Introduction Preview should render 1`] = `
+
+
+
+ If the company details or structure have changed contact us on
+
+
+ 0300 1234 931
+
+ or email
+
+ surveys@ons.gov.uk
+
+
+ bar",
+ }
+ }
+ data-test="description"
+ />
+
+
+ Your response is legally required
+
+
+ Notice is given under section 1 of the Statistics of Trade Act 1947.
+
+
+
+ Start survey
+
+
+ secondaryDescription",
+ }
+ }
+ />
+
+
+
+`;
+
+exports[`Introduction Preview should show section 1 legal basis when it is notice 1 1`] = `
+
+
+ Your response is legally required
+
+
+ Notice is given under section 1 of the Statistics of Trade Act 1947.
+
+
+`;
+
+exports[`Introduction Preview should show section 3 and 4 legal basis when it is notice 2 1`] = `
+
+
+ Your response is legally required
+
+
+ Notice is given under section 3 and 4 of the Statistics of Trade Act 1947.
+
+
+`;
diff --git a/eq-author/src/App/introduction/Preview/icon-chevron.svg b/eq-author/src/App/introduction/Preview/icon-chevron.svg
new file mode 100644
index 0000000000..14a96c473d
--- /dev/null
+++ b/eq-author/src/App/introduction/Preview/icon-chevron.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/eq-author/src/App/introduction/Preview/index.js b/eq-author/src/App/introduction/Preview/index.js
new file mode 100644
index 0000000000..980c5c2071
--- /dev/null
+++ b/eq-author/src/App/introduction/Preview/index.js
@@ -0,0 +1,222 @@
+/* eslint-disable react/no-danger */
+import React from "react";
+import { Query } from "react-apollo";
+import gql from "graphql-tag";
+import styled from "styled-components";
+import { propType } from "graphql-anywhere";
+import PropTypes from "prop-types";
+
+import Loading from "components/Loading";
+import { colors } from "constants/theme";
+import PageTitle from "components/preview/elements/PageTitle";
+
+import IntroductionLayout from "../IntroductionLayout";
+
+import iconChevron from "./icon-chevron.svg";
+
+const Container = styled.div`
+ padding: 0 2em 2em;
+ max-width: 40em;
+ font-size: 1.1em;
+ p {
+ margin: 0 0 1em;
+ }
+ p:last-of-type {
+ margin-bottom: 0;
+ }
+ em {
+ background-color: #dce5b0;
+ padding: 0 0.125em;
+ font-style: normal;
+ }
+ span[data-piped] {
+ background-color: #e0e0e0;
+ padding: 0 0.125em;
+ border-radius: 4px;
+ white-space: pre;
+ }
+`;
+
+const H2 = styled.h2`
+ font-size: 1.2em;
+ margin: 0 0 0.2rem;
+`;
+
+const Description = styled.div`
+ margin-bottom: 1rem;
+
+ li {
+ margin-bottom: 0.3em;
+ }
+`;
+
+const Button = styled.div`
+ color: white;
+ background-color: #0f8243;
+ border: 2px solid #0f8243;
+ padding: 0.75rem 1rem;
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ border-radius: 3px;
+ display: inline-block;
+ text-rendering: optimizeLegibility;
+ margin-bottom: 2em;
+`;
+
+export const Collapsibles = styled.div`
+ margin-bottom: 1em;
+`;
+
+const CollapsiblesTitle = styled.div`
+ display: flex;
+ align-items: center;
+ color: ${colors.primary};
+ margin-bottom: 0.5em;
+ &::before {
+ width: 32px;
+ height: 32px;
+ display: inline-block;
+ margin-left: -10px;
+ content: url(${iconChevron});
+ transform: rotate(90deg);
+ }
+`;
+
+const CollapsiblesContent = styled.div`
+ border-left: 2px solid #999;
+ margin-left: 6px;
+ padding: 0.2em 0 0.2em 1em;
+`;
+
+const Link = styled.span`
+ text-decoration: underline;
+ color: ${colors.primary};
+`;
+
+export const IntroductionPreview = ({ loading, data }) => {
+ if (loading) {
+ return Preview loading…;
+ }
+
+ const {
+ questionnaireIntroduction: {
+ title,
+ description,
+ legalBasis,
+ secondaryTitle,
+ secondaryDescription,
+ collapsibles,
+ tertiaryTitle,
+ tertiaryDescription,
+ },
+ } = data;
+
+ return (
+
+
+
+ If the company details or structure have changed contact us on{" "}
+ 0300 1234 931 or email surveys@ons.gov.uk
+
+
+ {legalBasis === "NOTICE_1" && (
+
+ Your response is legally required
+
+ Notice is given under section 1 of the Statistics of Trade Act 1947.
+
+
+ )}
+ {legalBasis === "NOTICE_2" && (
+
+ Your response is legally required
+
+ Notice is given under section 3 and 4 of the Statistics of Trade Act
+ 1947.
+
+
+ )}
+
+
+
+ {collapsibles
+ .filter(collapsible => collapsible.title && collapsible.description)
+ .map((collapsible, index) => (
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const fragment = gql`
+ fragment QuestionnaireIntroduction on QuestionnaireIntroduction {
+ id
+ title
+ description
+ legalBasis
+ secondaryTitle
+ secondaryDescription
+ collapsibles {
+ id
+ title
+ description
+ }
+ tertiaryTitle
+ tertiaryDescription
+ }
+`;
+
+IntroductionPreview.propTypes = {
+ data: PropTypes.shape({
+ questionnaireIntroduction: propType(fragment),
+ }),
+ loading: PropTypes.bool.isRequired,
+};
+
+export const QUESTIONNAIRE_QUERY = gql`
+ query GetQuestionnaireIntroduction($id: ID!) {
+ questionnaireIntroduction(id: $id) {
+ ...QuestionnaireIntroduction
+ }
+ }
+ ${fragment}
+`;
+
+const IntroductionPreviewWithData = props => (
+
+
+ {innerProps => }
+
+
+);
+IntroductionPreviewWithData.propTypes = {
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ introductionId: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default IntroductionPreviewWithData;
diff --git a/eq-author/src/App/introduction/Preview/index.test.js b/eq-author/src/App/introduction/Preview/index.test.js
new file mode 100644
index 0000000000..916da14189
--- /dev/null
+++ b/eq-author/src/App/introduction/Preview/index.test.js
@@ -0,0 +1,80 @@
+import React from "react";
+import { shallow } from "enzyme";
+
+import Loading from "components/Loading";
+
+import { IntroductionPreview, Collapsibles } from "./";
+
+describe("Introduction Preview", () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ loading: false,
+ data: {
+ questionnaireIntroduction: {
+ id: "1",
+ title: "foo",
+ description: "bar
",
+ secondaryTitle: "secondaryTitle",
+ secondaryDescription: "secondaryDescription
",
+ legalBasis: "NOTICE_1",
+ collapsibles: [],
+ tertiaryTitle: "tertiaryTitle",
+ tertiaryDescription: "tertiaryDescription",
+ },
+ },
+ };
+ });
+
+ it("should render", () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+
+ it("should not show incomplete collapsibles", () => {
+ props.data.questionnaireIntroduction.collapsibles = [
+ { id: "2", title: "collapsible title", description: "" },
+ { id: "3", title: "", description: "collapsible description" },
+ {
+ id: "4",
+ title: "collapsible title",
+ description: "collapsible description",
+ },
+ ];
+ expect(
+ shallow().find(Collapsibles)
+ ).toHaveLength(1);
+ });
+
+ it("should show loading when loading", () => {
+ expect(
+ shallow().find(Loading)
+ ).toHaveLength(1);
+ });
+
+ it("should show no legal description when legal basis is voluntary", () => {
+ props.data.questionnaireIntroduction.legalBasis = "VOLUNTARY";
+ expect(
+ shallow().find(
+ "[data-test='legalBasis']"
+ )
+ ).toHaveLength(0);
+ });
+
+ it("should show section 1 legal basis when it is notice 1", () => {
+ props.data.questionnaireIntroduction.legalBasis = "NOTICE_1";
+ expect(
+ shallow().find(
+ "[data-test='legalBasis']"
+ )
+ ).toMatchSnapshot();
+ });
+
+ it("should show section 3 and 4 legal basis when it is notice 2", () => {
+ props.data.questionnaireIntroduction.legalBasis = "NOTICE_2";
+ expect(
+ shallow().find(
+ "[data-test='legalBasis']"
+ )
+ ).toMatchSnapshot();
+ });
+});
diff --git a/eq-author/src/App/introduction/__snapshots__/IntroductionLayout.test.js.snap b/eq-author/src/App/introduction/__snapshots__/IntroductionLayout.test.js.snap
new file mode 100644
index 0000000000..a30608b468
--- /dev/null
+++ b/eq-author/src/App/introduction/__snapshots__/IntroductionLayout.test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IntroductionLayout should render 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/eq-author/src/App/introduction/index.js b/eq-author/src/App/introduction/index.js
new file mode 100644
index 0000000000..ea392150d7
--- /dev/null
+++ b/eq-author/src/App/introduction/index.js
@@ -0,0 +1,19 @@
+import React from "react";
+
+import { Route } from "react-router-dom";
+
+import Design from "./Design";
+import Preview from "./Preview";
+
+export default [
+ ,
+ ,
+];
diff --git a/eq-author/src/App/page/Design/CalculatedSummaryPageEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/page/Design/CalculatedSummaryPageEditor/__snapshots__/index.test.js.snap
index 8f0f0b61b4..726159afb9 100644
--- a/eq-author/src/App/page/Design/CalculatedSummaryPageEditor/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/page/Design/CalculatedSummaryPageEditor/__snapshots__/index.test.js.snap
@@ -48,6 +48,7 @@ exports[`CalculatedSummaryPageEditor should render 1`] = `
}
}
defaultTab="variables"
+ disabled={false}
fetchAnswers={[MockFunction]}
id="summary-title"
label="Page title"
@@ -93,6 +94,7 @@ exports[`CalculatedSummaryPageEditor should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="total-title"
label="Total title"
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/index.test.js.snap
index 9c95970852..10144b2838 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/index.test.js.snap
@@ -260,7 +260,7 @@ exports[`Answer Editor should render Currency 1`] = `
-
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/index.js b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/index.js
index ac2cbea59c..4869ba583b 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/index.js
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/index.js
@@ -6,11 +6,7 @@ import gql from "graphql-tag";
import fp from "lodash/fp";
import CustomPropTypes from "custom-prop-types";
-import DeleteButton from "components/buttons/DeleteButton";
-import MultipleChoiceAnswer from "App/page/Design/answers/MultipleChoiceAnswer";
-import DateRange from "App/page/Design/answers/DateRange";
-import Date from "App/page/Design/answers/Date";
import {
TEXTFIELD,
NUMBER,
@@ -21,13 +17,14 @@ import {
RADIO,
DATE_RANGE,
} from "constants/answer-types";
-import CurrencyAnswer from "App/page/Design/answers/CurrencyAnswer";
import Tooltip from "components/Forms/Tooltip";
-import BasicAnswer from "App/page/Design/answers/BasicAnswer";
+import DeleteButton from "components/buttons/DeleteButton";
+import MoveButton, { IconUp, IconDown } from "components/buttons/MoveButton";
-import MoveButton from "./MoveButton";
-import IconUp from "./icon-arrow-up.svg?inline";
-import IconDown from "./icon-arrow-down.svg?inline";
+import MultipleChoiceAnswer from "App/page/Design/answers/MultipleChoiceAnswer";
+import DateRange from "App/page/Design/answers/DateRange";
+import Date from "App/page/Design/answers/Date";
+import BasicAnswer from "App/page/Design/answers/BasicAnswer";
const Answer = styled.div`
border: 1px solid ${colors.bordersLight};
@@ -38,6 +35,7 @@ const Answer = styled.div`
border-color: ${colors.blue};
box-shadow: 0 0 0 1px ${colors.blue};
}
+ margin: 1em 2em;
`;
const AnswerHeader = styled.div`
@@ -91,12 +89,9 @@ class AnswerEditor extends React.Component {
if ([TEXTFIELD, TEXTAREA].includes(type)) {
return ;
}
- if ([PERCENTAGE, NUMBER].includes(type)) {
+ if ([PERCENTAGE, NUMBER, CURRENCY].includes(type)) {
return ;
}
- if (type === CURRENCY) {
- return ;
- }
if (type === CHECKBOX) {
return ;
}
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/__snapshots__/index.test.js.snap b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/__snapshots__/index.test.js.snap
index 75a19ed0f9..8dc1fe2bfb 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/__snapshots__/index.test.js.snap
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/__snapshots__/index.test.js.snap
@@ -1,73 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Answers Editor should render 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
+
+
`;
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.js b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.js
index a0ad31702e..6d0578e1c8 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.js
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.js
@@ -1,47 +1,14 @@
-import React, { useState, useRef, useEffect } from "react";
+import React from "react";
import PropTypes from "prop-types";
import { propType } from "graphql-anywhere";
-import styled, { keyframes, css } from "styled-components";
-import { TransitionGroup } from "react-transition-group";
-import getIdForObject from "utils/getIdForObject";
+import Reorder from "components/Reorder";
import withMoveAnswer from "./withMoveAnswer";
import AnswerTransition from "./AnswerTransition";
import AnswerEditor from "./AnswerEditor";
-const MOVE_DURATION = 400;
-const UP = "UP";
-const DOWN = "DOWN";
-
-const move = ({ transform, scale }) => keyframes`
- 0% {
- transform: translateY(0) scale(1);
- }
- 50% {
- transform: scale(${scale});
- }
- 100% {
- transform: translateY(calc(${transform})) scale(1);
- }
-`;
-
-export const AnswerSegment = styled.div`
- padding: 1em 2em;
- z-index: ${props => props.movement.zIndex};
- transform-origin: 50% 50%;
- animation: ${({ movement }) =>
- movement.transform !== 0 &&
- css`
- ${move(
- movement
- )} ${MOVE_DURATION}ms cubic-bezier(0.785, 0.135, 0.150, 0.860) 0s forwards 1;
- `};
-`;
-
-const startingStyles = answers => answers.map(() => ({ transform: 0 }));
-
-export const UnwrappedAnswersEditor = ({
+export const AnswersEditor = ({
answers,
onUpdate,
onAddOption,
@@ -51,108 +18,25 @@ export const UnwrappedAnswersEditor = ({
onDeleteAnswer,
moveAnswer,
}) => {
- const [renderedAnswers, setRenderedAnswers] = useState(answers);
- const [isTransitioning, setIsTransitioning] = useState(false);
- const [answerStyles, setAnswerStyles] = useState(startingStyles(answers));
-
- const hasNewAnswers = useRef(false);
- const answerElements = useRef([]);
- const prevAnswers = useRef(answers);
- const animationTimeout = useRef();
-
- if (prevAnswers.current !== answers) {
- prevAnswers.current = answers;
- hasNewAnswers.current = true;
- }
-
- if (hasNewAnswers.current && !isTransitioning) {
- setRenderedAnswers(answers);
- setAnswerStyles(startingStyles(answers));
- hasNewAnswers.current = false;
- }
-
- const handleRef = (node, index) => {
- if (!node) {
- return;
- }
-
- answerElements.current[index] = node.getBoundingClientRect().height;
- };
-
- useEffect(
- () => {
- return () => {
- if (animationTimeout.current) {
- clearTimeout(animationTimeout.current);
- animationTimeout.current = null;
- }
- };
- },
- [animationTimeout]
- );
-
- const handleMove = (answer, index, direction) => {
- const isUp = direction === UP;
- const indexA = index;
- const indexB = isUp ? index - 1 : index + 1;
-
- const heightA = answerElements.current[indexA];
- const heightB = answerElements.current[indexB];
-
- const newAnswerStyles = [...answerStyles];
-
- newAnswerStyles[indexA] = {
- transform: isUp ? `${0 - heightB}px` : `${heightB}px`,
- zIndex: 2,
- scale: 1.05,
- };
-
- newAnswerStyles[indexB] = {
- transform: isUp ? `${heightA}px` : `${0 - heightA}px`,
- zIndex: 1,
- scale: 0.95,
- };
-
- setIsTransitioning(true);
- setAnswerStyles(newAnswerStyles);
- moveAnswer({ id: answer.id, position: indexB });
- animationTimeout.current = setTimeout(() => {
- setIsTransitioning(false);
- }, MOVE_DURATION);
- };
-
return (
-
- {renderedAnswers.map((answer, index) => (
-
- handleRef(node, index)}
- movement={answerStyles[index]}
- >
- handleMove(answer, index, UP)}
- onMoveDown={() => handleMove(answer, index, DOWN)}
- canMoveUp={!isTransitioning && index > 0}
- canMoveDown={
- !isTransitioning && index < renderedAnswers.length - 1
- }
- onUpdate={onUpdate}
- onAddOption={onAddOption}
- onAddExclusive={onAddExclusive}
- onUpdateOption={onUpdateOption}
- onDeleteOption={onDeleteOption}
- onDeleteAnswer={onDeleteAnswer}
- />
-
-
- ))}
-
+
+ {(props, answer) => (
+
+ )}
+
);
};
-UnwrappedAnswersEditor.propTypes = {
+AnswersEditor.propTypes = {
answers: PropTypes.arrayOf(propType(AnswerEditor.fragments.AnswerEditor))
.isRequired,
onUpdate: PropTypes.func.isRequired,
@@ -164,8 +48,8 @@ UnwrappedAnswersEditor.propTypes = {
moveAnswer: PropTypes.func.isRequired,
};
-UnwrappedAnswersEditor.fragments = {
+AnswersEditor.fragments = {
AnswersEditor: AnswerEditor.fragments.AnswerEditor,
};
-export default withMoveAnswer(UnwrappedAnswersEditor);
+export default withMoveAnswer(AnswersEditor);
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.test.js b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.test.js
index 4eb3689312..c7f3fa487e 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.test.js
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.test.js
@@ -1,12 +1,7 @@
import React from "react";
-import { shallow, mount } from "enzyme";
-import { act } from "react-dom/test-utils";
+import { shallow } from "enzyme";
-import TestProvider from "tests/utils/TestProvider";
-
-import { UnwrappedAnswersEditor, AnswerSegment } from "./";
-
-import AnswerEditor from "./AnswerEditor";
+import { AnswersEditor } from "./";
describe("Answers Editor", () => {
jest.useFakeTimers();
@@ -27,114 +22,24 @@ describe("Answers Editor", () => {
});
it("should render", () => {
- const wrapper = shallow();
+ const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
});
- it("should call moveAnswer with the new index when an answer is moved down", () => {
- const wrapper = shallow();
- wrapper
- .find(AnswerEditor)
- .at(0)
- .simulate("moveDown");
- expect(props.moveAnswer).toHaveBeenCalledWith({
- id: "1",
- position: 1,
- });
- });
-
- it("should call moveAnswer with the new index when an answer is moved up", () => {
- const wrapper = shallow();
- wrapper
- .find(AnswerEditor)
- .at(1)
- .simulate("moveUp");
- expect(props.moveAnswer).toHaveBeenCalledWith({
- id: "2",
- position: 0,
- });
- });
-
- it("should not be able to moved down if its the last in the list", () => {
- const wrapper = shallow();
- const editor = wrapper.find(AnswerEditor).at(1);
- expect(editor.prop("canMoveDown")).toEqual(false);
- expect(editor.prop("canMoveUp")).toEqual(true);
- });
-
- it("should not be able to moved up if its the first in the list", () => {
- const wrapper = shallow();
- const editor = wrapper.find(AnswerEditor).at(0);
- expect(editor.prop("canMoveDown")).toEqual(true);
- expect(editor.prop("canMoveUp")).toEqual(false);
- });
-
- it("does not blow up when the segment ref is null", () => {
- const wrapper = shallow();
- expect(() =>
- wrapper
- .find(AnswerSegment)
- .at(0)
- .props()
- .innerRef(null)
- ).not.toThrow();
- });
-
- it("should get the height of the element to work out how much to transition", () => {
- const wrapper = shallow();
- const getBoundingClientRect = jest.fn().mockReturnValue({ height: 1 });
- wrapper
- .find(AnswerSegment)
- .at(0)
- .props()
- .innerRef({ getBoundingClientRect });
-
- expect(getBoundingClientRect).toHaveBeenCalledWith();
- });
-
- it("should not blow up if we unmount after triggering a move", () => {
- const store = {
- subscribe: jest.fn(),
- dispatch: jest.fn(),
- getState: jest.fn(),
+ it("should render the collapsible editor and additional props for the collapsible given", () => {
+ const extraProps = {
+ canMoveDown: false,
+ canMoveUp: false,
+ onMoveUp: jest.fn(),
+ onMoveDown: jest.fn(),
};
- props.answers = [
- {
- id: "1",
- description: "description",
- guidance: "guidance",
- label: "label",
- type: "Currency",
- },
- {
- id: "2",
- description: "description",
- guidance: "guidance",
- label: "label",
- type: "Currency",
- },
- ];
-
- const wrapper = mount(
-
-
-
- );
-
- act(() => {
- wrapper
- .find("[data-test='btn-move-answer-up']")
- .at(1)
- .simulate("click");
- });
-
- act(() => {
- wrapper.unmount();
- });
+ const wrapper = shallow().renderProp(
+ "children"
+ )(extraProps, props.answers[0]);
- act(() => {
- // This will throw an error if the timeout is not cleared
- jest.runAllTimers();
+ expect(wrapper.props()).toMatchObject({
+ answer: props.answers[0],
+ ...extraProps,
});
});
});
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/AdditionalInfo.test.js.snap b/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/AdditionalInfo.test.js.snap
index c4bb00f78b..8ed8699b09 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/AdditionalInfo.test.js.snap
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/AdditionalInfo.test.js.snap
@@ -43,6 +43,7 @@ exports[`AdditionalInfo should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="additional-info-content"
label="Content"
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/MetaEditor.test.js.snap b/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/MetaEditor.test.js.snap
index 584d6ea2f4..bccaac4425 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/MetaEditor.test.js.snap
+++ b/eq-author/src/App/page/Design/QuestionPageEditor/__snapshots__/MetaEditor.test.js.snap
@@ -10,6 +10,7 @@ exports[`MetaEditor should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="question-title"
label="Question"
@@ -40,6 +41,7 @@ exports[`MetaEditor should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="question-description"
label="Question description"
@@ -93,6 +95,7 @@ exports[`MetaEditor should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="definition-content"
label="Content"
@@ -121,6 +124,7 @@ exports[`MetaEditor should render 1`] = `
"piping": true,
}
}
+ disabled={false}
fetchAnswers={[MockFunction]}
id="question-guidance"
label="Include/exclude"
diff --git a/eq-author/src/App/page/Design/answers/CurrencyAnswer/CurrencyAnswer.test.js b/eq-author/src/App/page/Design/answers/CurrencyAnswer/CurrencyAnswer.test.js
deleted file mode 100644
index da0091dfd7..0000000000
--- a/eq-author/src/App/page/Design/answers/CurrencyAnswer/CurrencyAnswer.test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from "react";
-import { shallow } from "enzyme";
-import CurrencyAnswer from "./";
-import createMockStore from "tests/utils/createMockStore";
-
-const answer = {
- title: "Lorem ipsum",
- description: "Nullam id dolor id nibh ultricies.",
-};
-
-describe("CurrencyAnswer", () => {
- let handleChange;
- let handleUpdate;
- let component;
- let store;
-
- beforeEach(() => {
- handleChange = jest.fn();
- handleUpdate = jest.fn();
- store = createMockStore();
-
- component = shallow(
-
- );
- });
-
- it("should render", () => {
- expect(component).toMatchSnapshot();
- });
-});
diff --git a/eq-author/src/App/page/Design/answers/CurrencyAnswer/__snapshots__/CurrencyAnswer.test.js.snap b/eq-author/src/App/page/Design/answers/CurrencyAnswer/__snapshots__/CurrencyAnswer.test.js.snap
deleted file mode 100644
index 6299c2d08d..0000000000
--- a/eq-author/src/App/page/Design/answers/CurrencyAnswer/__snapshots__/CurrencyAnswer.test.js.snap
+++ /dev/null
@@ -1,28 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CurrencyAnswer should render 1`] = `
-
-
-
-
-
-`;
diff --git a/eq-author/src/App/page/Design/answers/CurrencyAnswer/index.js b/eq-author/src/App/page/Design/answers/CurrencyAnswer/index.js
deleted file mode 100644
index 01897a8d18..0000000000
--- a/eq-author/src/App/page/Design/answers/CurrencyAnswer/index.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import styled from "styled-components";
-import { colors, radius } from "constants/theme";
-import BasicAnswer from "../BasicAnswer";
-
-const StyledSpan = styled.span`
- display: inline-block;
- background-color: ${colors.lighterGrey};
- border-right: 1px solid ${colors.borders};
- border-radius: ${radius} 0 0 ${radius};
- padding: 0.6em 0;
- width: 2.5em;
- font-weight: 700;
- font-size: 1em;
- line-height: 1.1;
- text-align: center;
- position: absolute;
- left: 0;
- top: 0;
- color: ${colors.lightGrey};
-`;
-
-const FieldWrapper = styled.div`
- display: block;
- width: 50%;
- margin-bottom: 1em;
- position: relative;
- overflow: hidden;
-`;
-
-const CurrencyComponent = props => (
- {props.currencyUnit}
-);
-
-CurrencyComponent.propTypes = {
- currencyUnit: PropTypes.string,
-};
-
-CurrencyComponent.defaultProps = {
- currencyUnit: "£",
-};
-
-const CurrencyAnswer = props => (
-
-
-
-
-
-);
-
-CurrencyAnswer.fragments = {
- Currency: BasicAnswer.fragments.Answer,
-};
-
-export default CurrencyAnswer;
diff --git a/eq-author/src/App/questionConfirmation/Design/__snapshots__/Editor.test.js.snap b/eq-author/src/App/questionConfirmation/Design/__snapshots__/Editor.test.js.snap
index 1b180d8166..08dd8fd513 100644
--- a/eq-author/src/App/questionConfirmation/Design/__snapshots__/Editor.test.js.snap
+++ b/eq-author/src/App/questionConfirmation/Design/__snapshots__/Editor.test.js.snap
@@ -12,6 +12,7 @@ exports[`Editor Editor Component should autoFocus the title when there is not on
}
}
data-test="title-input"
+ disabled={false}
id="confirmation-title"
label="Confirmation question"
multiline={false}
@@ -65,6 +66,7 @@ exports[`Editor Editor Component should render 1`] = `
}
}
data-test="title-input"
+ disabled={false}
id="confirmation-title"
label="Confirmation question"
multiline={false}
diff --git a/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap b/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap
index 49bbd0f03c..d1826072f5 100644
--- a/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap
+++ b/eq-author/src/App/section/Design/SectionEditor/__snapshots__/SectionEditor.test.js.snap
@@ -26,6 +26,7 @@ exports[`SectionEditor should render 1`] = `
"emphasis": true,
}
}
+ disabled={false}
id="section-title"
label={
keyframes`
+ 0% {
+ transform: translateY(0) scale(1);
+ }
+ 50% {
+ transform: scale(${scale});
+ }
+ 100% {
+ transform: translateY(calc(${transform})) scale(1);
+ }
+`;
+
+export const Segment = styled.div`
+ z-index: ${props => props.movement.zIndex};
+ transform-origin: 50% 50%;
+ animation: ${({ movement }) =>
+ movement.transform !== 0 &&
+ css`
+ ${move(
+ movement
+ )} ${MOVE_DURATION}ms cubic-bezier(0.785, 0.135, 0.150, 0.860) 0s forwards 1;
+ `};
+`;
+
+const startingStyles = items => items.map(() => ({ transform: 0 }));
+
+const Reorder = ({ list, onMove, children, Transition }) => {
+ const OuterWrapper = Transition ? TransitionGroup : React.Fragment;
+ const InnerWrapper = Transition || React.Fragment;
+
+ const [renderedItems, setRenderedItems] = useState(list);
+ const [isTransitioning, setIsTransitioning] = useState(false);
+ const [itemStyles, setItemStyles] = useState(startingStyles(list));
+
+ const hasNewitems = useRef(false);
+ const itemElements = useRef([]);
+ const previtems = useRef(list);
+ const animationTimeout = useRef();
+
+ if (previtems.current !== list) {
+ previtems.current = list;
+ hasNewitems.current = true;
+ }
+
+ if (hasNewitems.current && !isTransitioning) {
+ setRenderedItems(list);
+ setItemStyles(startingStyles(list));
+ hasNewitems.current = false;
+ }
+
+ const handleRef = (node, index) => {
+ if (!node) {
+ return;
+ }
+
+ itemElements.current[index] = node.getBoundingClientRect().height;
+ };
+
+ useEffect(
+ () => () => {
+ if (animationTimeout.current) {
+ clearTimeout(animationTimeout.current);
+ animationTimeout.current = null;
+ }
+ },
+ [animationTimeout]
+ );
+
+ const handleMove = (item, index, direction) => {
+ const isUp = direction === UP;
+ const indexA = index;
+ const indexB = isUp ? index - 1 : index + 1;
+
+ const heightA = itemElements.current[indexA];
+ const heightB = itemElements.current[indexB];
+
+ const newitemStyles = [...itemStyles];
+
+ newitemStyles[indexA] = {
+ transform: isUp ? `${0 - heightB}px` : `${heightB}px`,
+ zIndex: 2,
+ scale: 1.05,
+ };
+
+ newitemStyles[indexB] = {
+ transform: isUp ? `${heightA}px` : `${0 - heightA}px`,
+ zIndex: 1,
+ scale: 0.95,
+ };
+
+ setIsTransitioning(true);
+ setItemStyles(newitemStyles);
+ onMove({ id: item.id, position: indexB });
+ animationTimeout.current = setTimeout(() => {
+ setIsTransitioning(false);
+ }, MOVE_DURATION);
+ };
+
+ return (
+
+ {renderedItems.map((item, index) => {
+ const itemProps = {
+ onMoveUp: () => handleMove(item, index, UP),
+ onMoveDown: () => handleMove(item, index, DOWN),
+ canMoveUp: !isTransitioning && index > 0,
+ canMoveDown: !isTransitioning && index < renderedItems.length - 1,
+ isMoving: isTransitioning,
+ };
+ return (
+
+ handleRef(node, index)}
+ movement={itemStyles[index]}
+ >
+ {children(itemProps, item)}
+
+
+ );
+ })}
+
+ );
+};
+
+Reorder.propTypes = {
+ list: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
+ onMove: PropTypes.func.isRequired,
+ children: PropTypes.func.isRequired,
+ Transition: PropTypes.func,
+};
+
+export default Reorder;
diff --git a/eq-author/src/components/Reorder/index.test.js b/eq-author/src/components/Reorder/index.test.js
new file mode 100644
index 0000000000..49de7be1cd
--- /dev/null
+++ b/eq-author/src/components/Reorder/index.test.js
@@ -0,0 +1,190 @@
+import React from "react";
+import { shallow, mount } from "enzyme";
+import { act } from "react-dom/test-utils";
+
+import TestProvider from "tests/utils/TestProvider";
+
+import Reorder, { Segment } from "./";
+
+describe("Reorder", () => {
+ jest.useFakeTimers();
+
+ let props;
+
+ beforeEach(() => {
+ props = {
+ list: [{ id: "1" }, { id: "2" }],
+ onMove: jest.fn(),
+ children: jest.fn(),
+ };
+ });
+
+ it("should call moveAnswer with the new index when an answer is moved down", () => {
+ shallow();
+ props.children.mock.calls[0][0].onMoveDown();
+ expect(props.onMove).toHaveBeenCalledWith({
+ id: "1",
+ position: 1,
+ });
+ });
+
+ it("should call moveAnswer with the new index when an answer is moved up", () => {
+ shallow();
+ props.children.mock.calls[1][0].onMoveUp();
+ expect(props.onMove).toHaveBeenCalledWith({
+ id: "2",
+ position: 0,
+ });
+ });
+
+ it("should not be able to moved down if its the last in the list", () => {
+ shallow();
+ const itemProps = props.children.mock.calls[1][0];
+ expect(itemProps.canMoveDown).toEqual(false);
+ expect(itemProps.canMoveUp).toEqual(true);
+ });
+
+ it("should not be able to moved up if its the first in the list", () => {
+ shallow();
+ const itemProps = props.children.mock.calls[0][0];
+ expect(itemProps.canMoveDown).toEqual(true);
+ expect(itemProps.canMoveUp).toEqual(false);
+ });
+
+ it("does not blow up when the segment ref is null", () => {
+ const wrapper = shallow();
+ expect(() =>
+ wrapper
+ .find(Segment)
+ .at(0)
+ .props()
+ .innerRef(null)
+ ).not.toThrow();
+ });
+
+ it("should get the height of the element to work out how much to transition", () => {
+ const wrapper = shallow();
+ const getBoundingClientRect = jest.fn().mockReturnValue({ height: 1 });
+ wrapper
+ .find(Segment)
+ .at(0)
+ .props()
+ .innerRef({ getBoundingClientRect });
+
+ expect(getBoundingClientRect).toHaveBeenCalledWith();
+ });
+
+ it("should not blow up if we unmount after triggering a move", () => {
+ const store = {
+ subscribe: jest.fn(),
+ dispatch: jest.fn(),
+ getState: jest.fn(),
+ };
+ props.list = [
+ {
+ id: "1",
+ description: "description",
+ guidance: "guidance",
+ label: "label",
+ type: "Currency",
+ __typename: "Answer",
+ },
+ {
+ id: "2",
+ description: "description",
+ guidance: "guidance",
+ label: "label",
+ type: "Currency",
+ __typename: "Answer",
+ },
+ ];
+
+ const wrapper = mount(
+
+
+
+ );
+
+ act(() => {
+ props.children.mock.calls[1][0].onMoveUp();
+ });
+
+ act(() => {
+ wrapper.unmount();
+ });
+
+ act(() => {
+ expect(() => jest.runAllTimers()).not.toThrow();
+ });
+ });
+
+ it("should not re-render until transitioning has finished", () => {
+ const store = {
+ subscribe: jest.fn(),
+ dispatch: jest.fn(),
+ getState: jest.fn(),
+ };
+ props.list = [
+ {
+ id: "1",
+ description: "description",
+ guidance: "guidance",
+ label: "label",
+ type: "Currency",
+ __typename: "Answer",
+ },
+ {
+ id: "2",
+ description: "description",
+ guidance: "guidance",
+ label: "label",
+ type: "Currency",
+ __typename: "Answer",
+ },
+ ];
+
+ props.children = jest.fn((props, item) => (
+ {item.id}
+ ));
+
+ const TestComponent = props => (
+
+
+
+ );
+
+ const wrapper = mount();
+
+ act(() => {
+ props.children.mock.calls[1][0].onMoveUp();
+ });
+
+ act(() => {
+ wrapper.setProps({
+ list: props.list.slice().reverse(),
+ });
+ });
+
+ expect(
+ wrapper
+ .find("[data-test='item']")
+ .at(1)
+ .text()
+ ).toEqual("2");
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ act(() => {
+ wrapper.mount();
+ });
+
+ expect(
+ wrapper
+ .find("[data-test='item']")
+ .at(1)
+ .text()
+ ).toEqual("1");
+ });
+});
diff --git a/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.js b/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.js
index e8a0562508..b6fad056c8 100644
--- a/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.js
+++ b/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.js
@@ -7,7 +7,7 @@ import AvailableAnswers from "graphql/fragments/available-answers.graphql";
import AvailableMetadata from "graphql/fragments/available-metadata.graphql";
export const GET_PIPING_CONTENT_PAGE = gql`
- query GetAvailablePipingContent($input: QueryInput!) {
+ query GetAvailablePipingContentForPage($input: QueryInput!) {
page(input: $input) {
id
displayName
@@ -25,7 +25,7 @@ export const GET_PIPING_CONTENT_PAGE = gql`
`;
export const GET_PIPING_CONTENT_SECTION = gql`
- query GetAvailablePipingContent($input: QueryInput!) {
+ query GetAvailablePipingContentForSection($input: QueryInput!) {
section(input: $input) {
id
displayName
@@ -42,7 +42,7 @@ export const GET_PIPING_CONTENT_SECTION = gql`
`;
export const GET_PIPING_CONTENT_QUESTION_CONFIRMATION = gql`
- query GetAvailablePipingContent($id: ID!) {
+ query GetAvailablePipingContentForQuestionConfirmation($id: ID!) {
questionConfirmation(id: $id) {
id
displayName
@@ -58,11 +58,28 @@ export const GET_PIPING_CONTENT_QUESTION_CONFIRMATION = gql`
${AvailableMetadata}
`;
+export const GET_PIPING_CONTENT_INTRODUCTION = gql`
+ query GetAvailablePipingContentForQuestionIntroduction($id: ID!) {
+ questionnaireIntroduction(id: $id) {
+ id
+ availablePipingAnswers {
+ ...AvailableAnswers
+ }
+ availablePipingMetadata {
+ ...AvailableMetadata
+ }
+ }
+ }
+ ${AvailableAnswers}
+ ${AvailableMetadata}
+`;
+
const determineQuery = ({
questionnaireId,
confirmationId,
pageId,
sectionId,
+ introductionId,
}) => {
if (confirmationId) {
return {
@@ -76,10 +93,18 @@ const determineQuery = ({
query: GET_PIPING_CONTENT_PAGE,
};
}
- return {
- variables: { input: { questionnaireId, sectionId } },
- query: GET_PIPING_CONTENT_SECTION,
- };
+ if (sectionId) {
+ return {
+ variables: { input: { questionnaireId, sectionId } },
+ query: GET_PIPING_CONTENT_SECTION,
+ };
+ }
+ if (introductionId) {
+ return {
+ variables: { id: introductionId },
+ query: GET_PIPING_CONTENT_INTRODUCTION,
+ };
+ }
};
const AvailablePipingContentQuery = ({
@@ -87,6 +112,7 @@ const AvailablePipingContentQuery = ({
pageId,
sectionId,
confirmationId,
+ introductionId,
children,
}) => {
const { variables, query } = determineQuery({
@@ -94,9 +120,10 @@ const AvailablePipingContentQuery = ({
pageId,
sectionId,
confirmationId,
+ introductionId,
});
return (
-
+
{children}
);
@@ -107,6 +134,7 @@ AvailablePipingContentQuery.propTypes = {
pageId: PropTypes.string,
sectionId: PropTypes.string,
confirmationId: PropTypes.string,
+ introductionId: PropTypes.string,
children: PropTypes.func.isRequired,
};
diff --git a/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.test.js b/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.test.js
index 60cd178ac3..2dcbf2f00e 100644
--- a/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.test.js
+++ b/eq-author/src/components/RichTextEditor/AvailablePipingContentQuery.test.js
@@ -6,6 +6,7 @@ import AvailablePipingContentQuery, {
GET_PIPING_CONTENT_PAGE,
GET_PIPING_CONTENT_SECTION,
GET_PIPING_CONTENT_QUESTION_CONFIRMATION,
+ GET_PIPING_CONTENT_INTRODUCTION,
} from "components/RichTextEditor/AvailablePipingContentQuery";
describe("Available Piping Content Query", () => {
@@ -13,6 +14,7 @@ describe("Available Piping Content Query", () => {
const pageId = "2";
const confirmationId = "3";
const questionnaireId = "4";
+ const introductionId = "5";
it("should make a query for section data when on a section page", () => {
const wrapper = shallow(
@@ -32,7 +34,6 @@ describe("Available Piping Content Query", () => {
it("should make a query for page data when on a question page", () => {
const wrapper = shallow(
@@ -48,8 +49,6 @@ describe("Available Piping Content Query", () => {
it("should make a query for confirmation data when on a question confirmation page", () => {
const wrapper = shallow(
@@ -61,4 +60,19 @@ describe("Available Piping Content Query", () => {
query: GET_PIPING_CONTENT_QUESTION_CONFIRMATION,
});
});
+
+ it("should make a query for introduction data when on an introduction page", () => {
+ const wrapper = shallow(
+
+ {() => {}}
+
+ );
+ expect(wrapper.find(Query).props()).toMatchObject({
+ variables: { id: introductionId },
+ query: GET_PIPING_CONTENT_INTRODUCTION,
+ });
+ });
});
diff --git a/eq-author/src/components/RichTextEditor/PipingMenu.js b/eq-author/src/components/RichTextEditor/PipingMenu.js
index cf7364a3b2..ad843fa656 100644
--- a/eq-author/src/components/RichTextEditor/PipingMenu.js
+++ b/eq-author/src/components/RichTextEditor/PipingMenu.js
@@ -121,14 +121,24 @@ export class Menu extends React.Component {
}
}
-const calculateEntityName = ({ pageId, confirmationId }) => {
+const calculateEntityName = ({
+ sectionId,
+ pageId,
+ confirmationId,
+ introductionId,
+}) => {
if (confirmationId) {
return "questionConfirmation";
}
if (pageId) {
return "page";
}
- return "section";
+ if (sectionId) {
+ return "section";
+ }
+ if (introductionId) {
+ return "questionnaireIntroduction";
+ }
};
export const UnwrappedPipingMenu = props => {
@@ -142,6 +152,7 @@ export const UnwrappedPipingMenu = props => {
pageId={props.match.params.pageId}
sectionId={props.match.params.sectionId}
confirmationId={props.match.params.confirmationId}
+ introductionId={props.match.params.introductionId}
>
{({ data = {}, ...innerProps }) => {
const entityName = calculateEntityName(props.match.params);
diff --git a/eq-author/src/components/RichTextEditor/PipingMenu.test.js b/eq-author/src/components/RichTextEditor/PipingMenu.test.js
index 8ba7945602..32958d508a 100644
--- a/eq-author/src/components/RichTextEditor/PipingMenu.test.js
+++ b/eq-author/src/components/RichTextEditor/PipingMenu.test.js
@@ -181,6 +181,13 @@ describe("PipingMenu", () => {
sectionId: "3",
},
},
+ {
+ name: "questionnaireIntroduction",
+ params: {
+ questionnaireId: "4",
+ introductionId: "5",
+ },
+ },
];
entities.forEach(({ name, params }) => {
diff --git a/eq-author/src/components/RichTextEditor/RichTextEditor.test.js b/eq-author/src/components/RichTextEditor/RichTextEditor.test.js
index 094653bf9f..c1cb1f5da0 100644
--- a/eq-author/src/components/RichTextEditor/RichTextEditor.test.js
+++ b/eq-author/src/components/RichTextEditor/RichTextEditor.test.js
@@ -83,6 +83,11 @@ describe("components/RichTextEditor", function() {
expect(wrapper).toMatchSnapshot();
});
+ it("should show as disabled and readonly when disabled", () => {
+ wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
it("should store a reference to the editor DOM node", () => {
wrapper.instance().setEditorInstance(editorInstance);
expect(wrapper.instance().editorInstance).toEqual(editorInstance);
diff --git a/eq-author/src/components/RichTextEditor/__snapshots__/RichTextEditor.test.js.snap b/eq-author/src/components/RichTextEditor/__snapshots__/RichTextEditor.test.js.snap
index 9cafbe6a04..a37af1389d 100644
--- a/eq-author/src/components/RichTextEditor/__snapshots__/RichTextEditor.test.js.snap
+++ b/eq-author/src/components/RichTextEditor/__snapshots__/RichTextEditor.test.js.snap
@@ -245,6 +245,7 @@ exports[`components/RichTextEditor should allow multiline input 1`] = `
},
]
}
+ readOnly={false}
spellCheck={true}
webDriverTestID="test-selector-foo"
/>
@@ -500,6 +501,7 @@ exports[`components/RichTextEditor should render 1`] = `
},
]
}
+ readOnly={false}
spellCheck={true}
webDriverTestID="test-selector-foo"
/>
@@ -861,6 +863,263 @@ exports[`components/RichTextEditor should render existing content 1`] = `
},
]
}
+ readOnly={false}
+ spellCheck={true}
+ webDriverTestID="test-selector-foo"
+ />
+
+
+
+`;
+
+exports[`components/RichTextEditor should show as disabled and readonly when disabled 1`] = `
+
+
+
+
+
+
diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js
index 6f18b3025d..158b26168d 100644
--- a/eq-author/src/components/RichTextEditor/index.js
+++ b/eq-author/src/components/RichTextEditor/index.js
@@ -141,6 +141,7 @@ class RichTextEditor extends React.Component {
placeholder: "",
multiline: false,
autoFocus: false,
+ disabled: false,
};
state = {
@@ -169,6 +170,7 @@ class RichTextEditor extends React.Component {
alias: PropTypes.string,
})
),
+ disabled: PropTypes.bool,
};
constructor(props) {
@@ -457,6 +459,7 @@ class RichTextEditor extends React.Component {
testSelector,
id,
placeholder,
+ disabled,
...otherProps
} = this.props;
@@ -468,6 +471,7 @@ class RichTextEditor extends React.Component {
onBlur={this.handleBlur}
onFocus={this.handleFocus}
data-test="rte-field"
+ disabled={disabled}
>
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/MoveButton.test.js.snap b/eq-author/src/components/buttons/MoveButton/__snapshots__/index.test.js.snap
similarity index 100%
rename from eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/__snapshots__/MoveButton.test.js.snap
rename to eq-author/src/components/buttons/MoveButton/__snapshots__/index.test.js.snap
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/icon-arrow-down.svg b/eq-author/src/components/buttons/MoveButton/icon-arrow-down.svg
similarity index 100%
rename from eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/icon-arrow-down.svg
rename to eq-author/src/components/buttons/MoveButton/icon-arrow-down.svg
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/icon-arrow-up.svg b/eq-author/src/components/buttons/MoveButton/icon-arrow-up.svg
similarity index 100%
rename from eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/icon-arrow-up.svg
rename to eq-author/src/components/buttons/MoveButton/icon-arrow-up.svg
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.js b/eq-author/src/components/buttons/MoveButton/index.js
similarity index 86%
rename from eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.js
rename to eq-author/src/components/buttons/MoveButton/index.js
index 3e080a18d1..8ff94c4c1a 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.js
+++ b/eq-author/src/components/buttons/MoveButton/index.js
@@ -3,6 +3,12 @@ import PropTypes from "prop-types";
import styled from "styled-components";
import { colors } from "constants/theme";
+import IconArrowUp from "./icon-arrow-up.svg?inline";
+import IconArrowDown from "./icon-arrow-down.svg?inline";
+
+export const IconUp = IconArrowUp;
+export const IconDown = IconArrowDown;
+
const Button = styled.button`
display: block;
color: ${colors.secondary};
diff --git a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.test.js b/eq-author/src/components/buttons/MoveButton/index.test.js
similarity index 95%
rename from eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.test.js
rename to eq-author/src/components/buttons/MoveButton/index.test.js
index d301886a27..2400e6b255 100644
--- a/eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/AnswerEditor/MoveButton.test.js
+++ b/eq-author/src/components/buttons/MoveButton/index.test.js
@@ -1,7 +1,7 @@
import React from "react";
import { shallow } from "enzyme";
-import MoveButton from "./MoveButton";
+import MoveButton from ".";
describe("MoveButton", () => {
it("should render", () => {
diff --git a/eq-author/src/components/preview/elements/PageTitle.js b/eq-author/src/components/preview/elements/PageTitle.js
index 74e56d41bd..b90d2eb380 100644
--- a/eq-author/src/components/preview/elements/PageTitle.js
+++ b/eq-author/src/components/preview/elements/PageTitle.js
@@ -9,7 +9,7 @@ const Title = styled.h1`
margin: 0 0 1em;
`;
-const PageTitle = ({ title }) => {
+const PageTitle = ({ title, missingText = "Missing Page Title" }) => {
let pageTitle = title && title.replace(/(]+?>|
|<\/p>)/gim, "");
return (
@@ -18,7 +18,7 @@ const PageTitle = ({ title }) => {
) : (
- Missing Page Title
+ {missingText}
)}
@@ -27,6 +27,7 @@ const PageTitle = ({ title }) => {
PageTitle.propTypes = {
title: PropTypes.string,
+ missingText: PropTypes.string,
};
export default PageTitle;
diff --git a/eq-author/src/constants/entities.js b/eq-author/src/constants/entities.js
index fb11201032..2529f05c84 100644
--- a/eq-author/src/constants/entities.js
+++ b/eq-author/src/constants/entities.js
@@ -1,3 +1,4 @@
export const SECTION = "section";
export const PAGE = "page";
export const QUESTION_CONFIRMATION = "question-confirmation";
+export const INTRODUCTION = "introduction";
diff --git a/eq-author/src/custom-prop-types/index.js b/eq-author/src/custom-prop-types/index.js
index 0e1ba5ba1c..2e2af35952 100644
--- a/eq-author/src/custom-prop-types/index.js
+++ b/eq-author/src/custom-prop-types/index.js
@@ -9,7 +9,6 @@ const CustomPropTypes = {
questionnaire: PropTypes.shape({
id: PropTypes.string,
description: PropTypes.string,
- legalBasis: PropTypes.string,
theme: PropTypes.string,
title: PropTypes.string,
navigation: PropTypes.bool,
diff --git a/eq-author/src/enhancers/withChangeUpdate.js b/eq-author/src/enhancers/withChangeUpdate.js
index 428e110ecd..e76b1b5ee2 100644
--- a/eq-author/src/enhancers/withChangeUpdate.js
+++ b/eq-author/src/enhancers/withChangeUpdate.js
@@ -9,6 +9,9 @@ const withChangeUpdate = WrappedComponent => {
};
static fragments = WrappedComponent.fragments;
+ static fragments = WrappedComponent.fragments;
+ static displayName = `withChangeUpdate(${WrappedComponent.displayName})`;
+
handleUpdate = update => this.props.onChange(update, this.props.onUpdate);
render() {
diff --git a/eq-author/src/enhancers/withPropRenamed.js b/eq-author/src/enhancers/withPropRenamed.js
index 664c88f361..e348bceb04 100644
--- a/eq-author/src/enhancers/withPropRenamed.js
+++ b/eq-author/src/enhancers/withPropRenamed.js
@@ -1,11 +1,14 @@
import React from "react";
-export default (oldName, newName) => WrappedComponent =>
- class RemappedComponent extends React.Component {
- static fragments = WrappedComponent.fragments;
- static displayName = `withPropRenamed(${WrappedComponent.displayName})`;
- render() {
- const newProps = { ...this.props, [newName]: this.props[oldName] };
- return ;
- }
+export default (oldName, newName) => WrappedComponent => {
+ const Component = props => {
+ const newProps = {
+ [newName]: props[oldName],
+ ...props,
+ };
+ return ;
};
+ Component.fragments = WrappedComponent.fragments;
+ Component.displayName = `withPropRenamed(${WrappedComponent.displayName})`;
+ return Component;
+};
diff --git a/eq-author/src/graphql/createQuestionnaire.graphql b/eq-author/src/graphql/createQuestionnaire.graphql
index 64ae96c733..095fc4ee8f 100644
--- a/eq-author/src/graphql/createQuestionnaire.graphql
+++ b/eq-author/src/graphql/createQuestionnaire.graphql
@@ -13,5 +13,8 @@ mutation createQuestionnaire($input: CreateQuestionnaireInput!) {
id
}
}
+ introduction {
+ id
+ }
}
}
diff --git a/eq-author/src/graphql/fragments/questionnaire.graphql b/eq-author/src/graphql/fragments/questionnaire.graphql
index ab02280a17..cf72061359 100644
--- a/eq-author/src/graphql/fragments/questionnaire.graphql
+++ b/eq-author/src/graphql/fragments/questionnaire.graphql
@@ -4,7 +4,6 @@ fragment Questionnaire on Questionnaire {
description
surveyId
theme
- legalBasis
navigation
summary
type
diff --git a/eq-author/src/utils/UrlUtils.js b/eq-author/src/utils/UrlUtils.js
index 9aceb7f543..018954e33d 100644
--- a/eq-author/src/utils/UrlUtils.js
+++ b/eq-author/src/utils/UrlUtils.js
@@ -1,7 +1,12 @@
import { curry } from "lodash";
import { generatePath as rrGeneratePath } from "react-router";
-import { SECTION, PAGE, QUESTION_CONFIRMATION } from "../constants/entities";
+import {
+ SECTION,
+ PAGE,
+ QUESTION_CONFIRMATION,
+ INTRODUCTION,
+} from "../constants/entities";
export const Routes = {
HOME: "/",
@@ -57,6 +62,17 @@ export const buildConfirmationPath = ({ confirmationId, tab, ...rest }) => {
entityName: QUESTION_CONFIRMATION,
});
};
+export const buildIntroductionPath = ({ introductionId, tab, ...rest }) => {
+ if (!introductionId) {
+ throw new Error("Confirmation id must be provided");
+ }
+ return generatePath(Routes.QUESTIONNAIRE)({
+ ...rest,
+ tab: sanitiseTab(["design", "preview"])(tab),
+ entityId: introductionId,
+ entityName: INTRODUCTION,
+ });
+};
const buildTabSwitcher = tab => params => {
let builder;
@@ -72,6 +88,9 @@ const buildTabSwitcher = tab => params => {
if (params.confirmationId) {
builder = buildConfirmationPath;
}
+ if (params.introductionId) {
+ builder = buildIntroductionPath;
+ }
if (!builder) {
throw new Error(
`Cannot find builder for params: ${JSON.stringify(params)}`
diff --git a/eq-author/src/utils/UrlUtils.test.js b/eq-author/src/utils/UrlUtils.test.js
index 91f5afa82d..a6791ca54b 100644
--- a/eq-author/src/utils/UrlUtils.test.js
+++ b/eq-author/src/utils/UrlUtils.test.js
@@ -3,6 +3,7 @@ import {
buildSectionPath,
buildPagePath,
buildConfirmationPath,
+ buildIntroductionPath,
buildDesignPath,
buildPreviewPath,
buildRoutingPath,
@@ -12,6 +13,7 @@ const questionnaireId = "1";
const sectionId = "2";
const pageId = "3";
const confirmationId = "4";
+const introductionId = "5";
describe("buildQuestionnairePath", () => {
it("builds a valid path", () => {
@@ -101,6 +103,36 @@ describe("buildConfirmationPath", () => {
});
});
+describe("buildIntroductionPath", () => {
+ it("builds a valid path", () => {
+ const path = buildIntroductionPath({
+ questionnaireId,
+ introductionId,
+ tab: "preview",
+ });
+ expect(path).toEqual(
+ `/q/${questionnaireId}/introduction/${introductionId}/preview`
+ );
+ });
+
+ it("throws if any param not supplied", () => {
+ expect(() => buildIntroductionPath({})).toThrow();
+ expect(() => buildIntroductionPath({ questionnaireId })).toThrow();
+ expect(() => buildIntroductionPath({ introductionId })).toThrow();
+ });
+
+ it("rejects invalid tabs", () => {
+ const path = buildIntroductionPath({
+ questionnaireId,
+ introductionId,
+ tab: "routing",
+ });
+ expect(path).toEqual(
+ `/q/${questionnaireId}/introduction/${introductionId}/design`
+ );
+ });
+});
+
describe("buildDesignPath", () => {
it("builds a page design path", () => {
const path = buildDesignPath({
@@ -130,6 +162,17 @@ describe("buildDesignPath", () => {
`/q/${questionnaireId}/question-confirmation/${confirmationId}/design`
);
});
+
+ it("builds an introduction design path", () => {
+ const path = buildDesignPath({
+ questionnaireId,
+ introductionId,
+ tab: "preview",
+ });
+ expect(path).toEqual(
+ `/q/${questionnaireId}/introduction/${introductionId}/design`
+ );
+ });
});
describe("buildPreviewPath", () => {
@@ -161,6 +204,17 @@ describe("buildPreviewPath", () => {
`/q/${questionnaireId}/question-confirmation/${confirmationId}/preview`
);
});
+
+ it("builds an introduction Preview path", () => {
+ const path = buildPreviewPath({
+ questionnaireId,
+ introductionId,
+ tab: "preview",
+ });
+ expect(path).toEqual(
+ `/q/${questionnaireId}/introduction/${introductionId}/preview`
+ );
+ });
});
describe("buildRoutingPath", () => {
diff --git a/eq-publisher/.eslintrc b/eq-publisher/.eslintrc
index c5575f71a4..1ca4d2380d 100644
--- a/eq-publisher/.eslintrc
+++ b/eq-publisher/.eslintrc
@@ -9,6 +9,6 @@
"sourceType": "script"
},
"rules": {
- "camelcase": 0
+ "camelcase": "off"
}
}
diff --git a/eq-publisher/src/constants/legalBases.js b/eq-publisher/src/constants/legalBases.js
new file mode 100644
index 0000000000..665af50561
--- /dev/null
+++ b/eq-publisher/src/constants/legalBases.js
@@ -0,0 +1,16 @@
+const contentMap = {
+ NOTICE_1:
+ "Notice is given under section 1 of the Statistics of Trade Act 1947.",
+ NOTICE_2:
+ "Notice is given under sections 3 and 4 of the Statistics of Trade Act 1947.",
+ VOLUNTARY: null,
+};
+
+module.exports.types = Object.keys(contentMap).reduce(
+ (hash, key) => ({
+ ...hash,
+ [key]: key,
+ }),
+ {}
+);
+module.exports.contentMap = contentMap;
diff --git a/eq-publisher/src/eq_schema/Question.js b/eq-publisher/src/eq_schema/Question.js
index 44b93e2a5b..d0d43a0fcc 100644
--- a/eq-publisher/src/eq_schema/Question.js
+++ b/eq-publisher/src/eq_schema/Question.js
@@ -106,7 +106,6 @@ class Question {
) {
last(this.answers).guidance = {
show_guidance: question.additionalInfoLabel,
-
hide_guidance: question.additionalInfoLabel,
...processContent(ctx)(question.additionalInfoContent),
};
diff --git a/eq-publisher/src/eq_schema/Question.test.js b/eq-publisher/src/eq_schema/Question.test.js
index 64d8e8774b..da93c27beb 100644
--- a/eq-publisher/src/eq_schema/Question.test.js
+++ b/eq-publisher/src/eq_schema/Question.test.js
@@ -413,7 +413,6 @@ describe("Question", () => {
expect.objectContaining({
minimum: {
value: "2017-02-17",
-
offset_by: {
days: -4,
},
@@ -424,7 +423,6 @@ describe("Question", () => {
expect.objectContaining({
maximum: {
value: "2018-02-17",
-
offset_by: {
years: 10,
},
diff --git a/eq-publisher/src/eq_schema/Questionnaire.js b/eq-publisher/src/eq_schema/Questionnaire.js
index 55813d0a80..f54a51b873 100644
--- a/eq-publisher/src/eq_schema/Questionnaire.js
+++ b/eq-publisher/src/eq_schema/Questionnaire.js
@@ -1,9 +1,15 @@
const { last } = require("lodash");
const { SOCIAL } = require("../constants/questionnaireTypes");
+const {
+ types: { VOLUNTARY },
+ contentMap,
+} = require("../constants/legalBases");
+
const Section = require("./Section");
const Summary = require("./block-types/Summary");
const Confirmation = require("./block-types/Confirmation");
+const Introduction = require("./block-types/Introduction");
const DEFAULT_METADATA = [
{
@@ -40,9 +46,11 @@ class Questionnaire {
const ctx = this.createContext(questionnaireJson);
this.sections = this.buildSections(questionnaireJson.sections, ctx);
+ this.buildIntroduction(questionnaireJson.introduction, ctx);
+
this.theme =
questionnaireJson.type === SOCIAL ? SOCIAL_THEME : DEFAULT_THEME;
- this.legal_basis = questionnaireJson.legalBasis;
+ this.legal_basis = this.buildLegalBasis(questionnaireJson.introduction);
this.navigation = {
visible: questionnaireJson.navigation,
};
@@ -82,6 +90,24 @@ class Questionnaire {
return [...DEFAULT_METADATA, ...userMetadata];
}
+
+ buildLegalBasis(introduction) {
+ if (!introduction || introduction.legalBasis === VOLUNTARY) {
+ return undefined;
+ }
+ return contentMap[introduction.legalBasis];
+ }
+
+ buildIntroduction(introduction, ctx) {
+ if (!introduction) {
+ return;
+ }
+ const groupToAddTo = this.sections[0].groups[0];
+ groupToAddTo.blocks = [
+ new Introduction(introduction, ctx),
+ ...groupToAddTo.blocks,
+ ];
+ }
}
Questionnaire.DEFAULT_METADATA = DEFAULT_METADATA;
diff --git a/eq-publisher/src/eq_schema/Questionnaire.test.js b/eq-publisher/src/eq_schema/Questionnaire.test.js
index 609d1e4c12..41d139c976 100644
--- a/eq-publisher/src/eq_schema/Questionnaire.test.js
+++ b/eq-publisher/src/eq_schema/Questionnaire.test.js
@@ -1,6 +1,9 @@
const { last } = require("lodash");
const { BUSINESS, SOCIAL } = require("../constants/questionnaireTypes");
+const {
+ types: { NOTICE_1, VOLUNTARY },
+} = require("../constants/legalBases");
const Questionnaire = require("./Questionnaire");
const Summary = require("./block-types/Summary");
@@ -14,7 +17,6 @@ describe("Questionnaire", () => {
title: "Quarterly Business Survey",
description: "Quarterly Business Survey",
type: BUSINESS,
- legalBasis: "StatisticsOfTradeAct",
navigation: false,
surveyId: "0112",
summary: true,
@@ -26,6 +28,10 @@ describe("Questionnaire", () => {
},
],
metadata: [],
+ introduction: {
+ legalBasis: NOTICE_1,
+ collapsibles: [],
+ },
},
questionnaire
);
@@ -45,11 +51,28 @@ describe("Questionnaire", () => {
title: "Quarterly Business Survey",
theme: "default",
sections: [expect.any(Section)],
- legal_basis: "StatisticsOfTradeAct",
+ legal_basis:
+ "Notice is given under section 1 of the Statistics of Trade Act 1947.",
metadata: expect.arrayContaining(Questionnaire.DEFAULT_METADATA),
});
});
+ it("should not set a legal basis when there is no introduction", () => {
+ questionnaire = new Questionnaire(
+ createQuestionnaireJSON({ introduction: null })
+ );
+ expect(questionnaire.legal_basis).toEqual(undefined);
+ });
+
+ it("should not set a legal basis when the legal basis is voluntary", () => {
+ questionnaire = new Questionnaire(
+ createQuestionnaireJSON({
+ introduction: { legalBasis: VOLUNTARY, collapsibles: [] },
+ })
+ );
+ expect(questionnaire.legal_basis).toEqual(undefined);
+ });
+
it("should set the theme based on the type", () => {
questionnaire = new Questionnaire(
createQuestionnaireJSON({ type: SOCIAL })
diff --git a/eq-publisher/src/eq_schema/block-types/Introduction.js b/eq-publisher/src/eq_schema/block-types/Introduction.js
new file mode 100644
index 0000000000..a818bb2980
--- /dev/null
+++ b/eq-publisher/src/eq_schema/block-types/Introduction.js
@@ -0,0 +1,71 @@
+const { flow } = require("lodash");
+const convertPipes = require("../../utils/convertPipes");
+const {
+ parseContent,
+ getInnerHTMLWithPiping,
+} = require("../../utils/HTMLUtils");
+
+const processContent = ctx =>
+ flow(
+ convertPipes(ctx),
+ parseContent
+ );
+
+const getSimpleText = (content, ctx) =>
+ flow(
+ convertPipes(ctx),
+ getInnerHTMLWithPiping
+ )(content);
+
+const getComplexText = (content, ctx) => {
+ const result = processContent(ctx)(content);
+ if (result) {
+ return result.content;
+ }
+ return undefined;
+};
+
+module.exports = class Introduction {
+ constructor(
+ {
+ description,
+ secondaryTitle,
+ secondaryDescription,
+ collapsibles,
+ tertiaryTitle,
+ tertiaryDescription,
+ },
+ ctx
+ ) {
+ this.type = "Introduction";
+ this.id = "introduction-block";
+
+ this.primary_content = [
+ {
+ type: "Basic",
+ id: "primary",
+ content: getComplexText(description, ctx),
+ },
+ ];
+
+ this.preview_content = {
+ id: "preview",
+ title: getSimpleText(secondaryTitle, ctx),
+ content: getComplexText(secondaryDescription, ctx),
+ questions: collapsibles
+ .filter(collapsible => collapsible.title && collapsible.description)
+ .map(({ title, description }) => ({
+ question: title,
+ content: getComplexText(description, ctx),
+ })),
+ };
+
+ this.secondary_content = [
+ {
+ id: "secondary-content",
+ title: getSimpleText(tertiaryTitle, ctx),
+ content: getComplexText(tertiaryDescription, ctx),
+ },
+ ];
+ }
+};
diff --git a/eq-publisher/src/eq_schema/block-types/Introduction.test.js b/eq-publisher/src/eq_schema/block-types/Introduction.test.js
new file mode 100644
index 0000000000..b1ec583db2
--- /dev/null
+++ b/eq-publisher/src/eq_schema/block-types/Introduction.test.js
@@ -0,0 +1,137 @@
+const Introduction = require("./Introduction");
+
+const piping = '[some_metadata]';
+
+describe("Introduction", () => {
+ let apiData, context;
+ beforeEach(() => {
+ apiData = {
+ id: "1",
+ description: `- Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated.
- You can provide info estimates if actual figures aren’t available.
- We will treat your data securely and confidentially.
- ${piping}
`,
+ legalBasis: "NOTICE_2",
+ secondaryTitle: `Information you need ${piping}
`,
+ secondaryDescription:
+ "You can select the dates of the period you are reporting for, if the given dates are not appropriate.
",
+ collapsibles: [
+ {
+ id: "d45bf1dd-f286-40ca-b6a2-fe0014574c36",
+ title: "Hello
",
+ description: `World ${piping}
`,
+ },
+ {
+ id: "1e7e5ecd-6f4c-4219-9893-6efdeea36ad0",
+ title: "Collapsible
",
+ description: "Description
",
+ },
+ ],
+ tertiaryTitle: `How we use your data ${piping}
`,
+ tertiaryDescription: `- You cannot appeal your selection. Your business was selected to give us a comprehensive view of the UK economy.
- The information you provide contributes to Gross Domestic Product (GDP).
- ${piping}
`,
+ };
+ context = {
+ questionnaireJson: {
+ metadata: [{ id: "1", key: "some_metadata" }],
+ },
+ };
+ });
+
+ it("set the correct id and type", () => {
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.type).toEqual("Introduction");
+ expect(introduction.id).toEqual("introduction-block");
+ });
+
+ it("should define the primary_content", () => {
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.primary_content).toMatchObject([
+ {
+ content: [
+ {
+ list: [
+ "Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated. ",
+ "You can provide info estimates if actual figures aren’t available.",
+ "We will treat your data securely and confidentially.",
+ "{{ metadata['some_metadata'] }}",
+ ],
+ },
+ ],
+ id: "primary",
+ type: "Basic",
+ },
+ ]);
+ });
+
+ it("should define the preview_content from the secondary settings", () => {
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.preview_content).toMatchObject({
+ content: [
+ {
+ description:
+ "You can select the dates of the period you are reporting for, if the given dates are not appropriate.",
+ },
+ ],
+ id: "preview",
+ questions: expect.any(Array),
+ title: "Information you need {{ metadata['some_metadata'] }}",
+ });
+ });
+
+ it("should define the preview_content questions from collapsibles", () => {
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.preview_content.questions).toMatchObject([
+ {
+ content: [{ description: "World {{ metadata['some_metadata'] }}" }],
+ question: "Hello
",
+ },
+ {
+ content: [{ description: "Description" }],
+ question: "Collapsible
",
+ },
+ ]);
+ });
+
+ it("should not publish partially completed collapsibles", () => {
+ apiData.collapsibles = [
+ {
+ id: "d45bf1dd-f286-40ca-b6a2-fe0014574c36",
+ title: "Hello
",
+ description: "World
",
+ },
+ {
+ id: "d45bf1dd-f286-40ca-b6a2-fe0014574c36",
+ title: "Hello
",
+ description: "",
+ },
+ {
+ id: "d45bf1dd-f286-40ca-b6a2-fe0014574c36",
+ title: "",
+ description: "Description
",
+ },
+ ];
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.preview_content.questions).toMatchObject([
+ {
+ content: [{ description: "World" }],
+ question: "Hello
",
+ },
+ ]);
+ });
+
+ it("should define the secondary_content from the tertiary settings", () => {
+ const introduction = new Introduction(apiData, context);
+ expect(introduction.secondary_content).toMatchObject([
+ {
+ content: [
+ {
+ list: [
+ "You cannot appeal your selection. Your business was selected to give us a comprehensive view of the UK economy.",
+ "The information you provide contributes to Gross Domestic Product (GDP).",
+ "{{ metadata['some_metadata'] }}",
+ ],
+ },
+ ],
+ id: "secondary-content",
+ title: "How we use your data {{ metadata['some_metadata'] }}",
+ },
+ ]);
+ });
+});
diff --git a/eq-publisher/src/getQuestionnaire/queries.js b/eq-publisher/src/getQuestionnaire/queries.js
index af9c6f5b82..4bbf804874 100644
--- a/eq-publisher/src/getQuestionnaire/queries.js
+++ b/eq-publisher/src/getQuestionnaire/queries.js
@@ -155,7 +155,21 @@ exports.getQuestionnaire = `
title
description
type
- legalBasis
+ introduction {
+ id
+ title
+ description
+ legalBasis
+ secondaryTitle
+ secondaryDescription
+ collapsibles {
+ id
+ title
+ description
+ }
+ tertiaryTitle
+ tertiaryDescription
+ }
navigation
surveyId
summary
diff --git a/eq-publisher/src/middleware/createAuthToken.test.js b/eq-publisher/src/middleware/createAuthToken.test.js
index 9175bd8687..9d92a0116a 100644
--- a/eq-publisher/src/middleware/createAuthToken.test.js
+++ b/eq-publisher/src/middleware/createAuthToken.test.js
@@ -34,7 +34,6 @@ describe("createAuthToken", () => {
email: "eq.team@ons.gov.uk",
name: "Publisher",
picture: "",
-
user_id: "Publisher",
});
});