@ukhomeoffice/cop-react-form-renderer 6.12.1 → 6.14.0

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 (25) hide show
  1. package/README.md +3 -0
  2. package/dist/components/CollectionSummary/CollectionSummary.js +6 -2
  3. package/dist/components/CollectionSummary/RenderListView.js +16 -4
  4. package/dist/components/CollectionSummary/RenderListView.scss +4 -0
  5. package/dist/components/CollectionSummary/RenderListView.test.js +35 -2
  6. package/dist/components/CollectionSummary/SummaryCard.js +9 -15
  7. package/dist/components/CollectionSummary/SummaryCardButtons.js +76 -0
  8. package/dist/components/CollectionSummary/SummaryCardButtons.test.js +83 -0
  9. package/dist/components/FormComponent/Collection.js +5 -3
  10. package/dist/components/FormPage/FormPage.js +42 -8
  11. package/dist/components/FormPage/FormPage.test.js +61 -0
  12. package/dist/components/FormRenderer/FormRenderer.js +11 -5
  13. package/dist/utils/CheckYourAnswers/getCYARowsForCollectionPage.js +3 -0
  14. package/dist/utils/CheckYourAnswers/getCYARowsForContainer.js +3 -1
  15. package/dist/utils/CheckYourAnswers/getCYARowsForContainer.test.js +42 -0
  16. package/dist/utils/Component/getComponent.js +6 -2
  17. package/dist/utils/Component/getComponentTests/getComponent.nested.test.js +52 -0
  18. package/dist/utils/Data/getOptions.js +37 -3
  19. package/dist/utils/Data/getOptions.test.js +48 -0
  20. package/dist/utils/FormPage/getFormPage.js +7 -0
  21. package/dist/utils/FormPage/useComponent.js +11 -0
  22. package/dist/utils/FormPage/useComponent.test.js +36 -0
  23. package/dist/utils/Validate/validateEmail.js +2 -1
  24. package/dist/utils/Validate/validateEmail.test.js +3 -0
  25. package/package.json +5 -3
package/README.md CHANGED
@@ -45,6 +45,9 @@ Runs Storybook on [http://localhost:6007](http://localhost:6007), which showcase
45
45
 
46
46
  Builds Storybook for production to the `storybook-static` folder.
47
47
 
48
+ ### `npm run yalc-publish`
49
+ This will compile the code with source maps enabled and publish it to the local yalc repository in `~/.yalc` Any linked repositories will be automatically updated.
50
+
48
51
  # Using the Service
49
52
  ## Versioning
50
53
  For versioning this project uses SemVer.
@@ -246,7 +246,9 @@ var CollectionSummary = function CollectionSummary(_ref) {
246
246
  id: "".concat(config.fieldId, ".addButton"),
247
247
  action: {
248
248
  collection: config.collectionName,
249
- label: config.addButton.label || DEFAULT_ADD_BUTTON_LABEL,
249
+ label: _utils.default.interpolateString(config.addButton.label || DEFAULT_ADD_BUTTON_LABEL, {
250
+ documents: data
251
+ }),
250
252
  page: config.addButton.page,
251
253
  type: _PageAction.PageActionTypes.COLLECTION_ADD,
252
254
  classModifiers: ['secondary']
@@ -303,7 +305,9 @@ var CollectionSummary = function CollectionSummary(_ref) {
303
305
  id: "".concat(config.fieldId, ".addButton"),
304
306
  action: {
305
307
  collection: config.collectionName,
306
- label: config.addButton.label || DEFAULT_ADD_BUTTON_LABEL,
308
+ label: _utils.default.interpolateString(config.addButton.label || DEFAULT_ADD_BUTTON_LABEL, {
309
+ documents: data
310
+ }),
307
311
  page: config.addButton.page,
308
312
  type: _PageAction.PageActionTypes.COLLECTION_ADD,
309
313
  classModifiers: ['secondary'],
@@ -29,7 +29,7 @@ var DEFAULT_DELETE_BUTTON_LABEL = exports.DEFAULT_DELETE_BUTTON_LABEL = 'Delete'
29
29
  var DEFAULT_CHANGE_BUTTON_CLASS = exports.DEFAULT_CHANGE_BUTTON_CLASS = 'secondary';
30
30
  var DEFAULT_DELETE_BUTTON_CLASS = exports.DEFAULT_DELETE_BUTTON_CLASS = 'warning';
31
31
  var RenderListView = function RenderListView(_ref) {
32
- var _config$changeAction, _config$deleteAction, _masterPage$childPage;
32
+ var _config$changeAction, _config$changeAction2, _config$changeAction3, _config$changeAction4, _config$deleteAction, _masterPage$childPage;
33
33
  var id = _ref.id,
34
34
  entryData = _ref.entryData,
35
35
  config = _ref.config,
@@ -59,16 +59,28 @@ var RenderListView = function RenderListView(_ref) {
59
59
  className: classes('actions')
60
60
  }, /*#__PURE__*/_react.default.createElement("li", {
61
61
  className: classes('action')
62
- }, config.changeAction && typeof onFullEdit === 'function' && /*#__PURE__*/_react.default.createElement("button", {
62
+ }, config.changeAction && typeof onFullEdit === 'function' &&
63
+ // If passed a value for classModifiers, render a Button with those classModifiers applied -
64
+ // otherwise, revert to the default GOVUK List View styling of rendering a Link button instead:
65
+ config.changeAction.classModifiers ? /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
66
+ id: "".concat(id, ".changeButton"),
67
+ onClick: function onClick() {
68
+ return onFullEdit(config.changeAction.page, entryData.id);
69
+ },
70
+ classModifiers: config.changeAction.classModifiers,
71
+ "aria-label": _copReactComponents.Utils.interpolateString("".concat(((_config$changeAction = config.changeAction) === null || _config$changeAction === void 0 ? void 0 : _config$changeAction.label) || DEFAULT_CHANGE_BUTTON_LABEL, " ").concat(config.title || DEFAULT_TITLE), _objectSpread(_objectSpread({}, entryData), {}, {
72
+ index: entryData.index + 1
73
+ }))
74
+ }, ((_config$changeAction2 = config.changeAction) === null || _config$changeAction2 === void 0 ? void 0 : _config$changeAction2.label) || DEFAULT_CHANGE_BUTTON_LABEL) : /*#__PURE__*/_react.default.createElement("button", {
63
75
  type: "button",
64
76
  className: "govuk-link govuk-link-button",
65
77
  onClick: function onClick() {
66
78
  return onFullEdit(config.changeAction.page, entryData.id);
67
79
  },
68
- "aria-label": _copReactComponents.Utils.interpolateString("".concat(((_config$changeAction = config.changeAction) === null || _config$changeAction === void 0 ? void 0 : _config$changeAction.label) || DEFAULT_CHANGE_BUTTON_LABEL, " ").concat(config.title || DEFAULT_TITLE), _objectSpread(_objectSpread({}, entryData), {}, {
80
+ "aria-label": _copReactComponents.Utils.interpolateString("".concat(((_config$changeAction3 = config.changeAction) === null || _config$changeAction3 === void 0 ? void 0 : _config$changeAction3.label) || DEFAULT_CHANGE_BUTTON_LABEL, " ").concat(config.title || DEFAULT_TITLE), _objectSpread(_objectSpread({}, entryData), {}, {
69
81
  index: entryData.index + 1
70
82
  }))
71
- }, config.changeAction.label || DEFAULT_CHANGE_BUTTON_LABEL)), /*#__PURE__*/_react.default.createElement("li", {
83
+ }, ((_config$changeAction4 = config.changeAction) === null || _config$changeAction4 === void 0 ? void 0 : _config$changeAction4.label) || DEFAULT_CHANGE_BUTTON_LABEL)), /*#__PURE__*/_react.default.createElement("li", {
72
84
  className: classes('action')
73
85
  }, config.deleteAction && typeof onDelete === 'function' && /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
74
86
  id: "".concat(id, ".deleteButton"),
@@ -289,3 +289,7 @@ $govuk-font-family: 'Roboto', arial, sans-serif;
289
289
  background: none;
290
290
  border: none;
291
291
  }
292
+
293
+ .hods-button {
294
+ margin: 0;
295
+ }
@@ -224,7 +224,7 @@ describe('components.CollectionSummary.RenderListView', function () {
224
224
  onDeleteArgs = [];
225
225
  onDeleteCalls = 0;
226
226
  });
227
- it('should render and handle action buttons correctly', function () {
227
+ it('should render and handle action buttons correctly, including a GOVUK default Change Link when no classModifiers are passed in', function () {
228
228
  var _renderWithValidation4 = (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react2.default.createElement(_RenderListView.default, {
229
229
  id: ID,
230
230
  entryData: ENTRY,
@@ -238,7 +238,7 @@ describe('components.CollectionSummary.RenderListView', function () {
238
238
  var listViewDiv = container.querySelector('.govuk-summary-card');
239
239
  expect(listViewDiv).not.toBeNull();
240
240
 
241
- // Check for change button
241
+ // Check for change link
242
242
  var changeLink = listViewDiv.querySelector('.govuk-link');
243
243
  expect(changeLink.textContent).toEqual(CONFIG.changeAction.label);
244
244
  expect(changeLink.getAttribute('aria-label')).toEqual("".concat(CONFIG.changeAction.label, " ").concat(CONFIG.title));
@@ -257,5 +257,38 @@ describe('components.CollectionSummary.RenderListView', function () {
257
257
  expect(onDeleteCalls).toEqual(1);
258
258
  expect(onDeleteArgs[0]).toEqual(ENTRY);
259
259
  });
260
+ it('should render and handle an Update button instead of the GOVUK default Change link when classModifiers are passed in', function () {
261
+ var CONFIG_WITH_BUTTON = _objectSpread(_objectSpread({}, CONFIG), {}, {
262
+ changeAction: {
263
+ label: 'Update label',
264
+ page: 'testPage',
265
+ classModifiers: 'primary'
266
+ }
267
+ });
268
+ var _renderWithValidation5 = (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react2.default.createElement(_RenderListView.default, {
269
+ id: ID,
270
+ entryData: ENTRY,
271
+ config: CONFIG_WITH_BUTTON,
272
+ onFullEdit: ON_CHANGE,
273
+ onDelete: ON_DELETE,
274
+ masterPage: MASTER_PAGE,
275
+ getComponentRow: getComponentRow
276
+ })),
277
+ container = _renderWithValidation5.container;
278
+ var listViewDiv = container.querySelector('.govuk-summary-card');
279
+ expect(listViewDiv).not.toBeNull();
280
+
281
+ // Check for update button
282
+ var changeButton = listViewDiv.querySelector('.hods-button');
283
+ expect(changeButton.classList).toContain('hods-button--primary');
284
+ expect(changeButton.textContent).toEqual(CONFIG_WITH_BUTTON.changeAction.label);
285
+ expect(changeButton.getAttribute('aria-label')).toEqual("".concat(CONFIG_WITH_BUTTON.changeAction.label, " ").concat(CONFIG_WITH_BUTTON.title));
286
+ _react.fireEvent.click(changeButton, {});
287
+ expect(onChangeCalls).toEqual(1);
288
+ expect(onChangeArgs[0]).toMatchObject({
289
+ page: CONFIG_WITH_BUTTON.changeAction.page,
290
+ id: ENTRY.id
291
+ });
292
+ });
260
293
  });
261
294
  });
@@ -15,6 +15,7 @@ var _FormPage = _interopRequireDefault(require("../FormPage"));
15
15
  var _BannerStrip = _interopRequireDefault(require("./BannerStrip"));
16
16
  var _RenderListView = _interopRequireDefault(require("./RenderListView"));
17
17
  var _SummaryCardDetails = _interopRequireDefault(require("./SummaryCardDetails"));
18
+ var _SummaryCardButtons = require("./SummaryCardButtons");
18
19
  require("./SummaryCard.scss");
19
20
  var _excluded = ["isDuplicate"];
20
21
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
@@ -47,7 +48,7 @@ var DEFAULT_DUPLICATE_BUTTON_LABEL = exports.DEFAULT_DUPLICATE_BUTTON_LABEL = 'D
47
48
  var DEFAULT_DETAILS_TITLE = exports.DEFAULT_DETAILS_TITLE = 'Full details';
48
49
  var DEFAULT_CHANGE_BUTTON_CLASS = exports.DEFAULT_CHANGE_BUTTON_CLASS = 'secondary';
49
50
  var SummaryCard = function SummaryCard(_ref) {
50
- var _config$changeAction2, _config$changeAction3, _config$changeAction4, _config$deleteAction, _config$deleteAction2, _config$duplicateActi, _config$duplicateActi2, _config$fullDetails2;
51
+ var _config$deleteAction, _config$deleteAction2, _config$duplicateActi, _config$duplicateActi2, _config$fullDetails2;
51
52
  var id = _ref.id,
52
53
  entryData = _ref.entryData,
53
54
  config = _ref.config,
@@ -191,18 +192,16 @@ var SummaryCard = function SummaryCard(_ref) {
191
192
  disabled: quickEdit,
192
193
  name: "QuickEdit",
193
194
  autoFocus: config.focusOn
194
- }, DEFAULT_EDIT_LABEL), config.changeAction && typeof onFullEdit === 'function' && /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
195
+ }, DEFAULT_EDIT_LABEL), config.changeAction && typeof onFullEdit === 'function' && /*#__PURE__*/_react.default.createElement(_SummaryCardButtons.SummaryCardButtons, {
195
196
  id: "".concat(id, ".changeButton"),
196
197
  "data-testid": "change-button",
197
- onClick: function onClick() {
198
- var _config$changeAction;
199
- return onFullEdit((_config$changeAction = config.changeAction) === null || _config$changeAction === void 0 ? void 0 : _config$changeAction.page, entryData.id);
200
- },
201
- classModifiers: ((_config$changeAction2 = config.changeAction) === null || _config$changeAction2 === void 0 ? void 0 : _config$changeAction2.classModifiers) || DEFAULT_CHANGE_BUTTON_CLASS,
202
- "aria-label": _utils.default.FormPage.getConditionalText((_config$changeAction3 = config.changeAction) === null || _config$changeAction3 === void 0 ? void 0 : _config$changeAction3.aria_label, entryData),
198
+ actions: config.changeAction,
199
+ onFullEdit: onFullEdit,
200
+ entryData: entryData,
201
+ formData: formData,
203
202
  name: "Change",
204
203
  autoFocus: config.focusOn
205
- }, ((_config$changeAction4 = config.changeAction) === null || _config$changeAction4 === void 0 ? void 0 : _config$changeAction4.label) || DEFAULT_CHANGE_BUTTON_LABEL), config.deleteAction && typeof onDelete === 'function' && /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
204
+ }), config.deleteAction && typeof onDelete === 'function' && /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
206
205
  id: "".concat(id, ".deleteButton"),
207
206
  onClick: function onClick() {
208
207
  return onDelete(entryData);
@@ -250,12 +249,7 @@ SummaryCard.propTypes = {
250
249
  banners: _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.shape({})])),
251
250
  title: _propTypes.default.string,
252
251
  details: _propTypes.default.string,
253
- changeAction: _propTypes.default.shape({
254
- label: _propTypes.default.string,
255
- page: _propTypes.default.string.isRequired,
256
- classModifiers: _propTypes.default.string,
257
- aria_label: _propTypes.default.string
258
- }),
252
+ changeAction: _propTypes.default.oneOfType([_propTypes.default.shape(_SummaryCardButtons.actionShape), _propTypes.default.arrayOf(_propTypes.default.shape(_SummaryCardButtons.actionShape))]),
259
253
  deleteAction: _propTypes.default.shape({
260
254
  label: _propTypes.default.string,
261
255
  aria_label: _propTypes.default.string
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.actionShape = exports.SummaryCardButtons = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _propTypes = _interopRequireDefault(require("prop-types"));
9
+ var _copReactComponents = require("@ukhomeoffice/cop-react-components");
10
+ var _SummaryCard = require("./SummaryCard");
11
+ var _utils = _interopRequireDefault(require("../../utils"));
12
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
+ var actionShape = exports.actionShape = {
14
+ label: _propTypes.default.string,
15
+ page: _propTypes.default.string.isRequired,
16
+ classModifiers: _propTypes.default.string,
17
+ aria_label: _propTypes.default.string
18
+ };
19
+ var SummaryCardButtons = exports.SummaryCardButtons = function SummaryCardButtons(_ref) {
20
+ var id = _ref.id,
21
+ testId = _ref["data-testid"],
22
+ actions = _ref.actions,
23
+ onFullEdit = _ref.onFullEdit,
24
+ entryData = _ref.entryData,
25
+ formData = _ref.formData,
26
+ name = _ref.name,
27
+ autoFocus = _ref.autoFocus;
28
+ if (!Array.isArray(actions)) {
29
+ return /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
30
+ id: id,
31
+ "data-testid": testId,
32
+ onClick: function onClick() {
33
+ return onFullEdit(actions === null || actions === void 0 ? void 0 : actions.page, entryData.id);
34
+ },
35
+ classModifiers: (actions === null || actions === void 0 ? void 0 : actions.classModifiers) || _SummaryCard.DEFAULT_CHANGE_BUTTON_CLASS,
36
+ "aria-label": _utils.default.FormPage.getConditionalText(actions === null || actions === void 0 ? void 0 : actions.aria_label, entryData),
37
+ name: name,
38
+ autoFocus: autoFocus
39
+ }, (actions === null || actions === void 0 ? void 0 : actions.label) || _SummaryCard.DEFAULT_CHANGE_BUTTON_LABEL);
40
+ }
41
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, actions.filter(function (a) {
42
+ return _utils.default.Component.show(a, formData);
43
+ }).map(function (action) {
44
+ return /*#__PURE__*/_react.default.createElement(_copReactComponents.Button, {
45
+ id: id,
46
+ key: id,
47
+ "data-testid": testId,
48
+ onClick: function onClick() {
49
+ return onFullEdit(action === null || action === void 0 ? void 0 : action.page, entryData.id);
50
+ },
51
+ classModifiers: (action === null || action === void 0 ? void 0 : action.classModifiers) || _SummaryCard.DEFAULT_CHANGE_BUTTON_CLASS,
52
+ "aria-label": _utils.default.FormPage.getConditionalText(action === null || action === void 0 ? void 0 : action.aria_label, entryData),
53
+ name: name,
54
+ autoFocus: autoFocus
55
+ }, (action === null || action === void 0 ? void 0 : action.label) || _SummaryCard.DEFAULT_CHANGE_BUTTON_LABEL);
56
+ }));
57
+ };
58
+ SummaryCardButtons.propTypes = {
59
+ id: _propTypes.default.string.isRequired,
60
+ "data-testid": _propTypes.default.string,
61
+ entryData: _propTypes.default.shape({
62
+ id: _propTypes.default.string.isRequired,
63
+ index: _propTypes.default.number.isRequired
64
+ }).isRequired,
65
+ onFullEdit: _propTypes.default.func,
66
+ autoFocus: _propTypes.default.bool,
67
+ name: _propTypes.default.string,
68
+ actions: _propTypes.default.oneOfType([_propTypes.default.shape(actionShape), _propTypes.default.arrayOf(_propTypes.default.shape(actionShape))]).isRequired,
69
+ formData: _propTypes.default.shape({}).isRequired
70
+ };
71
+ SummaryCardButtons.defaultProps = {
72
+ onFullEdit: null,
73
+ "data-testid": null,
74
+ autoFocus: null,
75
+ name: null
76
+ };
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ require("@testing-library/jest-dom/extend-expect");
4
+ var _react = _interopRequireDefault(require("react"));
5
+ var _setupTests = require("../../setupTests");
6
+ var _SummaryCardButtons = require("./SummaryCardButtons");
7
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
+ // Global imports.
9
+
10
+ // Local imports.
11
+
12
+ describe('components.CollectionSummary.SummaryCardButtons', function () {
13
+ var ID = 'summaryCardId';
14
+ var ENTRY = {
15
+ id: '001',
16
+ bannerText: 'A banner',
17
+ titleText: 'A title',
18
+ detailsText: 'Some details',
19
+ index: 0,
20
+ summaryText: 'Full details'
21
+ };
22
+ it('should correctly render a SummaryCardButtons component', function () {
23
+ var CONFIG = {
24
+ changeAction: {
25
+ label: 'Change label',
26
+ page: 'testPage',
27
+ aria_label: [{
28
+ text: 'testText'
29
+ }]
30
+ }
31
+ };
32
+ var _renderWithValidation = (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react.default.createElement(_SummaryCardButtons.SummaryCardButtons, {
33
+ id: "".concat(ID, ".changeButton"),
34
+ "data-testid": "change-button",
35
+ actions: CONFIG.changeAction,
36
+ name: "Change",
37
+ entryData: ENTRY,
38
+ formData: {}
39
+ })),
40
+ container = _renderWithValidation.container;
41
+ var changeButton = container.children[0];
42
+ expect(changeButton.name).toEqual('Change');
43
+ expect(changeButton.tagName).toEqual('BUTTON');
44
+ expect(changeButton.textContent).toEqual(CONFIG.changeAction.label);
45
+ expect(changeButton.getAttribute('aria-label')).toEqual(CONFIG.changeAction.aria_label[0].text);
46
+ });
47
+ it('should correctly render a SummaryCardButtons component when array of buttons', function () {
48
+ var CONFIG = {
49
+ changeAction: [{
50
+ label: 'Change label',
51
+ page: 'testPage',
52
+ aria_label: [{
53
+ text: 'testText'
54
+ }]
55
+ }, {
56
+ label: 'Invalid label',
57
+ page: 'testPage',
58
+ aria_label: [{
59
+ text: 'testText'
60
+ }],
61
+ show_when: {
62
+ field: "test",
63
+ op: "=",
64
+ value: true
65
+ }
66
+ }]
67
+ };
68
+ var _renderWithValidation2 = (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react.default.createElement(_SummaryCardButtons.SummaryCardButtons, {
69
+ id: "".concat(ID, ".changeButton"),
70
+ "data-testid": "change-button",
71
+ actions: CONFIG.changeAction,
72
+ name: "Change",
73
+ entryData: ENTRY,
74
+ formData: {}
75
+ })),
76
+ container = _renderWithValidation2.container;
77
+ var changeButton = container.children[0];
78
+ expect(changeButton.name).toEqual('Change');
79
+ expect(changeButton.tagName).toEqual('BUTTON');
80
+ expect(changeButton.textContent).toEqual(CONFIG.changeAction[0].label);
81
+ expect(changeButton.getAttribute('aria-label')).toEqual(CONFIG.changeAction[0].aria_label[0].text);
82
+ });
83
+ });
@@ -118,11 +118,13 @@ var Collection = function Collection(_ref) {
118
118
  (0, _react.useEffect)(function () {
119
119
  if (config.focusOnAdd && Array.isArray(value) && value.length) {
120
120
  if (value.length > config.minimumEntries || !config.minimumEntries) {
121
- var _value2, _document$getElementB, _focusable$;
121
+ var _value2, _document$getElementB;
122
122
  var containerId = (_value2 = value[value.length - 1]) === null || _value2 === void 0 ? void 0 : _value2.id;
123
123
  var container = (_document$getElementB = document.getElementById(containerId)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.childNodes[0];
124
124
  var focusable = container === null || container === void 0 ? void 0 : container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
125
- (_focusable$ = focusable[0]) === null || _focusable$ === void 0 || _focusable$.focus();
125
+ if (focusable && focusable.length > 0) {
126
+ focusable[0].focus();
127
+ }
126
128
  }
127
129
  }
128
130
  }, [value.length, config.focusOnAdd, config.minimumEntries]);
@@ -140,7 +142,7 @@ var Collection = function Collection(_ref) {
140
142
  id: config.id,
141
143
  required: config.required,
142
144
  className: classes('title')
143
- }, config.label), value && value.map(function (item, index) {
145
+ }, config.label), Array.isArray(value) && value.map(function (item, index) {
144
146
  var _config$removeLocatio;
145
147
  var fullPath = "".concat(config.full_path || config.fieldId, "[").concat(index, "]");
146
148
  var labelCount = (config.countOffset || 0) + index + 1;
@@ -31,7 +31,7 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } // Global
31
31
  // Styles
32
32
  var DEFAULT_CLASS = exports.DEFAULT_CLASS = 'hods-form__page';
33
33
  var FormPage = function FormPage(_ref) {
34
- var _formPage$actions, _page$actions;
34
+ var _page$customValidatio, _formPage$actions, _page$actions;
35
35
  var page = _ref.page,
36
36
  pages = _ref.pages,
37
37
  _onAction = _ref.onAction,
@@ -42,17 +42,33 @@ var FormPage = function FormPage(_ref) {
42
42
  classBlock = _ref.classBlock,
43
43
  classModifiers = _ref.classModifiers,
44
44
  className = _ref.className,
45
- submitting = _ref.submitting;
45
+ submitting = _ref.submitting,
46
+ pageId = _ref.pageId,
47
+ fromTarget = _ref.fromTarget;
46
48
  var _useState = (0, _react.useState)({}),
47
49
  _useState2 = _slicedToArray(_useState, 2),
48
50
  patch = _useState2[0],
49
51
  setPatch = _useState2[1];
50
52
  var _useValidation = (0, _hooks.useValidation)(),
51
- errors = _useValidation.errors;
53
+ errors = _useValidation.errors,
54
+ validate = _useValidation.validate;
52
55
  var _useState3 = (0, _react.useState)({}),
53
56
  _useState4 = _slicedToArray(_useState3, 2),
54
57
  patchLabel = _useState4[0],
55
58
  setPatchLabel = _useState4[1];
59
+ var _useState5 = (0, _react.useState)(false),
60
+ _useState6 = _slicedToArray(_useState5, 2),
61
+ isPageRendered = _useState6[0],
62
+ setIsPageRendered = _useState6[1];
63
+ var runOnLoad = Array.isArray(page === null || page === void 0 ? void 0 : page.customValidation) && page.customValidation.length > 0 ? ((_page$customValidatio = page.customValidation[0]) === null || _page$customValidatio === void 0 || (_page$customValidatio = _page$customValidatio.runWhen) === null || _page$customValidatio === void 0 ? void 0 : _page$customValidatio.onLoad) === true : false;
64
+ (0, _react.useEffect)(function () {
65
+ setIsPageRendered(true);
66
+ }, []);
67
+ (0, _react.useEffect)(function () {
68
+ if (isPageRendered && fromTarget && runOnLoad) {
69
+ validate.page(page);
70
+ }
71
+ }, [pageId, isPageRendered]);
56
72
 
57
73
  /**
58
74
  * Handle the state of the data directly within the page.
@@ -121,11 +137,20 @@ var FormPage = function FormPage(_ref) {
121
137
  submitting: submitting
122
138
  });
123
139
  });
140
+ var errorMessages = null;
141
+ if (runOnLoad && (errors === null || errors === void 0 ? void 0 : errors.length) > 0) {
142
+ var _page$customValidatio2;
143
+ errorMessages = [{
144
+ error: page === null || page === void 0 || (_page$customValidatio2 = page.customValidation) === null || _page$customValidatio2 === void 0 || (_page$customValidatio2 = _page$customValidatio2[0]) === null || _page$customValidatio2 === void 0 ? void 0 : _page$customValidatio2.message
145
+ }];
146
+ } else if ((errors === null || errors === void 0 ? void 0 : errors.length) > 0 && !runOnLoad) {
147
+ errorMessages = errors;
148
+ }
124
149
  return /*#__PURE__*/_react.default.createElement("div", {
125
150
  className: classes(),
126
151
  key: page.id
127
- }, (errors === null || errors === void 0 ? void 0 : errors.length) > 0 && /*#__PURE__*/_react.default.createElement(_copReactComponents.ErrorSummary, {
128
- errors: errors,
152
+ }, errorMessages && /*#__PURE__*/_react.default.createElement(_copReactComponents.ErrorSummary, {
153
+ errors: errorMessages,
129
154
  hashLink: hashLink
130
155
  }), page.fieldset && /*#__PURE__*/_react.default.createElement("fieldset", {
131
156
  className: "govuk-fieldset"
@@ -162,10 +187,18 @@ FormPage.propTypes = {
162
187
  label: _propTypes.default.string,
163
188
  required: _propTypes.default.bool,
164
189
  actions: _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.shape({}), _propTypes.default.string])),
165
- formData: _propTypes.default.shape({}).isRequired
190
+ formData: _propTypes.default.shape({}).isRequired,
191
+ customValidation: _propTypes.default.arrayOf(_propTypes.default.shape({
192
+ runWhen: _propTypes.default.shape({
193
+ onLoad: _propTypes.default.bool
194
+ }).isRequired,
195
+ message: _propTypes.default.string.isRequired
196
+ }))
166
197
  }).isRequired,
167
198
  pages: _propTypes.default.arrayOf(_propTypes.default.shape({})),
168
- submitting: _propTypes.default.bool
199
+ submitting: _propTypes.default.bool,
200
+ fromTarget: _propTypes.default.bool,
201
+ pageId: _propTypes.default.string.isRequired
169
202
  };
170
203
  FormPage.defaultProps = {
171
204
  classBlock: DEFAULT_CLASS,
@@ -176,6 +209,7 @@ FormPage.defaultProps = {
176
209
  onTopLevelChange: undefined,
177
210
  onWrapperChange: undefined,
178
211
  pages: [],
179
- submitting: false
212
+ submitting: false,
213
+ fromTarget: false
180
214
  };
181
215
  var _default = exports.default = FormPage;
@@ -2,6 +2,7 @@
2
2
 
3
3
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
4
4
  var _react = require("@testing-library/react");
5
+ require("@testing-library/jest-dom");
5
6
  var _react2 = _interopRequireDefault(require("react"));
6
7
  var _models = require("../../models");
7
8
  var _setupTests = require("../../setupTests");
@@ -53,6 +54,30 @@ describe('components.FormPage', function () {
53
54
  text: VALUE
54
55
  }
55
56
  };
57
+ var PAGE_WITH_CUSTOM_VALIDATION = {
58
+ id: 'pageId',
59
+ title: 'Page with Custom Validation',
60
+ components: [TEXT],
61
+ actions: [_models.PageAction.TYPES.SUBMIT],
62
+ formData: {
63
+ text: VALUE
64
+ },
65
+ customValidation: [{
66
+ message: 'Custom validation failed',
67
+ runWhen: {
68
+ onLoad: true
69
+ }
70
+ }]
71
+ };
72
+ var PAGE_WITHOUT_CUSTOM_VALIDATION = {
73
+ id: 'pageId',
74
+ title: 'Page without Custom Validation',
75
+ components: [TEXT],
76
+ actions: [_models.PageAction.TYPES.SUBMIT],
77
+ formData: {
78
+ text: VALUE
79
+ }
80
+ };
56
81
  var PAGE_WITH_BUTTON_ACTIONS = {
57
82
  id: 'pageId',
58
83
  // eslint-disable-next-line no-template-curly-in-string
@@ -328,5 +353,41 @@ describe('components.FormPage', function () {
328
353
  }
329
354
  }, _callee5);
330
355
  })));
356
+ it('should not trigger custom validation when fromTarget is false', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6() {
357
+ var _renderWithValidation6, queryByText;
358
+ return _regeneratorRuntime().wrap(function _callee6$(_context6) {
359
+ while (1) switch (_context6.prev = _context6.next) {
360
+ case 0:
361
+ _renderWithValidation6 = (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react2.default.createElement(_FormPage.default, {
362
+ page: PAGE_WITH_CUSTOM_VALIDATION,
363
+ fromTarget: false,
364
+ onAction: ON_ACTION
365
+ })), queryByText = _renderWithValidation6.queryByText;
366
+ _context6.next = 3;
367
+ return (0, _react.waitFor)(function () {
368
+ expect(queryByText('Custom validation failed')).toBeNull();
369
+ });
370
+ case 3:
371
+ case "end":
372
+ return _context6.stop();
373
+ }
374
+ }, _callee6);
375
+ })));
376
+ it('should not trigger custom validation if no customValidation is provided', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee7() {
377
+ return _regeneratorRuntime().wrap(function _callee7$(_context7) {
378
+ while (1) switch (_context7.prev = _context7.next) {
379
+ case 0:
380
+ (0, _setupTests.renderWithValidation)( /*#__PURE__*/_react2.default.createElement(_FormPage.default, {
381
+ page: PAGE_WITHOUT_CUSTOM_VALIDATION,
382
+ fromTarget: true,
383
+ onAction: ON_ACTION
384
+ }));
385
+ expect(ON_ACTION_CALLS.length).toEqual(0);
386
+ case 2:
387
+ case "end":
388
+ return _context7.stop();
389
+ }
390
+ }, _callee7);
391
+ })));
331
392
  });
332
393
  });
@@ -60,7 +60,8 @@ var FormRenderer = function FormRenderer(_ref) {
60
60
  noChangeAction = _ref.noChangeAction,
61
61
  newPageId = _ref.newPageId,
62
62
  viewOnly = _ref.viewOnly,
63
- hideBlankRows = _ref.hideBlankRows;
63
+ hideBlankRows = _ref.hideBlankRows,
64
+ fromTarget = _ref.fromTarget;
64
65
  return /*#__PURE__*/_react.default.createElement(_context.HooksContextProvider, {
65
66
  overrides: hooks
66
67
  }, /*#__PURE__*/_react.default.createElement(_context.ValidationContextProvider, null, /*#__PURE__*/_react.default.createElement(InternalFormRenderer, {
@@ -80,7 +81,8 @@ var FormRenderer = function FormRenderer(_ref) {
80
81
  noChangeAction: noChangeAction,
81
82
  newPageId: newPageId,
82
83
  viewOnly: viewOnly,
83
- hideBlankRows: hideBlankRows
84
+ hideBlankRows: hideBlankRows,
85
+ fromTarget: fromTarget
84
86
  })));
85
87
  };
86
88
  var DEFAULT_CLASS = exports.DEFAULT_CLASS = 'hods-form';
@@ -102,7 +104,8 @@ var InternalFormRenderer = function InternalFormRenderer(_ref2) {
102
104
  noChangeAction = _ref2.noChangeAction,
103
105
  newPageId = _ref2.newPageId,
104
106
  viewOnly = _ref2.viewOnly,
105
- hideBlankRows = _ref2.hideBlankRows;
107
+ hideBlankRows = _ref2.hideBlankRows,
108
+ fromTarget = _ref2.fromTarget;
106
109
  // Set up the initial states.
107
110
  var _useState = (0, _react.useState)({}),
108
111
  _useState2 = _slicedToArray(_useState, 2),
@@ -364,7 +367,9 @@ var InternalFormRenderer = function InternalFormRenderer(_ref2) {
364
367
  hashLink: hashLink,
365
368
  classModifiers: formState.page.classModifiers,
366
369
  className: formState.page.className,
367
- submitting: submitting
370
+ submitting: submitting,
371
+ pageId: pageId,
372
+ fromTarget: fromTarget
368
373
  }), formState.page && !formState.cya && formState.page.collection && /*#__PURE__*/_react.default.createElement(_CollectionPage.default, {
369
374
  page: formState.page,
370
375
  pages: pages,
@@ -407,7 +412,8 @@ var propTypes = {
407
412
  /** See <a href="/?path=/docs/f-json--page#formtypes">FormTypes</a>. */
408
413
  type: _propTypes.default.oneOf([_models.FormTypes.CYA, _models.FormTypes.FORM, _models.FormTypes.HUB, _models.FormTypes.TASK, _models.FormTypes.WIZARD, _models.FormTypes.TASK_CYA, _models.FormTypes.FORM_WITH_TASK]).isRequired,
409
414
  viewOnly: _propTypes.default.bool,
410
- hideBlankRows: _propTypes.default.bool
415
+ hideBlankRows: _propTypes.default.bool,
416
+ fromTarget: _propTypes.default.bool.isRequired
411
417
  };
412
418
  var defaultProps = {
413
419
  classBlock: DEFAULT_CLASS,
@@ -215,6 +215,9 @@ var getCYARowsForCollectionPage = function getCYARowsForCollectionPage(page, onA
215
215
  }
216
216
  collectionData.forEach(function (item) {
217
217
  var _page$collection2;
218
+ if (!item) {
219
+ return; // Skip this iteration if item is undefined
220
+ }
218
221
  // Keep track of the active entry in each collection.
219
222
  // This helps us make sure the right entry is being affected
220
223
  // by change/remove links.
@@ -18,7 +18,9 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _ty
18
18
  function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // Local imports
19
19
  var getCYARowsForContainer = function getCYARowsForContainer(page, container, formData, onAction, fnOverride) {
20
20
  if ((0, _showComponentCYA.default)(container, _objectSpread(_objectSpread({}, page.formData), formData))) {
21
- var allComponents = (0, _elevateNestedComponents.default)(container.components, formData);
21
+ var allComponents = (0, _elevateNestedComponents.default)(container.components.filter(function (component) {
22
+ return component.show_on_cya === undefined || component.show_on_cya;
23
+ }), formData);
22
24
  return allComponents.filter(function (c) {
23
25
  return (0, _showComponentCYA.default)(c, _objectSpread(_objectSpread({}, page.formData), formData));
24
26
  }).flatMap(function (component) {
@@ -151,6 +151,48 @@ describe('utils.CheckYourAnswers.getCYARowsForContainer', function () {
151
151
  });
152
152
  });
153
153
  });
154
+ it('should not display nested input components when passed a value of false for "show_on_cya" at the parent component level', function () {
155
+ var FORM_DATA = {
156
+ container: {
157
+ topLevelInput: 'A',
158
+ nestedInput: 'Bravo'
159
+ }
160
+ };
161
+ var PAGE = {
162
+ id: 'page',
163
+ formData: FORM_DATA,
164
+ cya_link: {}
165
+ };
166
+ var NESTED_COMPONENT = {
167
+ type: 'text',
168
+ id: 'nestedInput',
169
+ fieldId: 'nestedInput',
170
+ label: 'Nested input',
171
+ value: 'Bravo'
172
+ };
173
+ var COMPONENT = {
174
+ type: 'checkboxes',
175
+ id: 'topLevelInput',
176
+ fieldId: 'topLevelInput',
177
+ label: 'Top level input',
178
+ show_on_cya: false,
179
+ options: [{
180
+ value: 'A',
181
+ label: 'Option Alpha',
182
+ nested: [NESTED_COMPONENT]
183
+ }]
184
+ };
185
+ var CONTAINER = {
186
+ id: 'container',
187
+ fieldId: 'container',
188
+ type: _models.ComponentTypes.CONTAINER,
189
+ components: [COMPONENT],
190
+ value: FORM_DATA
191
+ };
192
+ var ON_ACTION = function ON_ACTION() {};
193
+ var ROWS = (0, _getCYARowsForContainer.default)(PAGE, CONTAINER, FORM_DATA.container, ON_ACTION);
194
+ expect(ROWS.length).toEqual(0);
195
+ });
154
196
  it('should get an appropriate row for a container with a single readonly text component inside a nested container', function () {
155
197
  var FORM_DATA = {
156
198
  container: {
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.getChildrenJsx = exports.default = void 0;
8
8
  var _copReactComponents = require("@ukhomeoffice/cop-react-components");
9
9
  var _react = _interopRequireDefault(require("react"));
10
+ var _Condition = _interopRequireDefault(require("../Condition"));
10
11
  var _models = require("../../models");
11
12
  var _Data = _interopRequireDefault(require("../Data"));
12
13
  var _cleanAttributes = _interopRequireDefault(require("./cleanAttributes"));
@@ -265,11 +266,14 @@ var getChildJsx = function getChildJsx(parent, paramChild) {
265
266
  * @param {*} childrenConfigs array of configurations for the child components
266
267
  */
267
268
  var getChildrenJsx = exports.getChildrenJsx = function getChildrenJsx(parentConfig, childrenConfigs) {
268
- return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, childrenConfigs.map(function (config) {
269
+ var validChildren = childrenConfigs.filter(function (config) {
270
+ return !(config.show_when && !_Condition.default.meetsAll(config.show_when, parentConfig.formData));
271
+ }).map(function (config) {
269
272
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, {
270
273
  key: config.id
271
274
  }, getChildJsx(parentConfig, config));
272
- }));
275
+ });
276
+ return validChildren.length > 0 ? validChildren : null;
273
277
  };
274
278
  /**
275
279
  * Get a renderable component, based on a configuration object.
@@ -180,4 +180,56 @@ describe('utils.Component.get', function () {
180
180
  }
181
181
  }, _callee4);
182
182
  })));
183
+ it('should correctly show nested component with show_when', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5() {
184
+ var FIELD_ID, VALUE, VisibleComponent, HiddenComponent, PARENT_CONFIG, _renderWithValidation4, container, child, label, input;
185
+ return _regeneratorRuntime().wrap(function _callee5$(_context5) {
186
+ while (1) switch (_context5.prev = _context5.next) {
187
+ case 0:
188
+ FIELD_ID = 'fieldId';
189
+ VALUE = 'nestedValue';
190
+ VisibleComponent = {
191
+ id: 'testId',
192
+ fieldId: FIELD_ID,
193
+ label: 'Test label',
194
+ type: 'text',
195
+ show_when: [{
196
+ field: FIELD_ID,
197
+ op: "=",
198
+ value: VALUE
199
+ }]
200
+ };
201
+ HiddenComponent = {
202
+ id: 'hiddenID',
203
+ fieldId: "hiddenFeild",
204
+ label: 'Hidden label',
205
+ type: 'text',
206
+ show_when: [{
207
+ field: FIELD_ID,
208
+ op: "!=",
209
+ value: VALUE
210
+ }]
211
+ };
212
+ PARENT_CONFIG = {
213
+ onChange: function onChange() {},
214
+ formData: _defineProperty({}, FIELD_ID, VALUE)
215
+ };
216
+ _renderWithValidation4 = (0, _setupTests.renderWithValidation)((0, _getComponent.getChildrenJsx)(PARENT_CONFIG, [VisibleComponent, HiddenComponent])), container = _renderWithValidation4.container;
217
+ expect(container.childNodes.length).toEqual(1);
218
+ child = container.childNodes[0];
219
+ expect(child.childNodes[0].childNodes.length).toEqual(2);
220
+ expect(child.classList).toContain('govuk-form-group');
221
+ label = child.childNodes[0];
222
+ expect(label).toBeDefined();
223
+ expect(label.innerHTML).toContain(VisibleComponent.label);
224
+ input = child.childNodes[2];
225
+ expect(input.tagName).toEqual('INPUT');
226
+ expect(input.classList).toContain('govuk-input');
227
+ expect(input.id).toEqual("".concat(VisibleComponent.id));
228
+ expect(input.value).toEqual(VALUE);
229
+ case 18:
230
+ case "end":
231
+ return _context5.stop();
232
+ }
233
+ }, _callee5);
234
+ })));
183
235
  });
@@ -26,6 +26,9 @@ var interpolateOptions = function interpolateOptions(config, options) {
26
26
  if (typeof opt === 'string') {
27
27
  return opt;
28
28
  }
29
+ if (!opt.value) {
30
+ return opt.label;
31
+ }
29
32
  if (config.alternativeHintField) {
30
33
  // eslint-disable-next-line no-param-reassign
31
34
  opt.hint = opt[config.alternativeHintField];
@@ -39,13 +42,44 @@ var interpolateOptions = function interpolateOptions(config, options) {
39
42
  return n;
40
43
  });
41
44
  };
45
+
46
+ /**
47
+ * Returns a list of options templated from a collection specified.
48
+ * config requires a item field with the templated options e.g
49
+ * "item": {
50
+ * "value": "${id}"
51
+ * "label": "${name}"
52
+ * "hint": "Select a person name"
53
+ * }
54
+ * @param {*} config
55
+ * @param {*} collectionName
56
+ * @returns
57
+ */
58
+ var getCollectionOptions = function getCollectionOptions(config, collectionName) {
59
+ var collectionOptions = config.formData[collectionName].map(function (collection) {
60
+ return {
61
+ value: _copReactComponents.Utils.interpolateString(config.item.value, collection),
62
+ label: _copReactComponents.Utils.interpolateString(config.item.label, collection),
63
+ hint: _copReactComponents.Utils.interpolateString(config.item.hint, collection)
64
+ };
65
+ });
66
+ if (config.data.options) {
67
+ return [].concat(collectionOptions, config.data.options);
68
+ }
69
+ return collectionOptions;
70
+ };
42
71
  var getOptions = function getOptions(config, callback) {
43
72
  if (config) {
44
- var _config$data, _config$data2;
45
- if (config.options && config !== null && config !== void 0 && (_config$data = config.data) !== null && _config$data !== void 0 && _config$data.options && config !== null && config !== void 0 && (_config$data2 = config.data) !== null && _config$data2 !== void 0 && _config$data2.url) {
46
- var combinedOptions = (0, _nestInRefdataOptions.default)(config.options, config.data.options);
73
+ var _config$data, _config$data3, _config$data4;
74
+ if (config !== null && config !== void 0 && (_config$data = config.data) !== null && _config$data !== void 0 && _config$data.collection && config !== null && config !== void 0 && config.item) {
75
+ var _config$data2;
76
+ var combinedOptions = getCollectionOptions(config, config === null || config === void 0 || (_config$data2 = config.data) === null || _config$data2 === void 0 ? void 0 : _config$data2.collection);
47
77
  return callback(interpolateOptions(config, combinedOptions));
48
78
  }
79
+ if (config.options && config !== null && config !== void 0 && (_config$data3 = config.data) !== null && _config$data3 !== void 0 && _config$data3.options && config !== null && config !== void 0 && (_config$data4 = config.data) !== null && _config$data4 !== void 0 && _config$data4.url) {
80
+ var _combinedOptions = (0, _nestInRefdataOptions.default)(config.options, config.data.options);
81
+ return callback(interpolateOptions(config, _combinedOptions));
82
+ }
49
83
  if (config.options) {
50
84
  return callback(interpolateOptions(config, config.options));
51
85
  }
@@ -28,6 +28,16 @@ describe('utils', function () {
28
28
  expect(options).toEqual(CONFIG.options);
29
29
  });
30
30
  });
31
+ it('should handle stings in option and if no value is supplied treat it as a string', function () {
32
+ var CONFIG = {
33
+ options: ["or", {
34
+ label: "and"
35
+ }]
36
+ };
37
+ (0, _getOptions.default)(CONFIG, function (options) {
38
+ expect(options).toEqual(["or", "and"]);
39
+ });
40
+ });
31
41
  it('should get any specified options from the data property of the config', function () {
32
42
  var CONFIG = {
33
43
  data: {
@@ -46,6 +56,44 @@ describe('utils', function () {
46
56
  expect(options).toEqual(CONFIG.data.options);
47
57
  });
48
58
  });
59
+ it('should get options from form data collection', function () {
60
+ var CONFIG = {
61
+ data: {
62
+ collection: "people"
63
+ },
64
+ item: {
65
+ /* eslint-disable no-template-curly-in-string */
66
+ label: "${name}",
67
+ value: "${name}",
68
+ hint: "${name}"
69
+ /* eslint-enable no-template-curly-in-string */
70
+ },
71
+
72
+ formData: {
73
+ people: [{
74
+ "name": "test"
75
+ }]
76
+ }
77
+ };
78
+ (0, _getOptions.default)(CONFIG, function (options) {
79
+ expect(options[0].hint).toEqual("test");
80
+ });
81
+ });
82
+ it('should not show options if there is collection but not item data', function () {
83
+ var CONFIG = {
84
+ data: {
85
+ collection: "people"
86
+ },
87
+ formData: {
88
+ people: [{
89
+ "name": "test"
90
+ }]
91
+ }
92
+ };
93
+ (0, _getOptions.default)(CONFIG, function (options) {
94
+ expect(options).toEqual([]);
95
+ });
96
+ });
49
97
  it('should use the top-level options over those in the data property', function () {
50
98
  var CONFIG = {
51
99
  options: [{
@@ -9,6 +9,7 @@ var _Data = _interopRequireDefault(require("../Data"));
9
9
  var _getPageActions = _interopRequireDefault(require("./getPageActions"));
10
10
  var _getParagraphFromText = _interopRequireDefault(require("./getParagraphFromText"));
11
11
  var _useComponent = _interopRequireDefault(require("./useComponent"));
12
+ var _models = require("../../models");
12
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
15
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -32,6 +33,12 @@ var getFormPage = function getFormPage(pageOptions, formComponents, formData) {
32
33
  return (0, _getParagraphFromText.default)(componentOptions);
33
34
  }
34
35
  var ret;
36
+ if (componentOptions.type === _models.ComponentTypes.CONTAINER) {
37
+ // eslint-disable-next-line no-param-reassign
38
+ componentOptions.components = componentOptions.components.map(function (cmp) {
39
+ return cmp.use ? (0, _useComponent.default)(cmp, formComponents) : cmp;
40
+ });
41
+ }
35
42
  if (componentOptions.use) {
36
43
  ret = (0, _useComponent.default)(componentOptions, formComponents);
37
44
  } else {
@@ -53,6 +53,17 @@ var getComponentToUse = function getComponentToUse(toUse, formComponents) {
53
53
  } else {
54
54
  wrapper = component;
55
55
  }
56
+
57
+ // this allows us to have the 'use: component' functionality inside item array
58
+ if (component.item && Array.isArray(component.item)) {
59
+ component.item = component.item.map(function (item) {
60
+ if (item.use) {
61
+ // eslint-disable-next-line no-use-before-define
62
+ return useComponent(item, formComponents);
63
+ }
64
+ return item;
65
+ });
66
+ }
56
67
  return wrapper;
57
68
  }
58
69
 
@@ -175,6 +175,42 @@ describe('utils', function () {
175
175
  label: TO_USE.label
176
176
  }));
177
177
  });
178
+ it('should handle nested use references within item', function () {
179
+ var SEARCH_METHOD = {
180
+ id: 'searchMethod',
181
+ label: 'Search Method',
182
+ type: _models.ComponentTypes.TEXT
183
+ };
184
+ var SEARCH_TECHNIQUES = {
185
+ id: 'searchTechniques',
186
+ label: 'Search Techniques',
187
+ type: _models.ComponentTypes.CONTAINER,
188
+ item: [{
189
+ use: 'searchMethod',
190
+ data: {
191
+ url: 'https://api.example.com/searchtype'
192
+ }
193
+ }]
194
+ };
195
+ var EXTENDED_FORM_COMPONENTS = [].concat(FORM_COMPONENTS, [SEARCH_METHOD, SEARCH_TECHNIQUES]);
196
+ var TO_USE = {
197
+ use: 'searchTechniques'
198
+ };
199
+ var expectedComponent = _objectSpread(_objectSpread({
200
+ use: 'searchTechniques'
201
+ }, SEARCH_TECHNIQUES), {}, {
202
+ cya_label: SEARCH_TECHNIQUES.label,
203
+ item: [_objectSpread(_objectSpread({
204
+ use: 'searchMethod'
205
+ }, SEARCH_METHOD), {}, {
206
+ cya_label: SEARCH_METHOD.label,
207
+ data: {
208
+ url: 'https://api.example.com/searchtype'
209
+ }
210
+ })]
211
+ });
212
+ expect((0, _useComponent.default)(TO_USE, EXTENDED_FORM_COMPONENTS)).toEqual(expectedComponent);
213
+ });
178
214
  });
179
215
  });
180
216
  });
@@ -7,6 +7,7 @@ exports.default = void 0;
7
7
  // eslint-disable-next-line no-control-regex
8
8
  // const EMAIL_REGEX = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i;
9
9
  var HODS_EMAIL_REGEX = /^[a-z0-9._\-']+@(digital\.)?homeoffice.gov.uk$/i;
10
+ var JMSC_EMAIL_REGEX = /^[a-z0-9._\-']+@jmsc.gov.uk$/i;
10
11
 
11
12
  /**
12
13
  * Validates an email address, ensuring it is in the correct domain and
@@ -26,7 +27,7 @@ var validateEmail = function validateEmail(value) {
26
27
  if (!!value) {
27
28
  var name = label ? label.toLowerCase() : 'email address';
28
29
  if (typeof value === 'string') {
29
- if (HODS_EMAIL_REGEX.test(value)) {
30
+ if (HODS_EMAIL_REGEX.test(value) || JMSC_EMAIL_REGEX.test(value)) {
30
31
  return undefined;
31
32
  }
32
33
  }
@@ -14,6 +14,9 @@ describe('utils', function () {
14
14
  it('should return no error when the value is a valid .gov.uk address', function () {
15
15
  expect((0, _validateEmail.default)('alpha@homeoffice.gov.uk', LABEL)).toBeUndefined();
16
16
  });
17
+ it('should return no error when the value is a valid jmsc.gov.uk address', function () {
18
+ expect((0, _validateEmail.default)('alpha@jmsc.gov.uk', LABEL)).toBeUndefined();
19
+ });
17
20
  it('should return no error when the value is a capitalised digital.homeoffice.gov.uk address', function () {
18
21
  expect((0, _validateEmail.default)('ALPHA.BRAVO@DIGITAL.HOMEOFFICE.GOV.UK', LABEL)).toBeUndefined();
19
22
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ukhomeoffice/cop-react-form-renderer",
3
- "version": "6.12.1",
3
+ "version": "6.14.0",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "clean": "rimraf dist",
@@ -13,10 +13,12 @@
13
13
  "storybook": "yarn storybook:start",
14
14
  "deploy-storybook": "storybook-to-ghpages -e storybook-static",
15
15
  "compile": "yarn clean && cross-env NODE_ENV=production babel src --out-dir dist --copy-files && yarn post-compile",
16
- "post-compile": "rimraf dist/*.test.* dist/**/*.test.* dist/**/*.stories.* dist/docs dist/assets"
16
+ "post-compile": "rimraf dist/*.test.* dist/**/*.test.* dist/**/*.stories.* dist/docs dist/assets",
17
+ "compile-with-maps": "yarn clean && cross-env NODE_ENV=production babel src --out-dir dist --source-maps true --copy-files && yarn post-compile",
18
+ "yalc-publish": "yarn compile-with-maps && cp -r src dist/src && yalc publish --push"
17
19
  },
18
20
  "dependencies": {
19
- "@ukhomeoffice/cop-react-components": "4.7.2",
21
+ "@ukhomeoffice/cop-react-components": "4.7.3",
20
22
  "axios": "^0.23.0",
21
23
  "dayjs": "^1.11.0",
22
24
  "govuk-frontend": "^5.0.0",