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

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 (36) 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-page-nested-radio-component.json +200 -0
  9. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/forms/form-page-same-component-reused-one-shown.json +74 -0
  10. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/cop-airpax-carrier.json +407 -0
  11. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/data-page-nested-radio-component.json +45 -0
  12. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/data-page-same-component-reused-one-shown.json +8 -0
  13. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/input/reassign-to-rcc.json +72 -0
  14. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/data-page-nested-radio-component-removed.json +45 -0
  15. package/dist/components/FormRenderer/clear-uncompleted-routes/test-data/output/data-page-same-component-reused-one-shown-removed.json +7 -0
  16. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutes.js +248 -171
  17. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutes.test.js +43 -7
  18. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutesUtils.js +139 -40
  19. package/dist/components/FormRenderer/helpers/clearOutUncompletedRoutesUtils.test.js +64 -7
  20. package/dist/components/FormRenderer/onCYAAction.js +0 -2
  21. package/dist/components/FormRenderer/onCYAAction.test.js +5 -0
  22. package/dist/components/FormRenderer/onPageAction.js +0 -1
  23. package/dist/hooks/useGetRequest.js +15 -15
  24. package/dist/hooks/useRefData.js +3 -2
  25. package/dist/utils/Component/getComponentTests/getComponent.multifile.test.js +2 -1
  26. package/dist/utils/Component/getDefaultValueFromConfig.js +2 -1
  27. package/dist/utils/Condition/meetsCondition.js +26 -12
  28. package/dist/utils/Condition/meetsCondition.test.js +21 -0
  29. package/dist/utils/Data/getAutocompleteSource.js +68 -51
  30. package/dist/utils/Data/getAutocompleteSource.test.js +31 -18
  31. package/dist/utils/Operate/doesContainValue.js +34 -0
  32. package/dist/utils/Operate/doesContainValue.test.js +75 -0
  33. package/dist/utils/Operate/runPageOperations.js +2 -0
  34. package/dist/utils/Validate/validateOnPageLoad.js +23 -0
  35. package/dist/utils/Validate/validateOnPageLoad.test.js +88 -0
  36. package/package.json +5 -4
@@ -7,6 +7,7 @@ exports.default = void 0;
7
7
  var _mergeCollectionPages = _interopRequireDefault(require("../../../utils/CollectionPage/mergeCollectionPages"));
8
8
  var Utils = _interopRequireWildcard(require("./clearOutUncompletedRoutesUtils"));
9
9
  var _optionIsSelected = _interopRequireDefault(require("../../../utils/Component/optionIsSelected"));
10
+ var _getSourceData = _interopRequireDefault(require("../../../utils/Data/getSourceData"));
10
11
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
11
12
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
12
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -15,173 +16,80 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
15
16
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
16
17
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
17
18
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* eslint-disable no-param-reassign */
18
- const addValue = (key, value, multiMap) => {
19
- if (!multiMap.has(key)) {
20
- multiMap.set(key, []);
21
- }
22
- multiMap.get(key).push(value);
23
- };
24
- const pruneSingleComponent = (formData, path, component, componentsToKeep) => {
25
- // If there is more than one entry for this component, then it is being used elsewhere in a visible page. Don't prune, but reduce the count by 1
26
- if (componentsToKeep[path] > 1) {
27
- componentsToKeep[path] -= 1;
28
- } else {
29
- var _component$data;
30
- Utils.deleteNodeByPath(formData, path);
31
- // 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.
32
- if (component !== null && component !== void 0 && (_component$data = component.data) !== null && _component$data !== void 0 && _component$data.options) {
33
- var _component$data2;
34
- 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 => {
35
- var _option$nested;
36
- (_option$nested = option.nested) === null || _option$nested === void 0 || _option$nested.forEach(nested => {
37
- Utils.deleteNodeByPath(formData, Utils.getNestedQuestionPath(path, nested.fieldId));
38
- });
39
- });
40
- }
41
- }
42
- };
43
- const toArray = value => Array.isArray(value) ? value : [value];
44
- const recursivelyMapFieldsAndDeleteHiddenNested = (page, component, path, allComponents, componentsToKeep, formData) => {
45
- var _component$data3;
46
- // Many component, such as warnings, html and details do not have data so we can ignore them
47
- if (!component.fieldId) return;
48
- path = path ? "".concat(path, ".").concat(component.fieldId) : component.fieldId;
49
-
50
- // Components can be referenced from more than one page. We need to keep track of the count of each component path, to avoid pruning paths that exist elsewhere
51
- componentsToKeep[path] = (componentsToKeep[path] || 0) + 1;
52
-
53
- // add the fully qualified path to the component, which will be required when resolving the dependencies later
54
- component.path = path;
19
+ /**
20
+ *
21
+ * This function recursively reads in the non-collection pages and components and builds lists from it:
22
+ *
23
+ * 1: allComponents. This is a map of all components in the form, keyed by the fully qualified component path.
24
+ * If the component belongs to a page (or other parent entity) that has its own show_when rule, then this rule
25
+ * is combined with the show_when rule of the component, so the decision as to whether the component's data should
26
+ * exist can be made just by processing the component.
27
+ * The main purpose of this map is to allow us to build a dependency graph in the function createComponentDependenciesGraph
28
+ *
29
+ * 2: componentsToKeep: This is an object containing a count of how many times each component is defined. This is to support the
30
+ * fact that a component can be used more than once in a form (but with mutually exclusive show_when rules so only used
31
+ * once with the same path).
32
+ *
33
+ * @param {Array} condensedPages All pages in the form, with the collection pages collated into a single object per collection
34
+ * @param {Object} formData The form payload
35
+ * @param {Map} componentByIdMap Map of all components, to enable performant lookup by id
36
+ * @param {Map} componentByFieldIdMap Map of all components, to enable performant lookup by fieldId
37
+ * @param {Object}
38
+ * @return {Map, Object, Map} allComponents All components in form (including nested), keyed by path:
39
+ * componentsToKeep A count of how many times each component is used, to prevent us deleting components defined > 1 times
40
+ * allCollections A map of collection objects, which are a grouping of the pages that make up a single collection
41
+ */
42
+ const createComponentMapsFromForm = (condensedPages, componentByIdMap, componentByFieldIdMap, formData) => {
43
+ const allComponents = new Map();
44
+ const componentsToKeep = {};
45
+ const allCollections = new Map();
55
46
 
56
- // If the parent page has a rule, combine it with any component rule
57
- if (page.show_when) {
58
- component.show_when = component.show_when ? [].concat(toArray(page.show_when), toArray(component.show_when)) : page.show_when;
59
- }
47
+ /*
48
+ * Inner function to support the recursion required to traverse through the nested structures of the form
49
+ */
50
+ const recursivelyMapFieldsAndDeleteHiddenNested = (page, component, path) => {
51
+ var _component$data;
52
+ if (!component.fieldId) return; // Many component, such as warnings, html and details do not have data so we can ignore them
53
+ path = path ? "".concat(path, ".").concat(component.fieldId) : component.fieldId; // Build up the path to reflect nested components
54
+ componentsToKeep[path] = (componentsToKeep[path] || 0) + 1; // We need to keep track of the count of each component path, to avoid pruning paths that exist elsewhere
55
+ component.path = path; // Add the fully qualified path to the component, which will be required when resolving the dependencies later
60
56
 
61
- // this is the master repository of all components. Can be more than one component per path, so keep a map of <path, array>
62
- addValue(path, component, allComponents);
63
- if (component.components) {
64
- component.components.forEach(c => {
65
- recursivelyMapFieldsAndDeleteHiddenNested(page, c, path, allComponents, componentsToKeep, formData);
66
- });
67
- } else if (component !== null && component !== void 0 && (_component$data3 = component.data) !== null && _component$data3 !== void 0 && _component$data3.options) {
68
- var _component$data4;
69
- /**
70
- * Perform processing for any options, and any options containing nested questions.
71
- * - set hidden nested questions when the parent option is not selected
72
- * - any nested questions must be treated as components which can be shown/hidden
73
- */
74
- component === null || component === void 0 || (_component$data4 = component.data) === null || _component$data4 === void 0 || (_component$data4 = _component$data4.options) === null || _component$data4 === void 0 || _component$data4.forEach(option => {
75
- if (option.nested) {
76
- if (!(0, _optionIsSelected.default)(formData[component.id], option)) {
77
- option.nested.forEach(nestedComponent => {
78
- // It is safe to delete this now, as we know this data item can not be required as the associated option is not selected
79
- Utils.deleteNodeByPath(formData, Utils.getNestedQuestionPath(path, nestedComponent.fieldId));
80
- });
81
- } else {
82
- // If the option is selected, then add any nested components to the allComponents list. If the nested block has a show_when, pass it down to the child questions
83
- const blockShowWhen = option.show_when;
84
- option.nested.forEach(nestedComponent => {
85
- if (blockShowWhen) nestedComponent.show_when = blockShowWhen;
86
- recursivelyMapFieldsAndDeleteHiddenNested(page, nestedComponent, Utils.getImmediateParent(path), allComponents, componentsToKeep, formData);
87
- });
88
- }
89
- }
90
- });
91
- }
92
- };
93
- const recursivelyMapDependencies = (component, allDependencyRelationships) => {
94
- // Non data components can be ignored - can we remove this when doing the allcomponents thing?
95
- if (!component.fieldId) return;
96
- const componentDependencies = Utils.getDependencies(component);
97
- if (componentDependencies) {
98
- // This path may already exist (if a component is defined twice in the form) so build up the dependencies
99
- if (allDependencyRelationships.has(component.path)) {
100
- const existingSet = allDependencyRelationships.get(component.path);
101
- componentDependencies.forEach(dep => existingSet.add(dep));
102
- } else {
103
- allDependencyRelationships.set(component.path, new Set(componentDependencies));
57
+ // If the parent page has a rule, combine it with any component rule as we will be resolving dependencies for components only
58
+ if (page.show_when) {
59
+ component.show_when = component.show_when ? [].concat(Utils.toArray(page.show_when), Utils.toArray(component.show_when)) : page.show_when;
104
60
  }
105
- }
106
- if (component.components) {
107
- component.components.forEach(c => {
108
- recursivelyMapDependencies(c, allDependencyRelationships);
109
- });
110
- }
111
- };
112
- const recursivelyResolveDependencies = (visited, dependentEntity, dependencyRelationships, form, formData, allComponents, componentsToKeep, allCollections) => {
113
- if (visited.has(dependentEntity)) return;
114
- visited.add(dependentEntity);
115
- const neighbours = dependencyRelationships.get(dependentEntity.path);
116
- neighbours === null || neighbours === void 0 || neighbours.forEach(neighbourPath => {
117
- const neighbourArray = Utils.getDependencyObjectFromPath(neighbourPath, allComponents);
118
- if (neighbourArray) {
119
- neighbourArray.forEach(neighbour => {
120
- if (!visited.has(neighbour)) {
121
- recursivelyResolveDependencies(visited, neighbour, dependencyRelationships, form, formData, allComponents, componentsToKeep, allCollections);
122
- }
61
+ Utils.addValue(path, component, allComponents); // There can be more than one component per path, so keep a map of <path, array>
62
+ // recurse if there is nesting
63
+ if (component.components) {
64
+ component.components.forEach(c => {
65
+ recursivelyMapFieldsAndDeleteHiddenNested(page, c, path, allComponents, componentsToKeep, formData);
123
66
  });
124
- } else {
125
- // Refactor out helper methods
126
- const arrayNamePattern = /([a-zA-Z_$][\w$]*)\[\d+\]/;
127
- const match = neighbourPath.match(arrayNamePattern);
128
- if (match) {
129
- const collectionName = match[1];
130
- const collection = allCollections.get(collectionName);
131
- if (collection) {
132
- var _collection$childPage;
133
- (_collection$childPage = collection.childPages) === null || _collection$childPage === void 0 || _collection$childPage.forEach(childPage => {
134
- if (childPage.show_when) {
135
- const message = "It is not possible to reliably clean hidden data when a component is dependent on a \n collection's data, and that collection is itself dependent on data elsewhere in the form.\n The dependency path is ".concat(neighbourPath, ", and the chained show_when is ").concat(JSON.stringify(childPage.show_when), ".\n The form will need to be changed to break this chained dependency");
136
- throw new Error(message);
137
- }
138
- ;
139
- });
67
+ } else if (component !== null && component !== void 0 && (_component$data = component.data) !== null && _component$data !== void 0 && _component$data.options) {
68
+ var _component$data2;
69
+ 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 => {
70
+ if (option.nested) {
71
+ // in container need to use the JPath for the data since the field that the component is looking at is nested in the container.
72
+ if (!(0, _optionIsSelected.default)((0, _getSourceData.default)(formData, path), option)) {
73
+ option.nested.forEach(nestedComponent => {
74
+ // delete hidden nested question payload items when the parent option is not selected
75
+ Utils.deleteNodeByPath(formData, Utils.getNestedQuestionPath(path, nestedComponent.fieldId));
76
+ });
77
+ } else {
78
+ // If the option is selected, then add any nested components to the allComponents list. If the nested block has a show_when, pass it down to the child questions
79
+ const blockShowWhen = option.show_when;
80
+ option.nested.forEach(nestedComponent => {
81
+ if (blockShowWhen) nestedComponent.show_when = blockShowWhen;
82
+ recursivelyMapFieldsAndDeleteHiddenNested(page, nestedComponent, Utils.getImmediateParent(path), allComponents, componentsToKeep, formData);
83
+ });
84
+ }
140
85
  }
141
- }
142
- }
143
- });
144
- // Whilst unravelling the recursion, we can start pruning from the leaf upwards
145
- if (!Utils.isShowEntity(dependentEntity, formData)) {
146
- pruneSingleComponent(formData, dependentEntity.path, dependentEntity, componentsToKeep);
147
- }
148
- };
149
- const pruneCollectionEntry = (pathsToKeep, componentsToPrune, collectionDataObject, formData) => {
150
- componentsToPrune.forEach(component => {
151
- if (!pathsToKeep.has(component.fieldId)) {
152
- var _component$data5;
153
- if (component.type === "multifile") {
154
- Utils.deleteCorrespondingMetaInfo(component, collectionDataObject, formData);
155
- }
156
- Utils.deleteNodeByPath(collectionDataObject, component.fieldId);
157
-
158
- // REFACTOR
159
- // 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.
160
- if (component !== null && component !== void 0 && (_component$data5 = component.data) !== null && _component$data5 !== void 0 && _component$data5.options) {
161
- var _component$data6;
162
- component === null || component === void 0 || (_component$data6 = component.data) === null || _component$data6 === void 0 || (_component$data6 = _component$data6.options) === null || _component$data6 === void 0 || _component$data6.forEach(option => {
163
- var _option$nested2;
164
- (_option$nested2 = option.nested) === null || _option$nested2 === void 0 || _option$nested2.forEach(nested => {
165
- Utils.deleteNodeByPath(collectionDataObject, Utils.getNestedQuestionPath(component.fieldId, nested.fieldId));
166
- });
167
- });
168
- }
86
+ });
169
87
  }
170
- });
171
- };
172
- const clearOutUncompletedRoutes = (form, formData) => {
173
- // Load components into maps keyed on id and field for subsequent efficient access
174
- const componentByIdMap = new Map(form.components.map(c => [c.id, c]));
175
- const componentByFieldIdMap = new Map(form.components.map(c => [c.fieldId, c]));
88
+ };
176
89
 
177
- // Recurse through the whole form, building a map of all fields and their owning component id.
178
- // This is required to build the component->dependency graph in the next step, as the dependencies reference fields, not components.
179
-
180
- const condensedPages = (0, _mergeCollectionPages.default)(form.pages);
181
- const allComponents = new Map();
182
- const componentsToKeep = {};
183
- const allCollections = new Map();
90
+ // Entry point for the recursive traverse of the form specification.
184
91
  condensedPages === null || condensedPages === void 0 || condensedPages.forEach(page => {
92
+ // Make the distinction between collections (which will be processed separately) and non-collections, which will be processed here.
185
93
  if (page.childPages) {
186
94
  allCollections.set(page.collection.name, page);
187
95
  } else {
@@ -192,25 +100,126 @@ const clearOutUncompletedRoutes = (form, formData) => {
192
100
  });
193
101
  }
194
102
  });
103
+ return {
104
+ allComponents,
105
+ componentsToKeep,
106
+ allCollections
107
+ };
108
+ };
109
+ /**
110
+ *
111
+ * This function iterates through the allComponents list. For each component, it will derive all payload paths
112
+ * that this component is dependent on. The same component might be defined more than once in the form, so add the dependencies
113
+ * for all the component definitions.
114
+ *
115
+ * @param {Map} allDependencyRelationships A map of each component with dependencies. The key is the fully qualified path and the value is a Set of all the paths that this component is dependent on.
116
+ * @param {Map} allComponents All components in form (including nested), keyed by path. Used to build the allDependencyRelationships
117
+ * @return {Map }allDependencyRelationships graph of the relationships between all entities
118
+ */
119
+ const createComponentDependenciesGraph = allComponents => {
195
120
  const allDependencyRelationships = new Map();
196
121
  allComponents === null || allComponents === void 0 || allComponents.forEach(componentArray => {
197
122
  componentArray === null || componentArray === void 0 || componentArray.forEach(component => {
198
- recursivelyMapDependencies(component, allDependencyRelationships);
123
+ if (!component.fieldId) return;
124
+ const componentDependencies = Utils.getDependencies(component);
125
+ if (componentDependencies) {
126
+ if (allDependencyRelationships.has(component.path)) {
127
+ const existingSet = allDependencyRelationships.get(component.path);
128
+ componentDependencies.forEach(dep => existingSet.add(dep));
129
+ } else {
130
+ allDependencyRelationships.set(component.path, new Set(componentDependencies));
131
+ }
132
+ }
199
133
  });
200
134
  });
135
+ return allDependencyRelationships;
136
+ };
201
137
 
202
- // Visit all components with 'show_whens' and resolve the dependency. Only delete the data at the leaf element, or when un-winding the recursion
203
- // to deal with the edge case that an element is dependent on a data item that is hidden by a different constraint
138
+ /**
139
+ *
140
+ * This function loops through each entry in the allDependencyRelationships map. Each entry will contain a Set of all
141
+ * paths that the key path is dependent on. Recursively delve into each dependency path and repeat the process until we
142
+ * have reached an entry that has no dependencies. At this point we can safely resolve the dependency using its show_when rule,
143
+ * and unwind the recursion.
144
+ *
145
+ * Because all the components are inter-connected, this could result in resolving the same entry repeatedly, so keep a 'visited'
146
+ * array to prevent this.
147
+ *
148
+ * @param {Object} formData The form payload
149
+ * @param {Map} allComponents All components in form (including nested), keyed by path
150
+ * @param {Object} componentsToKeep A count of how many times each component is used, to prevent us deleting components defined > 1 times
151
+ * @param {Map} allDependencyRelationships A map of each component with dependencies. The key is the fully qualified path and the value is a Set of all the paths that this component is dependent on.
152
+ * @param {Map} allCollections A map of collection objects, which are a grouping of the pages that make up a single collection
153
+ * @param {Object} form The form specification
154
+ */
155
+ const resolveComponentDependenciesGraph = (allDependencyRelationships, allComponents, componentsToKeep, allCollections, form, formData) => {
156
+ /*
157
+ * Inner function to support the recursion required to traverse through the nested nature of the allDependencyRelationships
158
+ */
159
+ const recursivelyResolveDependencies = (visited, dependentComponent, dependencyRelationships) => {
160
+ if (visited.has(dependentComponent)) return;
161
+ visited.add(dependentComponent);
162
+ const dependencies = dependencyRelationships.get(dependentComponent.path);
163
+ dependencies === null || dependencies === void 0 || dependencies.forEach(dependencyPath => {
164
+ const dependencyComponents = Utils.getDependencyObjectFromPath(dependencyPath, allComponents);
165
+ if (dependencyComponents) {
166
+ dependencyComponents.forEach(dependency => {
167
+ if (!visited.has(dependency)) {
168
+ recursivelyResolveDependencies(visited, dependency, dependencyRelationships);
169
+ }
170
+ });
171
+ } else {
172
+ // We are here if the dependency path was not found in the list of allComponents. This is valid and can happen when
173
+ // a component is dependent on a field that was generated by cop-ui, e.g. jobHolderStaffDetails.linemanagerEmail.
174
+ // However, there is an edge case (described in the message below) that we need to validate for.
175
+ const arrayNamePattern = /([a-zA-Z_$][\w$]*)\[\d+\]/;
176
+ const match = dependencyPath.match(arrayNamePattern);
177
+ if (match) {
178
+ const collectionName = match[1];
179
+ const collection = allCollections.get(collectionName);
180
+ if (collection) {
181
+ var _collection$childPage;
182
+ (_collection$childPage = collection.childPages) === null || _collection$childPage === void 0 || _collection$childPage.forEach(childPage => {
183
+ if (childPage.show_when) {
184
+ const message = "It is not possible to reliably clean hidden data when a component is dependent on a \n collection's data, and that collection is itself dependent on data elsewhere in the form.\n The dependency path is ".concat(dependencyPath, ", and the chained show_when is ").concat(JSON.stringify(childPage.show_when), ".\n The form will need to be changed to break this chained dependency");
185
+ throw new Error(message);
186
+ }
187
+ ;
188
+ });
189
+ }
190
+ }
191
+ }
192
+ });
193
+ // Whilst unravelling the recursion, we can start deleting from the leaf upwards
194
+ if (!Utils.isShowEntity(dependentComponent, formData)) {
195
+ Utils.deleteComponentData(formData, dependentComponent.path, dependentComponent, componentsToKeep);
196
+ }
197
+ };
204
198
  const visited = new Set();
205
- allDependencyRelationships === null || allDependencyRelationships === void 0 || allDependencyRelationships.forEach((neighbours, dependentEntityPath) => {
199
+ allDependencyRelationships === null || allDependencyRelationships === void 0 || allDependencyRelationships.forEach((dependencies, dependentComponentPath) => {
206
200
  // Each path that a component is dependent on may have > 1 possible dependent components, if a field is used twice
207
- const dependentEntities = allComponents.get(dependentEntityPath);
208
- dependentEntities === null || dependentEntities === void 0 || dependentEntities.forEach(dependentEntity => {
209
- if (!visited.has(dependentEntity)) {
210
- recursivelyResolveDependencies(visited, dependentEntity, allDependencyRelationships, form, formData, allComponents, componentsToKeep, allCollections);
201
+ const dependentComponents = allComponents.get(dependentComponentPath);
202
+ dependentComponents === null || dependentComponents === void 0 || dependentComponents.forEach(dependentComponent => {
203
+ if (!visited.has(dependentComponent)) {
204
+ recursivelyResolveDependencies(visited, dependentComponent, allDependencyRelationships, form, formData, allComponents, componentsToKeep, allCollections);
211
205
  }
212
206
  });
213
207
  });
208
+ };
209
+
210
+ /**
211
+ * For each collection, iterate through the payload array's objects, and apply the collections show_when
212
+ * rules for each payload object.
213
+ *
214
+ * Some of the rules may be dependent on payload items outside the collection. These will already have been cleansed
215
+ * so will be in a reliable state.
216
+ *
217
+ * @param {Map} allCollections A map of collection objects, which are a grouping of the pages that make up a single collection
218
+ * @param {Object} formData The form payload
219
+ * @param {Map} componentByIdMap Map of all components, to enable performant lookup by id
220
+ * @param {Map} componentByFieldIdMap Map of all components, to enable performant lookup by fieldId
221
+ */
222
+ const cleanseCollectionData = (allCollections, formData, componentByIdMap, componentByFieldIdMap) => {
214
223
  allCollections === null || allCollections === void 0 || allCollections.forEach((collection, collectionName) => {
215
224
  const collectionDataArray = formData[collectionName];
216
225
  collectionDataArray === null || collectionDataArray === void 0 || collectionDataArray.forEach(collectionDataEntry => {
@@ -225,23 +234,91 @@ const clearOutUncompletedRoutes = (form, formData) => {
225
234
  const showPage = Utils.isShowEntity(childPage, dataForEvaluation);
226
235
  (_childPage$components = childPage.components) === null || _childPage$components === void 0 || _childPage$components.forEach(useComponentInPage => {
227
236
  const componentObj = Utils.findComponentDefinitionInForm(useComponentInPage, componentByIdMap, componentByFieldIdMap);
228
-
229
237
  // Non-data components can be ignored (eg html)
230
238
  if (!componentObj.fieldId) return;
231
239
  const showComponentOnPage = Utils.isShowEntity(useComponentInPage, dataForEvaluation);
232
240
  if (showPage && showComponentOnPage && Utils.isShowEntity(componentObj, dataForEvaluation)) {
233
- // There may be 2 components with the same path (eg quantity in EAB2), so don't delete hidden components if they are required elsewhere
241
+ // There may be >1 components with the same path (eg quantity in EAB2), so don't delete hidden components if they are required elsewhere
234
242
  pathsToKeep.add(componentObj.fieldId);
235
243
  } else {
236
244
  componentsToPrune.set(componentObj.id, componentObj);
237
245
  }
238
246
  });
239
247
  });
240
- pruneCollectionEntry(pathsToKeep, componentsToPrune, collectionDataEntry, formData);
248
+ Utils.pruneCollectionEntry(pathsToKeep, componentsToPrune, collectionDataEntry, formData);
241
249
  });
242
250
  //
243
- if (collectionDataArray) Utils.removeObjectWithSingleIdFieldInPlace(collectionDataArray);
251
+ if (collectionDataArray) Utils.removeObjectWithOnlySingleIdField(collectionDataArray);
244
252
  });
253
+ };
254
+
255
+ /**
256
+ *
257
+ * The purpose of this function is to apply the show_when rules defined in the form specification to the
258
+ * form payload (formData), and remove any data that should not be shown according to those rules.
259
+ * (the reason this data exists in the payload is that users can answer a question in such a way that
260
+ * other questions are 'shown' to them, which are then complete. The user can then go back and change the original
261
+ * question to then 'hide' those questions, but the answers they gave persist in the payload. The must be cleansed
262
+ * before being submitted.
263
+ *
264
+ * There are 2 types of payload data that have to be treated differently by this function for it to work.
265
+ *
266
+ * 1. Data from non-repeating pages, containing components.
267
+ *
268
+ * Each data item captured will be written to the payload as a field with the name of the fieldId of the payload.
269
+ * In the case of components that are nested within 'container' components, the payload will reflect the nesting.
270
+ * There is no limit to the level of nesting in a form.
271
+ *
272
+ * To cleanse the payload for this type of data, we do the following (high level description, see method comments for detail):
273
+ *
274
+ * - build a map of all components, keyed by their payload path (required for the next step)
275
+ * - for all of these components that have dependencies (show_when rules), create a graph datastructure to show all components
276
+ * on which a component is dependent, and then all components that those components may be dependent on. There is no limit to
277
+ * the depth of these chained dependencies.
278
+ * - the reason for building a graph of these dependency chains is so we know the sequence in which we must resolve the dependencies.
279
+ * For any given chain of dependencies it is essential that we resolve the dependencies starting at the end of the chains, and work our way
280
+ * back up the chain. If not, we could be resolving a dependency based on a payload item that itself will later be deleted.
281
+ * Therefore, the final step is to recursively traverse the dependency graph, resolving the dependency rule for the components in
282
+ * the chain from the leaf back up to the parent node.
283
+ *
284
+ * 2. Page collection data
285
+ *
286
+ * A page collection is a set of one or more pages which (as a group) can be filled in as many times is required
287
+ * by the user. For example, in EAB, you can add as many item-seal-details to an EAB form as are required. An item-seal-detail
288
+ * is made up of several different pages, all grouped. The data saved will be an array representing the whole collection, with
289
+ * each object in the array representing a single collection entry (a single item-seal-detail in this example).
290
+ *
291
+ * Cleanse collection data has to be driven from the payload rather than the form, as there are repeated objects in an array
292
+ * representing the data. Each one has to be treated independently as the data to be cleansed can be different in each one.
293
+ *
294
+ * - For each collection type, iterate through each payload object and treat it like an independent payload..
295
+ * - For each collection payload object, iterate through the collection's pages and components, and apply all the show_when rules
296
+ * found at both page and component level to the payload.
297
+ * - Repeat for each payload object
298
+ * - Repeat for all collections
299
+ *
300
+ * @param {*} form
301
+ * @param {*} formData
302
+ * @returns {*} cleansed form data
303
+ */
304
+ const clearOutUncompletedRoutes = (form, formData) => {
305
+ // Load components into maps keyed on id and field for subsequent performant access
306
+ const componentByIdMap = new Map(form.components.map(c => [c.id, c]));
307
+ const componentByFieldIdMap = new Map(form.components.map(c => [c.fieldId, c]));
308
+
309
+ // Group all pages relating to a page-collection into a single object to aid collection
310
+ // processing, and to allow us to differentiate between a collection and non-collection page.
311
+ const condensedPages = (0, _mergeCollectionPages.default)(form.pages);
312
+ const {
313
+ allComponents,
314
+ componentsToKeep,
315
+ allCollections
316
+ } = createComponentMapsFromForm(condensedPages, componentByIdMap, componentByFieldIdMap, formData);
317
+ const allDependencyRelationships = createComponentDependenciesGraph(allComponents);
318
+ resolveComponentDependenciesGraph(allDependencyRelationships, allComponents, componentsToKeep, allCollections, form, formData);
319
+ cleanseCollectionData(allCollections, formData, componentByIdMap, componentByFieldIdMap);
320
+
321
+ // The cleansing above may have left empty arrays and collection IDs. Tidy these up.
245
322
  Utils.removeEmptyArraysAndUnusedCollectionIDs(formData);
246
323
  return formData;
247
324
  };
@@ -40,6 +40,9 @@ var _dataHiddenPageRemoved = _interopRequireDefault(require("../clear-uncomplete
40
40
  var _formHiddenPageSameComponentReused = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/form-hidden-page-same-component-reused.json"));
41
41
  var _dataHiddenPageSameComponentReused = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/data-hidden-page-same-component-reused.json"));
42
42
  var _dataHiddenPageSameComponentReusedRemoved = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/data-hidden-page-same-component-reused-removed.json"));
43
+ var _formPageSameComponentReusedOneShown = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/form-page-same-component-reused-one-shown.json"));
44
+ var _dataPageSameComponentReusedOneShown = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/data-page-same-component-reused-one-shown.json"));
45
+ var _dataPageSameComponentReusedOneShownRemoved = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/data-page-same-component-reused-one-shown-removed.json"));
43
46
  var _formHiddenPageComponentUsedElsewhere = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/form-hidden-page-component-used-elsewhere.json"));
44
47
  var _dataHiddenPageComponentUsedElsewhere = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/data-hidden-page-component-used-elsewhere.json"));
45
48
  var _dataHiddenPageComponentUsedElsewhereRemoved = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/data-hidden-page-component-used-elsewhere-removed.json"));
@@ -82,11 +85,17 @@ var _copMandecRemoveCriminalityBefore = _interopRequireDefault(require("../clear
82
85
  var _copMandecRemoveCriminalityAfter = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/cop-mandec-remove-criminality-after.json"));
83
86
  var _copMandecRemoveUnspentConvictionsBefore = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/cop-mandec-remove-unspent-convictions-before.json"));
84
87
  var _copMandecRemoveUnspentConvictionsAfter = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/cop-mandec-remove-unspent-convictions-after.json"));
88
+ var _copReassignTaskToRcc = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/cop-reassign-task-to-rcc.json"));
89
+ var _reassignToRcc = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/reassign-to-rcc.json"));
85
90
  var _formCopAirpax = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/form-cop-airpax.json"));
86
91
  var _copAirpaxRemovePhotosBefore = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/cop-airpax-remove-photos-before.json"));
87
92
  var _copAirpaxRemovePhotosAfter = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/cop-airpax-remove-photos-after.json"));
88
93
  var _copAirpaxChangeWhatHappenedBefore = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/cop-airpax-change-what-happened-before.json"));
89
94
  var _copAirpaxChangeWhatHappenedAfter = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/cop-airpax-change-what-happened-after.json"));
95
+ var _copAirpaxCarrier = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/cop-airpax-carrier.json"));
96
+ var _formPageNestedRadioComponent = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/forms/form-page-nested-radio-component.json"));
97
+ var _dataPageNestedRadioComponent = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/input/data-page-nested-radio-component.json"));
98
+ var _dataPageNestedRadioComponentRemoved = _interopRequireDefault(require("../clear-uncompleted-routes/test-data/output/data-page-nested-radio-component-removed.json"));
90
99
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
91
100
  // Hidden component
92
101
 
@@ -114,6 +123,8 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
114
123
 
115
124
  // Hidden page containing same component reused
116
125
 
126
+ // should show component when reused but only one show_when true
127
+
117
128
  // Hidden page with component used elsewhere
118
129
 
119
130
  // Hidden component with show_when rule referring to a collection
@@ -144,8 +155,16 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
144
155
 
145
156
  // Mandec Unspent convictions
146
157
 
158
+ // TMS reassign-task-to-rcc
159
+
147
160
  // Airpax photos
148
161
 
162
+ // Who decided
163
+
164
+ // Carrier not removed
165
+
166
+ // Form with no componets but componet listed in page.
167
+
149
168
  describe('FormRenderer', () => {
150
169
  describe('helpers', () => {
151
170
  describe('clearOutUncompletedRoutes', () => {
@@ -227,13 +246,12 @@ describe('FormRenderer', () => {
227
246
  const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
228
247
  expect(result).toEqual(_dataHiddenPageSameComponentReusedRemoved.default);
229
248
  });
230
-
231
- // TEST FOR 3 components with the same name, a page show_when, 1 of which is shown.
232
-
233
- // Check the visited thing is working.
234
-
235
- // Refactor and comment the toArray method.
236
-
249
+ it('should show component when reused but only one show_when true.', () => {
250
+ const submissionData = JSON.parse(JSON.stringify(_dataPageSameComponentReusedOneShown.default));
251
+ const form = JSON.parse(JSON.stringify(_formPageSameComponentReusedOneShown.default));
252
+ const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
253
+ expect(result).toEqual(_dataPageSameComponentReusedOneShownRemoved.default);
254
+ });
237
255
  it('should remove a hidden collection on a page.', () => {
238
256
  const submissionData = JSON.parse(JSON.stringify(_dataHiddenPageCollection.default));
239
257
  const form = JSON.parse(JSON.stringify(_formHiddenPageCollection.default));
@@ -328,18 +346,36 @@ describe('FormRenderer', () => {
328
346
  const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
329
347
  expect(result).toEqual(_copMandecRemoveUnspentConvictionsAfter.default);
330
348
  });
349
+ it('Reassign to RCC form, preserved fields not cleansed.', () => {
350
+ const submissionData = JSON.parse(JSON.stringify(_reassignToRcc.default));
351
+ const form = JSON.parse(JSON.stringify(_copReassignTaskToRcc.default));
352
+ const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
353
+ expect(result).toEqual(_reassignToRcc.default);
354
+ });
331
355
  it('photos removed from airpax.', () => {
332
356
  const submissionData = JSON.parse(JSON.stringify(_copAirpaxRemovePhotosBefore.default));
333
357
  const form = JSON.parse(JSON.stringify(_formCopAirpax.default));
334
358
  const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
335
359
  expect(result).toEqual(_copAirpaxRemovePhotosAfter.default);
336
360
  });
361
+ it('carrier not removed from airpax.', () => {
362
+ const submissionData = JSON.parse(JSON.stringify(_copAirpaxCarrier.default));
363
+ const form = JSON.parse(JSON.stringify(_formCopAirpax.default));
364
+ const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
365
+ expect(result).toEqual(_copAirpaxCarrier.default);
366
+ });
337
367
  it('airpax change whatHappened to hide whoDecidedSelection.', () => {
338
368
  const submissionData = JSON.parse(JSON.stringify(_copAirpaxChangeWhatHappenedBefore.default));
339
369
  const form = JSON.parse(JSON.stringify(_formCopAirpax.default));
340
370
  const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
341
371
  expect(result).toEqual(_copAirpaxChangeWhatHappenedAfter.default);
342
372
  });
373
+ it('should handle nested option in container componet defined in the page.', () => {
374
+ const submissionData = JSON.parse(JSON.stringify(_dataPageNestedRadioComponent.default));
375
+ const form = JSON.parse(JSON.stringify(_formPageNestedRadioComponent.default));
376
+ const result = _index.default.clearOutUncompletedRoutes(form, submissionData);
377
+ expect(result).toEqual(_dataPageNestedRadioComponentRemoved.default);
378
+ });
343
379
  });
344
380
  });
345
381
  });