@defra/forms-engine-plugin 4.0.24 → 4.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.server/server/forms/components.json +2 -2
  2. package/.server/server/plugins/engine/components/DeclarationField.d.ts +2 -0
  3. package/.server/server/plugins/engine/components/DeclarationField.js +8 -1
  4. package/.server/server/plugins/engine/components/DeclarationField.js.map +1 -1
  5. package/.server/server/plugins/engine/components/HiddenField.d.ts +21 -0
  6. package/.server/server/plugins/engine/components/HiddenField.js +50 -0
  7. package/.server/server/plugins/engine/components/HiddenField.js.map +1 -0
  8. package/.server/server/plugins/engine/components/Markdown.d.ts +2 -0
  9. package/.server/server/plugins/engine/components/Markdown.js +4 -1
  10. package/.server/server/plugins/engine/components/Markdown.js.map +1 -1
  11. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  12. package/.server/server/plugins/engine/components/helpers/components.js +3 -0
  13. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  14. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  15. package/.server/server/plugins/engine/components/index.js +1 -0
  16. package/.server/server/plugins/engine/components/index.js.map +1 -1
  17. package/.server/server/plugins/engine/helpers.js +2 -1
  18. package/.server/server/plugins/engine/helpers.js.map +1 -1
  19. package/.server/server/plugins/engine/index.js +4 -1
  20. package/.server/server/plugins/engine/index.js.map +1 -1
  21. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
  22. package/.server/server/plugins/engine/models/FormModel.js +2 -0
  23. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  24. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -1
  25. package/.server/server/plugins/engine/pageControllers/PageController.js +2 -8
  26. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  27. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +7 -0
  28. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  29. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +8 -2
  30. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  31. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -1
  32. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  33. package/.server/server/plugins/engine/pageControllers/helpers/state.d.ts +11 -0
  34. package/.server/server/plugins/engine/pageControllers/helpers/state.js +66 -0
  35. package/.server/server/plugins/engine/pageControllers/helpers/state.js.map +1 -0
  36. package/.server/server/plugins/engine/routes/index.js +2 -1
  37. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  38. package/.server/server/plugins/engine/services/formsService.d.ts +6 -0
  39. package/.server/server/plugins/engine/services/formsService.js +10 -0
  40. package/.server/server/plugins/engine/services/formsService.js.map +1 -1
  41. package/.server/server/plugins/engine/services/formsService.test.js +14 -0
  42. package/.server/server/plugins/engine/services/formsService.test.js.map +1 -0
  43. package/.server/server/plugins/engine/types.d.ts +4 -0
  44. package/.server/server/plugins/engine/types.js.map +1 -1
  45. package/.server/server/plugins/engine/views/components/declarationfield.html +1 -1
  46. package/.server/server/plugins/engine/views/components/hiddenfield.html +3 -0
  47. package/.server/server/plugins/engine/views/components/markdown.html +1 -1
  48. package/.server/server/plugins/engine/views/confirmation.html +6 -1
  49. package/.server/server/plugins/engine/views/partials/form.html +9 -1
  50. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  51. package/.server/server/services/cacheService.d.ts +3 -7
  52. package/.server/server/services/cacheService.js.map +1 -1
  53. package/.server/server/types.d.ts +1 -0
  54. package/.server/server/types.js.map +1 -1
  55. package/.server/server/utils/file-form-service.d.ts +6 -0
  56. package/.server/server/utils/file-form-service.js +22 -1
  57. package/.server/server/utils/file-form-service.js.map +1 -1
  58. package/.server/server/utils/file-form-service.test.js +86 -0
  59. package/.server/server/utils/file-form-service.test.js.map +1 -0
  60. package/package.json +3 -2
  61. package/src/server/forms/components.json +2 -2
  62. package/src/server/plugins/engine/components/DeclarationField.test.ts +24 -0
  63. package/src/server/plugins/engine/components/DeclarationField.ts +20 -2
  64. package/src/server/plugins/engine/components/HiddenField.test.ts +188 -0
  65. package/src/server/plugins/engine/components/HiddenField.ts +68 -0
  66. package/src/server/plugins/engine/components/Markdown.ts +4 -1
  67. package/src/server/plugins/engine/components/helpers/components.ts +5 -0
  68. package/src/server/plugins/engine/components/helpers/helpers.test.ts +17 -0
  69. package/src/server/plugins/engine/components/index.ts +1 -0
  70. package/src/server/plugins/engine/helpers.ts +2 -1
  71. package/src/server/plugins/engine/index.ts +5 -2
  72. package/src/server/plugins/engine/models/FormModel.ts +3 -0
  73. package/src/server/plugins/engine/pageControllers/PageController.test.ts +4 -11
  74. package/src/server/plugins/engine/pageControllers/PageController.ts +1 -9
  75. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +7 -0
  76. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +9 -2
  77. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +5 -1
  78. package/src/server/plugins/engine/pageControllers/helpers/state.test.ts +221 -0
  79. package/src/server/plugins/engine/pageControllers/helpers/state.ts +93 -0
  80. package/src/server/plugins/engine/routes/index.test.ts +24 -11
  81. package/src/server/plugins/engine/routes/index.ts +1 -1
  82. package/src/server/plugins/engine/services/formsService.js +10 -0
  83. package/src/server/plugins/engine/services/formsService.test.js +21 -0
  84. package/src/server/plugins/engine/types.ts +5 -0
  85. package/src/server/plugins/engine/views/components/declarationfield.html +1 -1
  86. package/src/server/plugins/engine/views/components/hiddenfield.html +3 -0
  87. package/src/server/plugins/engine/views/components/markdown.html +1 -1
  88. package/src/server/plugins/engine/views/confirmation.html +6 -1
  89. package/src/server/plugins/engine/views/partials/form.html +9 -1
  90. package/src/server/services/cacheService.ts +3 -2
  91. package/src/server/types.ts +1 -0
  92. package/src/server/utils/file-form-service.js +27 -1
  93. package/src/server/utils/file-form-service.test.js +114 -0
@@ -33,6 +33,12 @@ export class FileFormService {
33
33
  * @returns {FormMetadata}
34
34
  */
35
35
  getFormMetadata(slug: string): FormMetadata;
36
+ /**
37
+ * Get the form metadata by form id
38
+ * @param {string} id - the form id
39
+ * @returns {FormMetadata}
40
+ */
41
+ getFormMetadataById(id: string): FormMetadata;
36
42
  /**
37
43
  * Get the form defintion by id
38
44
  * @param {string} id - the form id
@@ -89,6 +89,19 @@ export class FileFormService {
89
89
  return metadata;
90
90
  }
91
91
 
92
+ /**
93
+ * Get the form metadata by form id
94
+ * @param {string} id - the form id
95
+ * @returns {FormMetadata}
96
+ */
97
+ getFormMetadataById(id) {
98
+ const metadata = Array.from(this.#metadata.values()).find(form => form.id === id);
99
+ if (!metadata) {
100
+ throw new Error(`Form metadata id '${id}' not found`);
101
+ }
102
+ return metadata;
103
+ }
104
+
92
105
  /**
93
106
  * Get the form defintion by id
94
107
  * @param {string} id - the form id
@@ -116,6 +129,14 @@ export class FileFormService {
116
129
  getFormMetadata: slug => {
117
130
  return Promise.resolve(this.getFormMetadata(slug));
118
131
  },
132
+ /**
133
+ * Get the form metadata by form id
134
+ * @param {string} id
135
+ * @returns {Promise<FormMetadata>}
136
+ */
137
+ getFormMetadataById: id => {
138
+ return Promise.resolve(this.getFormMetadataById(id));
139
+ },
119
140
  /**
120
141
  * Get the form defintion by id
121
142
  * @param {string} id
@@ -129,6 +150,6 @@ export class FileFormService {
129
150
  }
130
151
 
131
152
  /**
132
- * @import { FormMetadata, FormDefinition } from '@defra/forms-model'
153
+ * @import { FormMetadata, FormDefinition, FormStatus } from '@defra/forms-model'
133
154
  */
134
155
  //# sourceMappingURL=file-form-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-form-service.js","names":["fs","path","YAML","FileFormService","metadata","Map","definition","addForm","filepath","readForm","set","slug","id","ext","extname","toLowerCase","readJsonForm","readYamlForm","Error","JSON","parse","readFile","getFormMetadata","get","getFormDefinition","toFormsService","Promise","resolve"],"sources":["../../../src/server/utils/file-form-service.js"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'node:path'\n\nimport YAML from 'yaml'\n\n/**\n * FileFormService class\n */\nexport class FileFormService {\n /**\n * The map of form metadatas by slug\n * @type {Map<string, FormMetadata>}\n */\n #metadata = new Map()\n\n /**\n * The map of form definitions by id\n * @type {Map<string, FormDefinition>}\n */\n #definition = new Map()\n\n /**\n * Add form from a file\n * @param {string} filepath - the file path\n * @param {FormMetadata} metadata - the metadata to use for this form\n * @returns {Promise<FormDefinition>}\n */\n async addForm(filepath, metadata) {\n const definition = await this.readForm(filepath)\n\n this.#metadata.set(metadata.slug, metadata)\n this.#definition.set(metadata.id, definition)\n\n return definition\n }\n\n /**\n * Read the form definition from file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readForm(filepath) {\n const ext = path.extname(filepath).toLowerCase()\n\n switch (ext) {\n case '.json':\n return this.readJsonForm(filepath)\n case '.yaml':\n return this.readYamlForm(filepath)\n default:\n throw new Error(`Invalid file extension '${ext}'`)\n }\n }\n\n /**\n * Read the form definition from a json file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readJsonForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = JSON.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Read the form definition from a yaml file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readYamlForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = YAML.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Get the form metadata by slug\n * @param {string} slug - the form slug\n * @returns {FormMetadata}\n */\n getFormMetadata(slug) {\n const metadata = this.#metadata.get(slug)\n\n if (!metadata) {\n throw new Error(`Form metadata '${slug}' not found`)\n }\n\n return metadata\n }\n\n /**\n * Get the form defintion by id\n * @param {string} id - the form id\n * @returns {FormDefinition}\n */\n getFormDefinition(id) {\n const definition = this.#definition.get(id)\n\n if (!definition) {\n throw new Error(`Form definition '${id}' not found`)\n }\n\n return definition\n }\n\n /**\n * Returns a FormsService compliant interface\n * @returns {import('~/src/server/types.js').FormsService}\n */\n toFormsService() {\n return {\n /**\n * Get the form metadata by slug\n * @param {string} slug\n * @returns {Promise<FormMetadata>}\n */\n getFormMetadata: (slug) => {\n return Promise.resolve(this.getFormMetadata(slug))\n },\n\n /**\n * Get the form defintion by id\n * @param {string} id\n * @returns {Promise<FormDefinition>}\n */\n getFormDefinition: (id) => {\n return Promise.resolve(this.getFormDefinition(id))\n }\n }\n }\n}\n\n/**\n * @import { FormMetadata, FormDefinition } from '@defra/forms-model'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"file-form-service.js","names":["fs","path","YAML","FileFormService","metadata","Map","definition","addForm","filepath","readForm","set","slug","id","ext","extname","toLowerCase","readJsonForm","readYamlForm","Error","JSON","parse","readFile","getFormMetadata","get","getFormMetadataById","Array","from","values","find","form","getFormDefinition","toFormsService","Promise","resolve"],"sources":["../../../src/server/utils/file-form-service.js"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'node:path'\n\nimport YAML from 'yaml'\n\n/**\n * FileFormService class\n */\nexport class FileFormService {\n /**\n * The map of form metadatas by slug\n * @type {Map<string, FormMetadata>}\n */\n #metadata = new Map()\n\n /**\n * The map of form definitions by id\n * @type {Map<string, FormDefinition>}\n */\n #definition = new Map()\n\n /**\n * Add form from a file\n * @param {string} filepath - the file path\n * @param {FormMetadata} metadata - the metadata to use for this form\n * @returns {Promise<FormDefinition>}\n */\n async addForm(filepath, metadata) {\n const definition = await this.readForm(filepath)\n\n this.#metadata.set(metadata.slug, metadata)\n this.#definition.set(metadata.id, definition)\n\n return definition\n }\n\n /**\n * Read the form definition from file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readForm(filepath) {\n const ext = path.extname(filepath).toLowerCase()\n\n switch (ext) {\n case '.json':\n return this.readJsonForm(filepath)\n case '.yaml':\n return this.readYamlForm(filepath)\n default:\n throw new Error(`Invalid file extension '${ext}'`)\n }\n }\n\n /**\n * Read the form definition from a json file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readJsonForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = JSON.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Read the form definition from a yaml file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readYamlForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = YAML.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Get the form metadata by slug\n * @param {string} slug - the form slug\n * @returns {FormMetadata}\n */\n getFormMetadata(slug) {\n const metadata = this.#metadata.get(slug)\n\n if (!metadata) {\n throw new Error(`Form metadata '${slug}' not found`)\n }\n\n return metadata\n }\n\n /**\n * Get the form metadata by form id\n * @param {string} id - the form id\n * @returns {FormMetadata}\n */\n getFormMetadataById(id) {\n const metadata = Array.from(this.#metadata.values()).find(\n (form) => form.id === id\n )\n\n if (!metadata) {\n throw new Error(`Form metadata id '${id}' not found`)\n }\n\n return metadata\n }\n\n /**\n * Get the form defintion by id\n * @param {string} id - the form id\n * @returns {FormDefinition}\n */\n getFormDefinition(id) {\n const definition = this.#definition.get(id)\n\n if (!definition) {\n throw new Error(`Form definition '${id}' not found`)\n }\n\n return definition\n }\n\n /**\n * Returns a FormsService compliant interface\n * @returns {import('~/src/server/types.js').FormsService}\n */\n toFormsService() {\n return {\n /**\n * Get the form metadata by slug\n * @param {string} slug\n * @returns {Promise<FormMetadata>}\n */\n getFormMetadata: (slug) => {\n return Promise.resolve(this.getFormMetadata(slug))\n },\n\n /**\n * Get the form metadata by form id\n * @param {string} id\n * @returns {Promise<FormMetadata>}\n */\n getFormMetadataById: (id) => {\n return Promise.resolve(this.getFormMetadataById(id))\n },\n\n /**\n * Get the form defintion by id\n * @param {string} id\n * @returns {Promise<FormDefinition>}\n */\n getFormDefinition: (id) => {\n return Promise.resolve(this.getFormDefinition(id))\n }\n }\n }\n}\n\n/**\n * @import { FormMetadata, FormDefinition, FormStatus } from '@defra/forms-model'\n */\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,86 @@
1
+ import { join } from 'node:path';
2
+ import { FormStatus } from "../routes/types.js";
3
+ import { FileFormService } from "./file-form-service.js";
4
+ describe('File-form-service', () => {
5
+ /** @type {FileFormService} */
6
+ let service;
7
+ beforeEach(async () => {
8
+ const now = new Date();
9
+ const user = {
10
+ id: 'user',
11
+ displayName: 'Username'
12
+ };
13
+ const author = {
14
+ createdAt: now,
15
+ createdBy: user,
16
+ updatedAt: now,
17
+ updatedBy: user
18
+ };
19
+ service = new FileFormService();
20
+ const metadata = {
21
+ organisation: 'Defra',
22
+ teamName: 'Team name',
23
+ teamEmail: 'team@defra.gov.uk',
24
+ submissionGuidance: "Thanks for your submission, we'll be in touch",
25
+ notificationEmail: 'email@domain.com',
26
+ ...author,
27
+ live: author
28
+ };
29
+ await service.addForm(`${join(import.meta.dirname, '../../../test/form/definitions')}/components.json`, {
30
+ ...metadata,
31
+ id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
32
+ title: 'Form test',
33
+ slug: 'form-test'
34
+ });
35
+ });
36
+ describe('metadata by slug', () => {
37
+ it('should get form metadata by slug', () => {
38
+ const meta = service.getFormMetadata('form-test');
39
+ expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31');
40
+ expect(meta.title).toBe('Form test');
41
+ });
42
+ it('should throw if not found', () => {
43
+ expect(() => service.getFormMetadata('form-test-missing')).toThrow("Form metadata 'form-test-missing' not found");
44
+ });
45
+ });
46
+ describe('metadata by id', () => {
47
+ it('should get form metadata by id', () => {
48
+ const meta = service.getFormMetadataById('95e92559-968d-44ae-8666-2b1ad3dffd31');
49
+ expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31');
50
+ expect(meta.title).toBe('Form test');
51
+ });
52
+ it('should throw if not found', () => {
53
+ expect(() => service.getFormMetadataById('id-missing')).toThrow("Form metadata id 'id-missing' not found");
54
+ });
55
+ });
56
+ describe('definition by id', () => {
57
+ it('should get form definition by id', () => {
58
+ const form = service.getFormDefinition('95e92559-968d-44ae-8666-2b1ad3dffd31');
59
+ expect(form.name).toBe('All components');
60
+ expect(form.startPage).toBe('/all-components');
61
+ });
62
+ it('should throw if not found', () => {
63
+ expect(() => service.getFormDefinition('id-missing')).toThrow("Form definition 'id-missing' not found");
64
+ });
65
+ });
66
+ describe('toFormsService', () => {
67
+ it('should create interface', async () => {
68
+ const interfaceImpl = service.toFormsService();
69
+ const res1 = await interfaceImpl.getFormMetadata('form-test');
70
+ expect(res1.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31');
71
+ expect(res1.title).toBe('Form test');
72
+ const res2 = await interfaceImpl.getFormMetadataById('95e92559-968d-44ae-8666-2b1ad3dffd31');
73
+ expect(res2.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31');
74
+ expect(res2.title).toBe('Form test');
75
+ const res3 = await interfaceImpl.getFormDefinition('95e92559-968d-44ae-8666-2b1ad3dffd31', FormStatus.Draft);
76
+ expect(res3?.name).toBe('All components');
77
+ expect(res3?.startPage).toBe('/all-components');
78
+ });
79
+ });
80
+ describe('readForm', () => {
81
+ it('should throw if invalid extension', async () => {
82
+ await expect(service.readForm('/some-folder/some-file.bad')).rejects.toThrow("Invalid file extension '.bad'");
83
+ });
84
+ });
85
+ });
86
+ //# sourceMappingURL=file-form-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-form-service.test.js","names":["join","FormStatus","FileFormService","describe","service","beforeEach","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","live","addForm","import","meta","dirname","title","slug","it","getFormMetadata","expect","toBe","toThrow","getFormMetadataById","form","getFormDefinition","name","startPage","interfaceImpl","toFormsService","res1","res2","res3","Draft","readForm","rejects"],"sources":["../../../src/server/utils/file-form-service.test.js"],"sourcesContent":["import { join } from 'node:path'\n\nimport { FormStatus } from '~/src/server/routes/types.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\ndescribe('File-form-service', () => {\n /** @type {FileFormService} */\n let service\n beforeEach(async () => {\n const now = new Date()\n const user = { id: 'user', displayName: 'Username' }\n const author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n }\n service = new FileFormService()\n const metadata = {\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: 'email@domain.com',\n ...author,\n live: author\n }\n await service.addForm(\n `${join(import.meta.dirname, '../../../test/form/definitions')}/components.json`,\n {\n ...metadata,\n id: '95e92559-968d-44ae-8666-2b1ad3dffd31',\n title: 'Form test',\n slug: 'form-test'\n }\n )\n })\n\n describe('metadata by slug', () => {\n it('should get form metadata by slug', () => {\n const meta = service.getFormMetadata('form-test')\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadata('form-test-missing')).toThrow(\n \"Form metadata 'form-test-missing' not found\"\n )\n })\n })\n\n describe('metadata by id', () => {\n it('should get form metadata by id', () => {\n const meta = service.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadataById('id-missing')).toThrow(\n \"Form metadata id 'id-missing' not found\"\n )\n })\n })\n\n describe('definition by id', () => {\n it('should get form definition by id', () => {\n const form = service.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(form.name).toBe('All components')\n expect(form.startPage).toBe('/all-components')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormDefinition('id-missing')).toThrow(\n \"Form definition 'id-missing' not found\"\n )\n })\n })\n\n describe('toFormsService', () => {\n it('should create interface', async () => {\n const interfaceImpl = service.toFormsService()\n const res1 = await interfaceImpl.getFormMetadata('form-test')\n expect(res1.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res1.title).toBe('Form test')\n\n const res2 = await interfaceImpl.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(res2.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res2.title).toBe('Form test')\n\n const res3 = await interfaceImpl.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31',\n FormStatus.Draft\n )\n expect(res3?.name).toBe('All components')\n expect(res3?.startPage).toBe('/all-components')\n })\n })\n\n describe('readForm', () => {\n it('should throw if invalid extension', async () => {\n await expect(\n service.readForm('/some-folder/some-file.bad')\n ).rejects.toThrow(\"Invalid file extension '.bad'\")\n })\n })\n})\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.0.24",
3
+ "version": "4.0.26",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "license": "SEE LICENSE IN LICENSE",
72
72
  "dependencies": {
73
- "@defra/forms-model": "^3.0.580",
73
+ "@defra/forms-model": "^3.0.585",
74
74
  "@defra/hapi-tracing": "^1.29.0",
75
75
  "@elastic/ecs-pino-format": "^1.5.0",
76
76
  "@hapi/boom": "^10.0.1",
@@ -91,6 +91,7 @@
91
91
  "blankie": "^5.0.0",
92
92
  "blipp": "^4.0.2",
93
93
  "btoa": "^1.2.1",
94
+ "chokidar": "3.6.0",
94
95
  "convict": "^6.2.4",
95
96
  "date-fns": "^4.1.0",
96
97
  "dotenv": "^17.2.3",
@@ -139,7 +139,7 @@
139
139
  "type": "Markdown",
140
140
  "name": "markdown",
141
141
  "title": "Title",
142
- "content": "### This is a H3 in markdown\n\n[An internal link](http://localhost:3009/fictional-page)\n\n[An external link](https://defra.gov.uk/fictional-page)",
142
+ "content": "# Markdown - This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n\n[An internal link](http://localhost:3009/fictional-page)\n\n[An external link](https://defra.gov.uk/fictional-page)",
143
143
  "options": {},
144
144
  "schema": {}
145
145
  },
@@ -147,7 +147,7 @@
147
147
  "type": "DeclarationField",
148
148
  "name": "declaration",
149
149
  "title": "Declaration",
150
- "content": "By submitting this form, I agree to:\n\n- Provide accurate and complete information\n- Comply with all applicable regulations\n- Accept responsibility for any false statements",
150
+ "content": "# H1\nBy submitting this form, I agree to:\n\n- Provide accurate and complete information\n- Comply with all applicable regulations\n- Accept responsibility for any false statements",
151
151
  "hint": "Please read and confirm the following terms",
152
152
  "options": {
153
153
  "required": false
@@ -11,6 +11,8 @@ import {
11
11
  } from '~/src/server/plugins/engine/components/helpers/components.js'
12
12
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
13
13
  import definition from '~/test/form/definitions/blank.js'
14
+ import declarationWithGuidance from '~/test/form/definitions/declaration-with-guidance.js'
15
+ import declarationWithoutGuidance from '~/test/form/definitions/declaration-without-guidance.js'
14
16
  import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
15
17
 
16
18
  describe('DeclarationField', () => {
@@ -481,4 +483,26 @@ describe('DeclarationField', () => {
481
483
  expect(DeclarationField.isBool(true)).toBe(true)
482
484
  })
483
485
  })
486
+
487
+ describe('Markdown header starting level', () => {
488
+ test('should determine startHeadingLevel is 3 some guidance', () => {
489
+ const modelDecl = new FormModel(declarationWithGuidance, {
490
+ basePath: 'test'
491
+ })
492
+ const field = modelDecl.componentMap.get(
493
+ 'declarationField'
494
+ ) as DeclarationField
495
+ expect(field.headerStartLevel).toBe(3)
496
+ })
497
+
498
+ test('should determine startHeadingLevel is 2 when no guidance', () => {
499
+ const modelDecl = new FormModel(declarationWithoutGuidance, {
500
+ basePath: 'test'
501
+ })
502
+ const field = modelDecl.componentMap.get(
503
+ 'declarationField'
504
+ ) as DeclarationField
505
+ expect(field.headerStartLevel).toBe(2)
506
+ })
507
+ })
484
508
  })
@@ -1,4 +1,10 @@
1
- import { type DeclarationFieldComponent, type Item } from '@defra/forms-model'
1
+ import {
2
+ ComponentType,
3
+ hasFormComponents,
4
+ isFormType,
5
+ type DeclarationFieldComponent,
6
+ type Item
7
+ } from '@defra/forms-model'
2
8
  import joi, {
3
9
  type ArraySchema,
4
10
  type BooleanSchema,
@@ -30,6 +36,7 @@ export class DeclarationField extends FormComponent {
30
36
  declare formSchema: ArraySchema<StringSchema[]>
31
37
  declare stateSchema: BooleanSchema
32
38
  declare content: string
39
+ headerStartLevel: number
33
40
 
34
41
  constructor(
35
42
  def: DeclarationFieldComponent,
@@ -64,6 +71,16 @@ export class DeclarationField extends FormComponent {
64
71
  this.content = content
65
72
  this.declarationConfirmationLabel =
66
73
  options.declarationConfirmationLabel ?? this.DEFAULT_DECLARATION_LABEL
74
+ const formComponents = hasFormComponents(props.page?.pageDef)
75
+ ? props.page.pageDef.components
76
+ : []
77
+ const numOfQuestionsOnPage = formComponents.filter((q) =>
78
+ isFormType(q.type)
79
+ ).length
80
+ const hasGuidance = formComponents.some(
81
+ (comp, idx) => comp.type === ComponentType.Markdown && idx === 0
82
+ )
83
+ this.headerStartLevel = numOfQuestionsOnPage < 2 && !hasGuidance ? 2 : 3
67
84
  }
68
85
 
69
86
  getFormValueFromState(state: FormSubmissionState) {
@@ -133,7 +150,8 @@ export class DeclarationField extends FormComponent {
133
150
  value: 'true',
134
151
  checked: isChecked
135
152
  }
136
- ]
153
+ ],
154
+ headerStartLevel: this.headerStartLevel
137
155
  }
138
156
  }
139
157
 
@@ -0,0 +1,188 @@
1
+ import { ComponentType, type HiddenFieldComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import {
5
+ getAnswer,
6
+ type Field
7
+ } from '~/src/server/plugins/engine/components/helpers/components.js'
8
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
9
+ import definition from '~/test/form/definitions/blank.js'
10
+ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
11
+
12
+ describe('HiddenField', () => {
13
+ let model: FormModel
14
+
15
+ beforeEach(() => {
16
+ model = new FormModel(definition, {
17
+ basePath: 'test'
18
+ })
19
+ })
20
+
21
+ describe('Defaults', () => {
22
+ let def: HiddenFieldComponent
23
+ let collection: ComponentCollection
24
+ let field: Field
25
+
26
+ beforeEach(() => {
27
+ def = {
28
+ title: 'Hidden field',
29
+ name: 'myComponent',
30
+ type: ComponentType.HiddenField,
31
+ options: {}
32
+ } satisfies HiddenFieldComponent
33
+
34
+ collection = new ComponentCollection([def], { model })
35
+ field = collection.fields[0]
36
+ })
37
+
38
+ describe('Schema', () => {
39
+ it('uses component title as label as default', () => {
40
+ const { formSchema } = collection
41
+ const { keys } = formSchema.describe()
42
+
43
+ expect(keys).toHaveProperty(
44
+ 'myComponent',
45
+ expect.objectContaining({
46
+ flags: expect.objectContaining({
47
+ label: 'Hidden field'
48
+ })
49
+ })
50
+ )
51
+ })
52
+
53
+ it('uses component name as keys', () => {
54
+ const { formSchema } = collection
55
+ const { keys } = formSchema.describe()
56
+
57
+ expect(field.keys).toEqual(['myComponent'])
58
+ expect(field.collection).toBeUndefined()
59
+
60
+ for (const key of field.keys) {
61
+ expect(keys).toHaveProperty(key)
62
+ }
63
+ })
64
+
65
+ it('is required by default', () => {
66
+ const { formSchema } = collection
67
+ const { keys } = formSchema.describe()
68
+
69
+ expect(keys).toHaveProperty(
70
+ 'myComponent',
71
+ expect.objectContaining({
72
+ flags: expect.objectContaining({
73
+ presence: 'required'
74
+ })
75
+ })
76
+ )
77
+ })
78
+ it('accepts valid values', () => {
79
+ const result1 = collection.validate(getFormData('Hidden value'))
80
+ const result2 = collection.validate(getFormData('Hidden value 2'))
81
+
82
+ expect(result1.errors).toBeUndefined()
83
+ expect(result2.errors).toBeUndefined()
84
+ })
85
+
86
+ it('adds errors for empty value', () => {
87
+ const result = collection.validate(getFormData(''))
88
+
89
+ expect(result.errors).toEqual([
90
+ expect.objectContaining({
91
+ text: 'Enter hidden field'
92
+ })
93
+ ])
94
+ })
95
+
96
+ it('adds errors for invalid values', () => {
97
+ const result1 = collection.validate(getFormData(['invalid']))
98
+ const result2 = collection.validate(
99
+ // @ts-expect-error - Allow invalid param for test
100
+ getFormData({ unknown: 'invalid' })
101
+ )
102
+
103
+ expect(result1.errors).toBeTruthy()
104
+ expect(result2.errors).toBeTruthy()
105
+ })
106
+ })
107
+
108
+ describe('State', () => {
109
+ it('returns text from state', () => {
110
+ const state1 = getFormState('Hidden field')
111
+ const state2 = getFormState(null)
112
+
113
+ const answer1 = getAnswer(field, state1)
114
+ const answer2 = getAnswer(field, state2)
115
+
116
+ expect(answer1).toBe('Hidden field')
117
+ expect(answer2).toBe('')
118
+ })
119
+
120
+ it('returns payload from state', () => {
121
+ const state1 = getFormState('Hidden field')
122
+ const state2 = getFormState(null)
123
+
124
+ const payload1 = field.getFormDataFromState(state1)
125
+ const payload2 = field.getFormDataFromState(state2)
126
+
127
+ expect(payload1).toEqual(getFormData('Hidden field'))
128
+ expect(payload2).toEqual(getFormData())
129
+ })
130
+
131
+ it('returns value from state', () => {
132
+ const state1 = getFormState('Hidden field')
133
+ const state2 = getFormState(null)
134
+
135
+ const value1 = field.getFormValueFromState(state1)
136
+ const value2 = field.getFormValueFromState(state2)
137
+
138
+ expect(value1).toBe('Hidden field')
139
+ expect(value2).toBeUndefined()
140
+ })
141
+
142
+ it('returns context for conditions and form submission', () => {
143
+ const state1 = getFormState('Hidden field')
144
+ const state2 = getFormState(null)
145
+
146
+ const value1 = field.getContextValueFromState(state1)
147
+ const value2 = field.getContextValueFromState(state2)
148
+
149
+ expect(value1).toBe('Hidden field')
150
+ expect(value2).toBeNull()
151
+ })
152
+
153
+ it('returns state from payload', () => {
154
+ const payload1 = getFormData('Hidden field')
155
+ const payload2 = getFormData()
156
+
157
+ const value1 = field.getStateFromValidForm(payload1)
158
+ const value2 = field.getStateFromValidForm(payload2)
159
+
160
+ expect(value1).toEqual(getFormState('Hidden field'))
161
+ expect(value2).toEqual(getFormState(null))
162
+ })
163
+ })
164
+
165
+ describe('View model', () => {
166
+ it('sets Nunjucks component defaults', () => {
167
+ const viewModel = field.getViewModel(getFormData('Hidden field'))
168
+
169
+ expect(viewModel).toEqual(
170
+ expect.objectContaining({
171
+ label: { text: def.title },
172
+ name: 'myComponent',
173
+ id: 'myComponent',
174
+ value: 'Hidden field'
175
+ })
176
+ )
177
+ })
178
+ })
179
+
180
+ describe('AllPossibleErrors', () => {
181
+ it('should return errors', () => {
182
+ const errors = field.getAllPossibleErrors()
183
+ expect(errors.baseErrors).not.toBeEmpty()
184
+ expect(errors.advancedSettingsErrors).toBeEmpty()
185
+ })
186
+ })
187
+ })
188
+ })
@@ -0,0 +1,68 @@
1
+ import {
2
+ type HiddenFieldComponent,
3
+ type TextFieldComponent
4
+ } from '@defra/forms-model'
5
+ import joi, { type StringSchema } from 'joi'
6
+
7
+ import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
8
+ import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
9
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
10
+ import {
11
+ type ErrorMessageTemplateList,
12
+ type FormState,
13
+ type FormStateValue,
14
+ type FormSubmissionState
15
+ } from '~/src/server/plugins/engine/types.js'
16
+
17
+ export class HiddenField extends FormComponent {
18
+ declare formSchema: StringSchema
19
+ declare stateSchema: StringSchema
20
+ declare schema: TextFieldComponent['schema']
21
+ declare options: TextFieldComponent['options']
22
+
23
+ constructor(
24
+ def: HiddenFieldComponent,
25
+ props: ConstructorParameters<typeof FormComponent>[1]
26
+ ) {
27
+ super(def, props)
28
+
29
+ const { options } = def
30
+
31
+ let formSchema = joi.string().trim().label(this.label).required()
32
+
33
+ if (options.required === false) {
34
+ formSchema = formSchema.allow('')
35
+ }
36
+
37
+ this.formSchema = formSchema.default('')
38
+ this.stateSchema = formSchema.default(null).allow(null)
39
+ this.schema = {}
40
+ this.options = {}
41
+ }
42
+
43
+ getFormValueFromState(state: FormSubmissionState) {
44
+ const { name } = this
45
+ return this.getFormValue(state[name])
46
+ }
47
+
48
+ isValue(value?: FormStateValue | FormState): value is string {
49
+ return TextField.isText(value)
50
+ }
51
+
52
+ /**
53
+ * For error preview page that shows all possible errors on a component
54
+ */
55
+ getAllPossibleErrors(): ErrorMessageTemplateList {
56
+ return HiddenField.getAllPossibleErrors()
57
+ }
58
+
59
+ /**
60
+ * Static version of getAllPossibleErrors that doesn't require a component instance.
61
+ */
62
+ static getAllPossibleErrors(): ErrorMessageTemplateList {
63
+ return {
64
+ baseErrors: [{ type: 'required', template: messageTemplate.required }],
65
+ advancedSettingsErrors: []
66
+ }
67
+ }
68
+ }
@@ -5,6 +5,7 @@ import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentB
5
5
  export class Markdown extends ComponentBase {
6
6
  declare options: MarkdownComponent['options']
7
7
  content: MarkdownComponent['content']
8
+ headerStartLevel: number
8
9
 
9
10
  constructor(
10
11
  def: MarkdownComponent,
@@ -16,6 +17,7 @@ export class Markdown extends ComponentBase {
16
17
 
17
18
  this.content = content
18
19
  this.options = options
20
+ this.headerStartLevel = 2
19
21
  }
20
22
 
21
23
  getViewModel() {
@@ -23,7 +25,8 @@ export class Markdown extends ComponentBase {
23
25
 
24
26
  return {
25
27
  ...viewModel,
26
- content
28
+ content,
29
+ headerStartLevel: this.headerStartLevel
27
30
  }
28
31
  }
29
32
  }
@@ -34,6 +34,7 @@ export type Field = InstanceType<
34
34
  | typeof Components.TextField
35
35
  | typeof Components.UkAddressField
36
36
  | typeof Components.FileUploadField
37
+ | typeof Components.HiddenField
37
38
  >
38
39
 
39
40
  // Guidance component instances only
@@ -186,6 +187,10 @@ export function createComponent(
186
187
  case ComponentType.LatLongField:
187
188
  component = new Components.LatLongField(def, options)
188
189
  break
190
+
191
+ case ComponentType.HiddenField:
192
+ component = new Components.HiddenField(def, options)
193
+ break
189
194
  }
190
195
 
191
196
  if (typeof component === 'undefined') {
@@ -2,6 +2,7 @@ import { ComponentType, type ComponentDef } from '@defra/forms-model'
2
2
 
3
3
  import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
4
  import { EastingNorthingField } from '~/src/server/plugins/engine/components/EastingNorthingField.js'
5
+ import { HiddenField } from '~/src/server/plugins/engine/components/HiddenField.js'
5
6
  import { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
6
7
  import { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
7
8
  import { OsGridRefField } from '~/src/server/plugins/engine/components/OsGridRefField.js'
@@ -96,6 +97,22 @@ describe('helpers tests', () => {
96
97
  expect(component.name).toBe('testField')
97
98
  expect(component.title).toBe('Test National Grid')
98
99
  })
100
+
101
+ test('should create HiddenField component', () => {
102
+ const component = createComponent(
103
+ {
104
+ type: ComponentType.HiddenField,
105
+ name: 'hiddenField',
106
+ title: 'Hidden field',
107
+ options: {}
108
+ },
109
+ { model: formModel }
110
+ )
111
+
112
+ expect(component).toBeInstanceOf(HiddenField)
113
+ expect(component.name).toBe('hiddenField')
114
+ expect(component.title).toBe('Hidden field')
115
+ })
99
116
  })
100
117
 
101
118
  describe('ComponentBase tests', () => {
@@ -28,3 +28,4 @@ export { EastingNorthingField } from '~/src/server/plugins/engine/components/Eas
28
28
  export { OsGridRefField } from '~/src/server/plugins/engine/components/OsGridRefField.js'
29
29
  export { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
30
30
  export { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
31
+ export { HiddenField } from '~/src/server/plugins/engine/components/HiddenField.js'