@ukhomeoffice/cop-react-form-renderer 6.15.6-alpha → 6.15.6

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 (43) hide show
  1. package/dist/components/CollectionSummary/BannerStrip.scss +4 -0
  2. package/dist/components/FormComponent/FormComponent.js +6 -0
  3. package/dist/components/FormComponent/helpers/addLabel.js +3 -2
  4. package/dist/components/FormPage/FormPage.js +28 -17
  5. package/dist/components/FormPage/FormPage.test.js +53 -0
  6. package/dist/components/FormRenderer/FormRenderer.js +5 -10
  7. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/forms/cop-reassign-task-to-rcc.json +445 -0
  8. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/forms/form-hidden-component-show-when-in-component-and-page.json +62 -0
  9. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/forms/form-hidden-page-same-component-reused.json +61 -0
  10. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/forms/form-page-same-component-reused-one-shown.json +74 -0
  11. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/cop-airpax-carrier.json +407 -0
  12. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/cop-airpax-change-what-happened-before.json +300 -0
  13. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/data-hidden-component-show-when-in-component-and-page.json +6 -0
  14. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/data-hidden-page-same-component-reused.json +6 -0
  15. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/data-page-same-component-reused-one-shown.json +8 -0
  16. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/reassign-to-rcc.json +72 -0
  17. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/cop-airpax-change-what-happened-after.json +280 -0
  18. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/data-hidden-component-show-when-in-component-and-page-removed.json +5 -0
  19. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/data-hidden-page-same-component-reused-removed.json +5 -0
  20. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/data-page-same-component-reused-one-shown-removed.json +7 -0
  21. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutes.js +259 -289
  22. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutes.test.js +89 -27
  23. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutesUtils.js +389 -0
  24. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutesUtils.test.js +559 -0
  25. package/dist/components/FormRenderer/onCYAAction.js +4 -2
  26. package/dist/components/FormRenderer/onCYAAction.test.js +5 -0
  27. package/dist/components/FormRenderer/onPageAction.js +0 -1
  28. package/dist/hooks/useGetRequest.js +15 -15
  29. package/dist/hooks/useRefData.js +3 -2
  30. package/dist/utils/Component/getComponentTests/getComponent.multifile.test.js +2 -1
  31. package/dist/utils/Component/getDefaultValueFromConfig.js +2 -1
  32. package/dist/utils/Condition/meetsCondition.js +26 -12
  33. package/dist/utils/Condition/meetsCondition.test.js +21 -0
  34. package/dist/utils/Data/getAutocompleteSource.js +68 -51
  35. package/dist/utils/Data/getAutocompleteSource.test.js +31 -18
  36. package/dist/utils/Operate/doesContainValue.js +34 -0
  37. package/dist/utils/Operate/doesContainValue.test.js +75 -0
  38. package/dist/utils/Operate/runPageOperations.js +2 -0
  39. package/dist/utils/Validate/validateOnPageLoad.js +23 -0
  40. package/dist/utils/Validate/validateOnPageLoad.test.js +88 -0
  41. package/package.json +4 -4
  42. package/dist/components/FormRenderer/helpers/deleteNodeByPath.js +0 -29
  43. package/dist/components/FormRenderer/helpers/deleteNodeByPath.test.js +0 -56
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.toArray = exports.removeObjectWithOnlySingleIdField = exports.removeEmptyArraysAndUnusedCollectionIDs = exports.pruneCollectionEntry = exports.isShowEntity = exports.getNestedQuestionPath = exports.getImmediateParent = exports.getDependencyObjectFromPath = exports.getDependencies = exports.findComponentDefinitionInForm = exports.deleteNodeByPath = exports.deleteNodeAndOptions = exports.deleteCorrespondingMetaInfo = exports.deleteComponentData = exports.addValue = void 0;
7
+ var _Condition = _interopRequireDefault(require("../../../utils/Condition"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ /* eslint-disable no-param-reassign */
10
+
11
+ /**
12
+ * Add a value to a map of arrays. If the key exists, append the value to the array.
13
+ * If not, create the map entry with that key.
14
+ *
15
+ * @param {*} key
16
+ * @param {*} value
17
+ * @param {*} multiMap
18
+ */
19
+ const addValue = (key, value, multiMap) => {
20
+ if (!multiMap.has(key)) {
21
+ multiMap.set(key, []);
22
+ }
23
+ multiMap.get(key).push(value);
24
+ };
25
+
26
+ /**
27
+ * Given a path, remove a node from this path within an object.
28
+ *
29
+ * @param {Object} payload Javascript object from which the node will be deleted. Updated by the method.
30
+ * @param {String} path A string containing a decimal point delimited path specifying the node to delete.
31
+ * @return {void}, obj above updated.
32
+ */
33
+ exports.addValue = addValue;
34
+ const deleteNodeByPath = (payload, path) => {
35
+ if (Array.isArray(payload)) {
36
+ // If payload is an array, recursively call deleteNodeByPath on each element
37
+ for (let i = 0; i < payload.length; i += 1) {
38
+ deleteNodeByPath(payload[i], path);
39
+ }
40
+ }
41
+ const keys = path.split('.');
42
+ let current = payload;
43
+ for (let i = 0; i < keys.length - 1; i += 1) {
44
+ current = current[keys[i]];
45
+ if (current === undefined) {
46
+ return;
47
+ }
48
+ }
49
+ if (current[keys[keys.length - 1]]) {
50
+ delete current[keys[keys.length - 1]];
51
+ }
52
+ };
53
+
54
+ /**
55
+ * Return the immediate parent of a path string passed in.
56
+ * Useful for processing 'options' in a form, as nested fieldIds are placed in the payload at
57
+ * the same level in the heirarchy as the option question they relate to.
58
+ *
59
+ * @param {String} path Decimal point delimited path.
60
+ * @returns {String} Immediate parent of the path passed in.
61
+ */
62
+ exports.deleteNodeByPath = deleteNodeByPath;
63
+ const getImmediateParent = path => {
64
+ if (typeof path !== 'string' || !path.includes('.')) {
65
+ return null;
66
+ }
67
+ const parts = path.split('.');
68
+ parts.pop();
69
+ return parts.join('.');
70
+ };
71
+
72
+ /**
73
+ * Questions can be nested within options, eg if you answer 'yes' to a radio question, this can
74
+ * reveal additional nested questions.
75
+ * The path of the fields from these nested questions are at the same level as the original option
76
+ * answer, so this utility derives the nested question path from the option path.
77
+ *
78
+ * @param {String} optionPath Decimal point delimited path
79
+ * @param {String} nestedFieldId fieldId (dataname) of the nested question within the above option
80
+ * @returns {String} Fully qualified path of the nested question
81
+ */
82
+ exports.getImmediateParent = getImmediateParent;
83
+ const getNestedQuestionPath = (optionPath, nestedFieldId) => {
84
+ const parentPath = getImmediateParent(optionPath);
85
+ return parentPath ? "".concat(parentPath, ".").concat(nestedFieldId) : nestedFieldId;
86
+ };
87
+
88
+ /**
89
+ * If the component has options, go through each option removing the data for any nested fields.
90
+ * Required as nested options are in the payload at the same heirarchical level.
91
+ *
92
+ * There are occasions when a field shouldn't be removed from the payload, eg if a form is being used for 2 purposes
93
+ * eg the cop-reassign-task-to-rcc.json, and the field is only being hidden for display purposes. Allow these fields
94
+ * to be preserved with a property on the component.
95
+
96
+ * @param {Object} payload Javascript object from which the node will be deleted. Updated by the method.
97
+ * @param {String} path A string containing a decimal point delimited path specifying the node to delete.
98
+ * @param {Object} component The form component representing the path being deleted
99
+ * @return {void}, obj above updated.
100
+ */
101
+ exports.getNestedQuestionPath = getNestedQuestionPath;
102
+ const deleteNodeAndOptions = (payload, path, component) => {
103
+ var _component$data;
104
+ if (component.preserveInPayload) return;
105
+ deleteNodeByPath(payload, path);
106
+ // If the component has options, go through each option removing the data for any nested fields. Required as nested options are in the payload at the same heirarchical level.
107
+ if (component !== null && component !== void 0 && (_component$data = component.data) !== null && _component$data !== void 0 && _component$data.options) {
108
+ var _component$data2;
109
+ component === null || component === void 0 || (_component$data2 = component.data) === null || _component$data2 === void 0 || (_component$data2 = _component$data2.options) === null || _component$data2 === void 0 || _component$data2.forEach(option => {
110
+ var _option$nested;
111
+ (_option$nested = option.nested) === null || _option$nested === void 0 || _option$nested.forEach(nested => {
112
+ deleteNodeByPath(payload, getNestedQuestionPath(path, nested.fieldId));
113
+ });
114
+ });
115
+ }
116
+ };
117
+
118
+ /**
119
+ * Pruning a collection payload may have resulted in objects that are only left with their 'id' field, which isn't data
120
+ * but added by the renderer to find the activeId. If so, remove these objects entirely as they have been pruned.
121
+ *
122
+ * @param {Array} array Array of objects. Each object which has only 1 remaining field called 'id' should be removed.
123
+ * @return {void}, array above updated.
124
+ */
125
+ exports.deleteNodeAndOptions = deleteNodeAndOptions;
126
+ const removeObjectWithOnlySingleIdField = array => {
127
+ for (let i = array.length - 1; i >= 0; i -= 1) {
128
+ const obj = array[i];
129
+ if (Object.keys(obj).length === 1 && Object.keys(obj)[0] === 'id') {
130
+ array.splice(i, 1);
131
+ }
132
+ }
133
+ };
134
+
135
+ /**
136
+ * Helper method to establish all the payload paths (dependencies) that an entity (page or component) is
137
+ * dependent on through its show_when rule.
138
+ *
139
+ * This will be used to build a graph of chained dependencies to establish in which order
140
+ * to resolve dependencies. The exact rule is not required at this point, just the dependencies.
141
+ *
142
+ * The form specification allows complex show_when blocks including 1...n levels of nesting, so
143
+ * recursively search through the show_when block looking for all 'field' data items.
144
+ *
145
+ * @param {Object} entity Entity whose show_when rule is to be searched for 'field' data items within.
146
+ * @returns {Set} Set of payload paths that the entity (page or component) is dependent on.
147
+ */
148
+ exports.removeObjectWithOnlySingleIdField = removeObjectWithOnlySingleIdField;
149
+ const getDependencies = entity => {
150
+ const findShowWhenFields = (showWhenObject, showWhenFields) => {
151
+ if (typeof showWhenObject === 'object' && showWhenObject !== null) {
152
+ if (Array.isArray(showWhenObject)) {
153
+ showWhenObject.forEach(value => {
154
+ findShowWhenFields(value, showWhenFields);
155
+ });
156
+ } else {
157
+ Object.keys(showWhenObject).forEach(key => {
158
+ if (key === 'field') {
159
+ showWhenFields.push(showWhenObject[key]);
160
+ }
161
+ findShowWhenFields(showWhenObject[key], showWhenFields);
162
+ });
163
+ }
164
+ }
165
+ return showWhenFields;
166
+ };
167
+ return entity.show_when ? new Set(findShowWhenFields(entity.show_when, []) || []) : null;
168
+ };
169
+
170
+ /**
171
+ * Some show_when field values point to data items that are within objects provided by
172
+ * external calls to refdata, eg modeOfTransport.id. These won't map directly to the path
173
+ * keyed components in the allComponents map, so go back up levels in the path until we find
174
+ * the component that creates this field.
175
+ *
176
+ * This component might not be found at all, as the dependency might be on a field that is either
177
+ * provided by cop-ui (eg jobHolderStaffDetails.linemanagerEmail), or by the "addToFormData" function
178
+ * (eg epmsSubmitted). These will be leaf level in the dependency chain so component is not required.
179
+ *
180
+ * @param {String} optionPath Decimal point delimited path
181
+ * @param {String} nestedFieldId fieldId (dataname) of the nested question within the above option
182
+ * @returns {String} Fully qualified path of the nested question
183
+ *
184
+ */
185
+ exports.getDependencies = getDependencies;
186
+ const getDependencyObjectFromPath = (dependencyPath, allComponents) => {
187
+ const segments = dependencyPath.split(".");
188
+ for (let i = segments.length; i > 0; i -= 1) {
189
+ const currentPath = segments.slice(0, i).join(".");
190
+ const dependencyObject = allComponents.get(currentPath);
191
+ if (dependencyObject) return dependencyObject;
192
+ }
193
+ return null;
194
+ };
195
+
196
+ /**
197
+ *
198
+ * Evaluate the show_when rule to establish if the component should be shown (and the
199
+ * payload data should not be pruned). If there is no show_when rule, then return true.
200
+ *
201
+ * @param {*} entity A page or component that may have a show_when rule associated with it
202
+ * @param {*} data The payload data used to evaluate the show_when rule
203
+ * @returns {boolean} true if the show_when rule evaluate to true, or there is no show_when rule
204
+ */
205
+ exports.getDependencyObjectFromPath = getDependencyObjectFromPath;
206
+ const isShowEntity = (entity, data) => {
207
+ var _entity$show_when;
208
+ // If there is no rule set, then the entity can be shown
209
+ if (!entity.show_when) {
210
+ return true;
211
+ }
212
+ if (((_entity$show_when = entity.show_when) === null || _entity$show_when === void 0 ? void 0 : _entity$show_when.type) === "or") {
213
+ return _Condition.default.meetsOne(entity, data);
214
+ }
215
+ return _Condition.default.meetsAll(entity, data);
216
+ };
217
+
218
+ /**
219
+ *
220
+ * Components can be assigned to pages in 2 ways in the form specification:
221
+ *
222
+ * 1 - They can be included in the page's components array with the 'use' field, e.g.
223
+ * {
224
+ * "use": "port"
225
+ * }
226
+ * This "use" value will normally match a component's id, but can match its fieldId
227
+ * NB. In this case, a show_when rule can be applied here, which will supercede any show_when in the component. e.g.
228
+ *
229
+ * {
230
+ * "use": "port",
231
+ * "show_when": ....
232
+ * }
233
+ *
234
+ * 2 - The entire component can be embedded as an entry in the page's component array.
235
+ *
236
+ *
237
+ * @param {*} componentId The id of the component to find in the form.
238
+ * @param {*} componentByIdMap A map of all the form's components keyed on Id, for better performance than searching the form.
239
+ * @param {*} componentByFieldIdMap A map of all the form's components keyed on fieldId, for better performance than searching the form.
240
+ * @returns {Object} A cloned component object, containing the full definition of that component.
241
+ */
242
+ exports.isShowEntity = isShowEntity;
243
+ const findComponentDefinitionInForm = (pageComponentDef, componentByIdMap, componentByFieldIdMap) => {
244
+ var _ref, _componentByIdMap$get;
245
+ const componentInForm = (_ref = (_componentByIdMap$get = componentByIdMap.get(pageComponentDef.use)) !== null && _componentByIdMap$get !== void 0 ? _componentByIdMap$get : componentByFieldIdMap.get(pageComponentDef.use)) !== null && _ref !== void 0 ? _ref : pageComponentDef;
246
+
247
+ // Create clone of component, so processing can make changes to it without changing the form
248
+ let componentInFormClone = JSON.parse(JSON.stringify(componentInForm));
249
+ if (pageComponentDef.use && pageComponentDef.show_when) {
250
+ componentInFormClone.show_when = pageComponentDef.show_when;
251
+ }
252
+ return componentInFormClone;
253
+ };
254
+
255
+ /**
256
+ *
257
+ * When documents are added to the payload of type multifile, they are also added to a section of the payload
258
+ * 'meta', specifically an array 'documents'. When we remove the multifile elements, we need to remove
259
+ * the corresponding meta.document entries.
260
+ *
261
+ * @param {*} component The component definition being deleted that needs corresponding meta data also deleted
262
+ * @param {*} collectionDataObject The payload containing the file data being deleted (for which the corresponding meta needs deleting)
263
+ * @param {*} formData The entire payload, which will include the meta section
264
+ * @returns {void}, as the formData will be updated in situ
265
+ */
266
+ exports.findComponentDefinitionInForm = findComponentDefinitionInForm;
267
+ const deleteCorrespondingMetaInfo = (component, collectionDataObject, formData) => {
268
+ const {
269
+ meta: {
270
+ documents
271
+ }
272
+ } = formData;
273
+ const fileDataBeingDeleted = collectionDataObject[component.fieldId];
274
+ if (!documents || !fileDataBeingDeleted) return;
275
+ const fileDataAsArray = Array.isArray(fileDataBeingDeleted) ? fileDataBeingDeleted : [fileDataBeingDeleted];
276
+ // Iterate backwards to avoid index shifting when removing elements
277
+ for (let i = fileDataAsArray.length - 1; i >= 0; i -= 1) {
278
+ const matchIndex = documents.findIndex(metaDocument => metaDocument.url === fileDataAsArray[i].url);
279
+ if (matchIndex !== -1) {
280
+ documents.splice(matchIndex, 1);
281
+ }
282
+ }
283
+ };
284
+
285
+ /**
286
+ * After the payload has been cleansed of individual data items, empty arrays and objects may remain.
287
+ * Removing an array (when the payload for a collection) may leave a redundant activeId field. The active Id
288
+ * fields are not part of the payload data, but added by the react renderer to track which collection object
289
+ * is currently being worked on, so can be removed.
290
+ *
291
+ * To tidy this all up, recursively empty arrays and their associated "ActiveId" fields from a nested payload.
292
+ *
293
+ * This function traverses a given payload, which can be an array or an object, and performs the following operations:
294
+ * 1. If an empty array is found inside an array, it is removed using `splice`.
295
+ * 2. If an empty array is found inside an object:
296
+ * - It is removed from the object.
297
+ * - If there is a corresponding `<key>ActiveId` field, that field is also removed.
298
+ * 3. The function operates recursively to handle deeply nested structures.
299
+ *
300
+ * @param {any} payload - The input data structure, which can be an array or an object.
301
+ *
302
+ */
303
+ exports.deleteCorrespondingMetaInfo = deleteCorrespondingMetaInfo;
304
+ const removeEmptyArraysAndUnusedCollectionIDs = payload => {
305
+ if (Array.isArray(payload)) {
306
+ for (let i = payload.length - 1; i >= 0; i -= 1) {
307
+ if (Array.isArray(payload[i]) && payload[i].length === 0) {
308
+ payload.splice(i, 1);
309
+ } else {
310
+ removeEmptyArraysAndUnusedCollectionIDs(payload[i]); // Recurse for nested structures
311
+ }
312
+ // When unwinding out of the recursion, we may have emptied an object which is the remaining element of an
313
+ // array, in which case remove the element
314
+ if (typeof payload[i] === 'object' && Object.keys(payload[i]).length === 0) {
315
+ payload.splice(i, 1);
316
+ }
317
+ }
318
+ } else if (payload !== null && typeof payload === 'object') {
319
+ Object.keys(payload).forEach(key => {
320
+ if (Array.isArray(payload[key]) && payload[key].length === 0) {
321
+ // If the array being removed has an activeId associated with it, remove it
322
+ if (payload["".concat(key, "ActiveId")]) {
323
+ delete payload["".concat(key, "ActiveId")];
324
+ }
325
+ delete payload["".concat(key, "ActiveId")];
326
+ if (payload[key]) {
327
+ delete payload[key];
328
+ }
329
+ } else {
330
+ removeEmptyArraysAndUnusedCollectionIDs(payload[key]); // Recurse for nested structures
331
+ }
332
+ });
333
+ }
334
+ };
335
+
336
+ /**
337
+ * Delete a component's payload item from the overall payload.
338
+ * A component can be defined in >1 places in the form spec. To cater for this,
339
+ * if the componentsToKeep counter tells us that there is > 1 uses of this component
340
+ * still unaccounted for in the form then don't delete, but reduce the count by 1.
341
+ *
342
+ * When the counter reaches 1 we know all other occurences of the component have been resolved
343
+ * so it is safe to delete.
344
+ *
345
+ *
346
+ * @param {*} payload The form payload from which to delete the component data
347
+ * @param {*} path The payload path of the component
348
+ * @param {*} component The component whose data we should attempt to delete
349
+ * @param {*} componentsToKeep A list of all components with a count of their number of uses in the form
350
+ */
351
+ exports.removeEmptyArraysAndUnusedCollectionIDs = removeEmptyArraysAndUnusedCollectionIDs;
352
+ const deleteComponentData = (payload, path, component, componentsToKeep) => {
353
+ if (componentsToKeep[path] > 1) {
354
+ componentsToKeep[path] -= 1;
355
+ } else {
356
+ deleteNodeAndOptions(payload, path, component);
357
+ }
358
+ };
359
+
360
+ /**
361
+ *
362
+ * Takes a single page collection payload object and removes the payload items specified in the componentsToPrune list
363
+ * as long as they don't appear in the componentsToKeep list.
364
+ * Additionally, if the component type is multifile, remove the corresponding data entries that will have been created
365
+ * in the meta section of the payload by the form renderer.
366
+ *
367
+ * @param {Set} pathsToKeep paths that we cannot delete from the collectionDataObject
368
+ * @param {Map} componentsToPrune paths that we should delete, as long as they are not in the pathsToKeep
369
+ * @param {Object} collectionDataObject the payload from which to delete the paths
370
+ * @param {Object} formData The form data, whose meta section may include corresponding documents entries for multifile entries
371
+ */
372
+ exports.deleteComponentData = deleteComponentData;
373
+ const pruneCollectionEntry = (pathsToKeep, componentsToPrune, collectionDataObject, formData) => {
374
+ componentsToPrune.forEach(component => {
375
+ if (!pathsToKeep.has(component.fieldId)) {
376
+ if (component.type === "multifile") {
377
+ deleteCorrespondingMetaInfo(component, collectionDataObject, formData);
378
+ }
379
+ deleteNodeAndOptions(collectionDataObject, component.fieldId, component);
380
+ }
381
+ });
382
+ };
383
+
384
+ /*
385
+ * Converts an object to an array if it isn't already, for use when combining show when rules.
386
+ */
387
+ exports.pruneCollectionEntry = pruneCollectionEntry;
388
+ const toArray = value => Array.isArray(value) ? value : [value];
389
+ exports.toArray = toArray;