@plone/volto 18.10.1 → 18.11.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 (110) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +3 -3
  3. package/locales/ca/LC_MESSAGES/volto.po +46 -0
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +46 -0
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +46 -0
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +46 -0
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +46 -0
  12. package/locales/eu.json +1 -1
  13. package/locales/fi/LC_MESSAGES/volto.po +46 -0
  14. package/locales/fi.json +1 -1
  15. package/locales/fr/LC_MESSAGES/volto.po +46 -0
  16. package/locales/fr.json +1 -1
  17. package/locales/hi/LC_MESSAGES/volto.po +46 -0
  18. package/locales/hi.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +46 -0
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +46 -0
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +46 -0
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +46 -0
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +47 -1
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +46 -0
  30. package/locales/ro.json +1 -1
  31. package/locales/ru/LC_MESSAGES/volto.po +5243 -0
  32. package/locales/ru.json +1 -0
  33. package/locales/volto.pot +47 -1
  34. package/locales/zh_CN/LC_MESSAGES/volto.po +46 -0
  35. package/locales/zh_CN.json +1 -1
  36. package/package.json +4 -4
  37. package/razzle.config.js +1 -1
  38. package/src/components/index.js +2 -0
  39. package/src/components/manage/Blocks/Block/BlocksForm.jsx +5 -1
  40. package/src/components/manage/Blocks/Search/components/SearchInput.jsx +11 -1
  41. package/src/components/manage/DragDropList/DragDropList.jsx +78 -33
  42. package/src/components/manage/Form/Field.jsx +38 -29
  43. package/src/components/manage/Form/Form.jsx +141 -69
  44. package/src/components/manage/Form/ModalForm.jsx +29 -0
  45. package/src/components/manage/Sidebar/ObjectBrowserNav.jsx +1 -1
  46. package/src/components/manage/Sidebar/ObjectBrowserNav.test.jsx +34 -13
  47. package/src/components/manage/Widgets/CheckboxGroupWidget.jsx +214 -0
  48. package/src/components/manage/Widgets/CheckboxGroupWidget.stories.jsx +39 -0
  49. package/src/components/manage/Widgets/CheckboxGroupWidget.test.jsx +45 -0
  50. package/src/components/manage/Widgets/FileWidget.jsx +43 -2
  51. package/src/components/manage/Widgets/HiddenWidget.jsx +114 -0
  52. package/src/components/manage/Widgets/HiddenWidget.stories.jsx +20 -0
  53. package/src/components/manage/Widgets/HiddenWidget.test.jsx +32 -0
  54. package/src/components/manage/Widgets/ImageWidget.jsx +3 -0
  55. package/src/components/manage/Widgets/RadioGroupWidget.jsx +207 -0
  56. package/src/components/manage/Widgets/RadioGroupWidget.stories.jsx +39 -0
  57. package/src/components/manage/Widgets/RadioGroupWidget.test.jsx +46 -0
  58. package/src/components/manage/Widgets/SchemaWidget.jsx +806 -320
  59. package/src/components/manage/Widgets/SelectWidget.jsx +38 -2
  60. package/src/components/manage/Widgets/StaticTextWidget.jsx +34 -0
  61. package/src/components/manage/Widgets/StaticTextWidget.stories.jsx +20 -0
  62. package/src/components/manage/Widgets/StaticTextWidget.test.jsx +25 -0
  63. package/src/components/manage/Widgets/TimeWidget.jsx +96 -0
  64. package/src/components/manage/Widgets/TimeWidget.stories.jsx +22 -0
  65. package/src/components/manage/Widgets/TimeWidget.test.jsx +35 -0
  66. package/src/components/manage/Widgets/index.tsx +35 -0
  67. package/src/components/theme/ContactForm/ContactForm.jsx +7 -4
  68. package/src/components/theme/Widgets/HiddenWidget.jsx +2 -0
  69. package/src/components/theme/Widgets/HiddenWidget.stories.jsx +25 -0
  70. package/src/components/theme/Widgets/HiddenWidget.test.jsx +11 -0
  71. package/src/components/theme/Widgets/StaticTextWidget.jsx +16 -0
  72. package/src/components/theme/Widgets/StaticTextWidget.stories.jsx +29 -0
  73. package/src/components/theme/Widgets/StaticTextWidget.test.jsx +32 -0
  74. package/src/config/Blocks.jsx +2 -0
  75. package/src/config/Widgets.jsx +14 -0
  76. package/src/constants/Languages.cjs +1 -0
  77. package/src/helpers/FormValidation/FormValidation.jsx +4 -0
  78. package/src/helpers/FormValidation/FormValidation.test.js +147 -31
  79. package/src/helpers/FormValidation/validators.ts +4 -1
  80. package/src/helpers/Utils/Utils.jsx +14 -2
  81. package/theme/themes/pastanaga/collections/form.overrides +4 -0
  82. package/theme/themes/pastanaga/elements/input.overrides +7 -0
  83. package/theme/themes/pastanaga/extras/sidebar.less +2 -0
  84. package/types/components/index.d.ts +1 -1
  85. package/types/components/manage/Widgets/CheckboxGroupWidget.d.ts +6 -0
  86. package/types/components/manage/Widgets/CheckboxGroupWidget.stories.d.ts +15 -0
  87. package/types/components/manage/Widgets/CheckboxGroupWidget.test.d.ts +1 -0
  88. package/types/components/manage/Widgets/HiddenWidget.d.ts +54 -0
  89. package/types/components/manage/Widgets/HiddenWidget.stories.d.ts +9 -0
  90. package/types/components/manage/Widgets/HiddenWidget.test.d.ts +1 -0
  91. package/types/components/manage/Widgets/RadioGroupWidget.d.ts +6 -0
  92. package/types/components/manage/Widgets/RadioGroupWidget.stories.d.ts +15 -0
  93. package/types/components/manage/Widgets/RadioGroupWidget.test.d.ts +1 -0
  94. package/types/components/manage/Widgets/StaticTextWidget.d.ts +18 -0
  95. package/types/components/manage/Widgets/StaticTextWidget.stories.d.ts +9 -0
  96. package/types/components/manage/Widgets/StaticTextWidget.test.d.ts +1 -0
  97. package/types/components/manage/Widgets/TimeWidget.d.ts +2 -0
  98. package/types/components/manage/Widgets/TimeWidget.stories.d.ts +8 -0
  99. package/types/components/manage/Widgets/TimeWidget.test.d.ts +1 -0
  100. package/types/components/manage/Widgets/index.d.ts +5 -0
  101. package/types/components/theme/Widgets/HiddenWidget.d.ts +6 -0
  102. package/types/components/theme/Widgets/HiddenWidget.stories.d.ts +8 -0
  103. package/types/components/theme/Widgets/HiddenWidget.test.d.ts +1 -0
  104. package/types/components/theme/Widgets/StaticTextWidget.d.ts +5 -0
  105. package/types/components/theme/Widgets/StaticTextWidget.stories.d.ts +8 -0
  106. package/types/components/theme/Widgets/StaticTextWidget.test.d.ts +1 -0
  107. package/types/config/Widgets.d.ts +14 -0
  108. package/types/constants/Languages.d.cts +1 -0
  109. package/types/helpers/FormValidation/validators.d.ts +1 -1
  110. package/types/helpers/Utils/Utils.d.ts +1 -1
@@ -13,41 +13,46 @@ const MODE_HIDDEN = 'hidden'; //hidden mode. If mode is hidden, field is not ren
13
13
  /**
14
14
  * Get default widget
15
15
  * @method getViewDefault
16
+ * @param {object} widgets Widgets config
16
17
  * @returns {string} Widget component.
17
18
  */
18
- const getWidgetDefault = () => config.widgets.default;
19
+ const getWidgetDefault = (widgets) => widgets.default;
19
20
 
20
21
  /**
21
22
  * Get widget by field's `id` attribute
22
23
  * @method getWidgetById
24
+ * @param {object} widgets Widgets config
23
25
  * @param {string} id Id
24
26
  * @returns {string} Widget component.
25
27
  */
26
- const getWidgetByFieldId = (id) => config.widgets.id[id] || null;
28
+ const getWidgetByFieldId = (widgets, id) => widgets.id[id] || null;
27
29
 
28
30
  /**
29
31
  * Get widget by factory attribute
30
32
  * @method getWidgetByFactory
33
+ * @param {object} widgets Widgets config
31
34
  * @param {string} id Id
32
35
  * @returns {string} Widget component.
33
36
  */
34
- const getWidgetByFactory = (factory) =>
35
- config.widgets.factory?.[factory] || null;
37
+ const getWidgetByFactory = (widgets, factory) =>
38
+ widgets.factory?.[factory] || null;
36
39
 
37
40
  /**
38
41
  * Get widget by field's `widget` attribute
39
42
  * @method getWidgetByName
43
+ * @param {object} widgets Widgets config
40
44
  * @param {string} widget Widget
41
45
  * @returns {string} Widget component.
42
46
  */
43
- const getWidgetByName = (widget) =>
47
+ const getWidgetByName = (widgets, widget) =>
44
48
  typeof widget === 'string'
45
- ? config.widgets.widget[widget] || getWidgetDefault()
49
+ ? widgets.widget[widget] || getWidgetDefault(widgets)
46
50
  : null;
47
51
 
48
52
  /**
49
53
  * Get widget by tagged values
50
54
  * @param {object} widgetOptions
55
+ * @param {object} widgets Widgets config
51
56
  * @returns {string} Widget component.
52
57
  *
53
58
 
@@ -59,13 +64,14 @@ directives.widget(
59
64
  })
60
65
 
61
66
  */
62
- const getWidgetFromTaggedValues = (widgetOptions) =>
67
+ const getWidgetFromTaggedValues = (widgets, widgetOptions) =>
63
68
  typeof widgetOptions?.frontendOptions?.widget === 'string'
64
- ? config.widgets.widget[widgetOptions.frontendOptions.widget]
69
+ ? widgets.widget[widgetOptions.frontendOptions.widget]
65
70
  : null;
66
71
 
67
72
  /**
68
73
  * Get widget props from tagged values
74
+ * @param {object} widgets Widgets config
69
75
  * @param {object} widgetOptions
70
76
  * @returns {string} Widget component.
71
77
  *
@@ -78,7 +84,7 @@ directives.widget(
78
84
  })
79
85
 
80
86
  */
81
- const getWidgetPropsFromTaggedValues = (widgetOptions) =>
87
+ const getWidgetPropsFromTaggedValues = (widgets, widgetOptions) =>
82
88
  typeof widgetOptions?.frontendOptions?.widgetProps === 'object'
83
89
  ? widgetOptions.frontendOptions.widgetProps
84
90
  : null;
@@ -86,25 +92,25 @@ const getWidgetPropsFromTaggedValues = (widgetOptions) =>
86
92
  /**
87
93
  * Get widget by field's `vocabulary` attribute
88
94
  * @method getWidgetByVocabulary
95
+ * @param {object} widgets Widgets config
89
96
  * @param {string} vocabulary Widget
90
97
  * @returns {string} Widget component.
91
98
  */
92
- const getWidgetByVocabulary = (vocabulary) =>
99
+ const getWidgetByVocabulary = (widgets, vocabulary) =>
93
100
  vocabulary && vocabulary['@id']
94
- ? config.widgets.vocabulary[
95
- vocabulary['@id'].replace(/^.*\/@vocabularies\//, '')
96
- ]
101
+ ? widgets.vocabulary[vocabulary['@id'].replace(/^.*\/@vocabularies\//, '')]
97
102
  : null;
98
103
 
99
104
  /**
100
105
  * Get widget by field's hints `vocabulary` attribute in widgetOptions
101
106
  * @method getWidgetByVocabularyFromHint
107
+ * @param {object} widgets Widgets config
102
108
  * @param {string} props Widget props
103
109
  * @returns {string} Widget component.
104
110
  */
105
- const getWidgetByVocabularyFromHint = (props) =>
111
+ const getWidgetByVocabularyFromHint = (widgets, props) =>
106
112
  props.widgetOptions && props.widgetOptions.vocabulary
107
- ? config.widgets.vocabulary[
113
+ ? widgets.vocabulary[
108
114
  props.widgetOptions.vocabulary['@id'].replace(
109
115
  /^.*\/@vocabularies\//,
110
116
  '',
@@ -115,19 +121,20 @@ const getWidgetByVocabularyFromHint = (props) =>
115
121
  /**
116
122
  * Get widget by field's `choices` attribute
117
123
  * @method getWidgetByChoices
124
+ * @param {object} widgets Widgets config
118
125
  * @param {string} choices Widget
119
126
  * @returns {string} Widget component.
120
127
  */
121
- const getWidgetByChoices = (props) => {
128
+ const getWidgetByChoices = (widgets, props) => {
122
129
  if (props.choices) {
123
- return config.widgets.choices;
130
+ return widgets.choices;
124
131
  }
125
132
 
126
133
  if (props.vocabulary) {
127
134
  // If vocabulary exists, then it means it's a choice field in disguise with
128
135
  // no widget specified that probably contains a string then we force it
129
136
  // to be a select widget instead
130
- return config.widgets.choices;
137
+ return widgets.choices;
131
138
  }
132
139
 
133
140
  return null;
@@ -136,10 +143,11 @@ const getWidgetByChoices = (props) => {
136
143
  /**
137
144
  * Get widget by field's `type` attribute
138
145
  * @method getWidgetByType
146
+ * @param {object} widgets Widgets config
139
147
  * @param {string} type Type
140
148
  * @returns {string} Widget component.
141
149
  */
142
- const getWidgetByType = (type) => config.widgets.type[type] || null;
150
+ const getWidgetByType = (widgets, type) => widgets.type[type] || null;
143
151
 
144
152
  /**
145
153
  * Field component class.
@@ -148,16 +156,17 @@ const getWidgetByType = (type) => config.widgets.type[type] || null;
148
156
  * @returns {string} Markup of the component.
149
157
  */
150
158
  const UnconnectedField = (props, { intl }) => {
159
+ const widgets = props.widgets || config.widgets;
151
160
  const Widget =
152
- getWidgetByFieldId(props.id) ||
153
- getWidgetFromTaggedValues(props.widgetOptions) ||
154
- getWidgetByName(props.widget) ||
155
- getWidgetByChoices(props) ||
156
- getWidgetByVocabulary(props.vocabulary) ||
157
- getWidgetByVocabularyFromHint(props) ||
158
- getWidgetByFactory(props.factory) ||
159
- getWidgetByType(props.type) ||
160
- getWidgetDefault();
161
+ getWidgetByFieldId(widgets, props.id) ||
162
+ getWidgetFromTaggedValues(widgets, props.widgetOptions) ||
163
+ getWidgetByName(widgets, props.widget) ||
164
+ getWidgetByChoices(widgets, props) ||
165
+ getWidgetByVocabulary(widgets, props.vocabulary) ||
166
+ getWidgetByVocabularyFromHint(widgets, props) ||
167
+ getWidgetByFactory(widgets, props.factory) ||
168
+ getWidgetByType(widgets, props.type) ||
169
+ getWidgetDefault(widgets);
161
170
 
162
171
  if (props.mode === MODE_HIDDEN) {
163
172
  return null;
@@ -166,7 +175,7 @@ const UnconnectedField = (props, { intl }) => {
166
175
  // Adding the widget props from tagged values (if any)
167
176
  const widgetProps = {
168
177
  ...props,
169
- ...getWidgetPropsFromTaggedValues(props.widgetOptions),
178
+ ...getWidgetPropsFromTaggedValues(widgets, props.widgetOptions),
170
179
  };
171
180
 
172
181
  if (props.onOrder) {
@@ -15,6 +15,7 @@ import {
15
15
  getBlocksLayoutFieldname,
16
16
  hasBlocksData,
17
17
  } from '@plone/volto/helpers/Blocks/Blocks';
18
+ import { applySchemaEnhancer } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
18
19
  import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels';
19
20
  import aheadSVG from '@plone/volto/icons/ahead.svg';
20
21
  import clearSVG from '@plone/volto/icons/clear.svg';
@@ -81,6 +82,8 @@ class Form extends Component {
81
82
  definitions: PropTypes.objectOf(PropTypes.any),
82
83
  required: PropTypes.arrayOf(PropTypes.string),
83
84
  }),
85
+ widgets: PropTypes.objectOf(PropTypes.any),
86
+ component: PropTypes.any,
84
87
  formData: PropTypes.objectOf(PropTypes.any),
85
88
  globalData: PropTypes.objectOf(PropTypes.any),
86
89
  metadataFieldsets: PropTypes.arrayOf(PropTypes.string),
@@ -89,6 +92,9 @@ class Form extends Component {
89
92
  onSubmit: PropTypes.func,
90
93
  onCancel: PropTypes.func,
91
94
  submitLabel: PropTypes.string,
95
+ cancelLabel: PropTypes.string,
96
+ textButtons: PropTypes.bool,
97
+ buttonComponent: PropTypes.any,
92
98
  resetAfterSubmit: PropTypes.bool,
93
99
  resetOnCancel: PropTypes.bool,
94
100
  isEditForm: PropTypes.bool,
@@ -119,9 +125,14 @@ class Form extends Component {
119
125
  */
120
126
  static defaultProps = {
121
127
  formData: null,
128
+ widgets: null,
129
+ component: null,
122
130
  onSubmit: null,
123
131
  onCancel: null,
124
132
  submitLabel: null,
133
+ cancelLabel: null,
134
+ textButtons: false,
135
+ buttonComponent: null,
125
136
  resetAfterSubmit: false,
126
137
  resetOnCancel: false,
127
138
  isEditForm: false,
@@ -272,22 +283,19 @@ class Form extends Component {
272
283
  selected: null,
273
284
  });
274
285
  }
275
- if (requestError) {
286
+
287
+ if (requestError && prevProps.requestError !== requestError) {
276
288
  errors =
277
289
  FormValidation.giveServerErrorsToCorrespondingFields(requestError);
278
- if (
279
- !isEqual(prevProps.requestError, requestError) ||
280
- !isEqual(this.state.errors, errors)
281
- ) {
282
- activeIndex = FormValidation.showFirstTabWithErrors({
283
- errors,
284
- schema: this.props.schema,
285
- });
286
- this.setState({
287
- errors,
288
- activeIndex,
289
- });
290
- }
290
+ activeIndex = FormValidation.showFirstTabWithErrors({
291
+ errors,
292
+ schema: this.props.schema,
293
+ });
294
+
295
+ this.setState({
296
+ errors,
297
+ activeIndex,
298
+ });
291
299
  }
292
300
 
293
301
  if (this.props.onChangeFormData) {
@@ -551,6 +559,18 @@ class Form extends Component {
551
559
  formData: blocks[block],
552
560
  });
553
561
  }
562
+
563
+ if (config.blocks.blocksConfig[blocks[block]['@type']].blockSchema) {
564
+ blockSchema = applySchemaEnhancer({
565
+ schema: blockSchema,
566
+ formData: blocks[block],
567
+ intl: this.props.intl,
568
+ blocksConfig: config.blocks.blocksConfig,
569
+ navRoot: this.props.navRoot,
570
+ contentType: this.props.content['@type'],
571
+ });
572
+ }
573
+
554
574
  const blockErrors = FormValidation.validateFieldsPerFieldset({
555
575
  schema: blockSchema,
556
576
  formData: blocks[block],
@@ -564,11 +584,13 @@ class Form extends Component {
564
584
  }
565
585
  });
566
586
  }
587
+
567
588
  if (keys(errors).length > 0 || keys(blocksErrors).length > 0) {
568
589
  const activeIndex = FormValidation.showFirstTabWithErrors({
569
590
  errors,
570
591
  schema: this.props.schema,
571
592
  });
593
+
572
594
  this.setState({
573
595
  errors: {
574
596
  ...errors,
@@ -579,23 +601,14 @@ class Form extends Component {
579
601
 
580
602
  if (keys(errors).length > 0) {
581
603
  // Changes the focus to the metadata tab in the sidebar if error
582
- toast.error(
583
- <Toast
584
- error
585
- title={this.props.intl.formatMessage(messages.error)}
586
- content={
587
- <ul>
588
- {Object.keys(errors).map((err, index) => (
589
- <li key={index}>
590
- <strong>
591
- {this.props.schema.properties[err].title || err}:
592
- </strong>{' '}
593
- {errors[err]}
594
- </li>
595
- ))}
596
- </ul>
597
- }
598
- />,
604
+ Object.keys(errors).forEach((err) =>
605
+ toast.error(
606
+ <Toast
607
+ error
608
+ title={this.props.schema.properties[err].title || err}
609
+ content={errors[err].join(', ')}
610
+ />,
611
+ ),
599
612
  );
600
613
  this.props.setSidebarTab(0);
601
614
  } else if (keys(blocksErrors).length > 0) {
@@ -605,12 +618,14 @@ class Form extends Component {
605
618
  const errorMessage = Object.entries(
606
619
  Object.entries(blocksErrors)[0][1],
607
620
  )[0][1];
621
+ const errorFieldTitle = errorMessage.title || errorField;
622
+
608
623
  toast.error(
609
624
  <Toast
610
625
  error
611
626
  title={this.props.intl.formatMessage(
612
627
  messages.blocksFieldsErrorTitle,
613
- { errorField },
628
+ { errorField: errorFieldTitle },
614
629
  )}
615
630
  content={errorMessage}
616
631
  />,
@@ -717,11 +732,16 @@ class Form extends Component {
717
732
  navRoot,
718
733
  type,
719
734
  metadataFieldsets,
735
+ component,
736
+ buttonComponent,
720
737
  } = this.props;
721
738
  const formData = this.state.formData;
722
739
  const schema = this.removeBlocksLayoutFields(originalSchema);
723
740
  const Container =
724
741
  config.getComponent({ name: 'Container' }).component || SemanticContainer;
742
+ const FormComponent = component || UiForm;
743
+ const ButtonComponent = buttonComponent || Button;
744
+
725
745
  return this.props.visual ? (
726
746
  // Removing this from SSR is important, since react-beautiful-dnd supports SSR,
727
747
  // but draftJS don't like it much and the hydration gets messed up
@@ -885,7 +905,7 @@ class Form extends Component {
885
905
  )
886
906
  ) : (
887
907
  <Container>
888
- <UiForm
908
+ <FormComponent
889
909
  method="post"
890
910
  onSubmit={this.onSubmit}
891
911
  error={keys(this.state.errors).length > 0}
@@ -927,6 +947,7 @@ class Form extends Component {
927
947
  ),
928
948
  ...map(item.fields, (field, index) => (
929
949
  <Field
950
+ widgets={this.props.widgets}
930
951
  {...schema.properties[field]}
931
952
  id={field}
932
953
  formData={formData}
@@ -982,6 +1003,7 @@ class Form extends Component {
982
1003
  )}
983
1004
  {map(schema.fieldsets[0].fields, (field) => (
984
1005
  <Field
1006
+ widgets={this.props.widgets}
985
1007
  {...schema.properties[field]}
986
1008
  id={field}
987
1009
  value={formData?.[field]}
@@ -997,47 +1019,97 @@ class Form extends Component {
997
1019
  )}
998
1020
  {!this.props.hideActions && (
999
1021
  <Segment className="actions" clearing>
1000
- {onSubmit && (
1001
- <Button
1002
- basic
1003
- primary
1004
- floated="right"
1005
- type="submit"
1006
- aria-label={
1007
- this.props.submitLabel
1022
+ {onSubmit &&
1023
+ (this.props.textButtons ? (
1024
+ <ButtonComponent
1025
+ primary
1026
+ floated="right"
1027
+ type="submit"
1028
+ aria-label={
1029
+ this.props.submitLabel
1030
+ ? this.props.submitLabel
1031
+ : this.props.intl.formatMessage(messages.save)
1032
+ }
1033
+ title={
1034
+ this.props.submitLabel
1035
+ ? this.props.submitLabel
1036
+ : this.props.intl.formatMessage(messages.save)
1037
+ }
1038
+ loading={this.props.loading}
1039
+ >
1040
+ {this.props.submitLabel
1008
1041
  ? this.props.submitLabel
1009
- : this.props.intl.formatMessage(messages.save)
1010
- }
1011
- title={
1012
- this.props.submitLabel
1013
- ? this.props.submitLabel
1014
- : this.props.intl.formatMessage(messages.save)
1015
- }
1016
- loading={this.props.loading}
1017
- >
1018
- <Icon className="circled" name={aheadSVG} size="30px" />
1019
- </Button>
1020
- )}
1021
- {onCancel && (
1022
- <Button
1023
- type="button"
1024
- basic
1025
- secondary
1026
- aria-label={this.props.intl.formatMessage(
1027
- messages.cancel,
1028
- )}
1029
- title={this.props.intl.formatMessage(messages.cancel)}
1030
- floated="right"
1031
- onClick={this.onCancel}
1032
- >
1033
- <Icon className="circled" name={clearSVG} size="30px" />
1034
- </Button>
1035
- )}
1042
+ : this.props.intl.formatMessage(messages.save)}
1043
+ </ButtonComponent>
1044
+ ) : (
1045
+ <ButtonComponent
1046
+ basic
1047
+ primary
1048
+ floated="right"
1049
+ type="submit"
1050
+ aria-label={
1051
+ this.props.submitLabel
1052
+ ? this.props.submitLabel
1053
+ : this.props.intl.formatMessage(messages.save)
1054
+ }
1055
+ title={
1056
+ this.props.submitLabel
1057
+ ? this.props.submitLabel
1058
+ : this.props.intl.formatMessage(messages.save)
1059
+ }
1060
+ loading={this.props.loading}
1061
+ >
1062
+ <Icon className="circled" name={aheadSVG} size="30px" />
1063
+ </ButtonComponent>
1064
+ ))}
1065
+ {onCancel &&
1066
+ (this.props.textButtons ? (
1067
+ <ButtonComponent
1068
+ secondary
1069
+ type="button"
1070
+ aria-label={
1071
+ this.props.cancelLabel
1072
+ ? this.props.cancelLabel
1073
+ : this.props.intl.formatMessage(messages.cancel)
1074
+ }
1075
+ title={
1076
+ this.props.cancelLabel
1077
+ ? this.props.cancelLabel
1078
+ : this.props.intl.formatMessage(messages.cancel)
1079
+ }
1080
+ floated="right"
1081
+ onClick={this.onCancel}
1082
+ >
1083
+ {this.props.cancelLabel
1084
+ ? this.props.cancelLabel
1085
+ : this.props.intl.formatMessage(messages.cancel)}
1086
+ </ButtonComponent>
1087
+ ) : (
1088
+ <ButtonComponent
1089
+ basic
1090
+ secondary
1091
+ type="button"
1092
+ aria-label={
1093
+ this.props.cancelLabel
1094
+ ? this.props.cancelLabel
1095
+ : this.props.intl.formatMessage(messages.cancel)
1096
+ }
1097
+ title={
1098
+ this.props.cancelLabel
1099
+ ? this.props.cancelLabel
1100
+ : this.props.intl.formatMessage(messages.cancel)
1101
+ }
1102
+ floated="right"
1103
+ onClick={this.onCancel}
1104
+ >
1105
+ <Icon className="circled" name={clearSVG} size="30px" />
1106
+ </ButtonComponent>
1107
+ ))}
1036
1108
  </Segment>
1037
1109
  )}
1038
1110
  </Segment.Group>
1039
1111
  </fieldset>
1040
- </UiForm>
1112
+ </FormComponent>
1041
1113
  </Container>
1042
1114
  );
1043
1115
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import React, { Component } from 'react';
7
7
  import PropTypes from 'prop-types';
8
+ import isEqual from 'lodash/isEqual';
8
9
  import keys from 'lodash/keys';
9
10
  import map from 'lodash/map';
10
11
  import {
@@ -76,6 +77,7 @@ class ModalForm extends Component {
76
77
  submitError: PropTypes.string,
77
78
  onSubmit: PropTypes.func.isRequired,
78
79
  onCancel: PropTypes.func,
80
+ onChangeFormData: PropTypes.func,
79
81
  open: PropTypes.bool,
80
82
  submitLabel: PropTypes.string,
81
83
  loading: PropTypes.bool,
@@ -207,6 +209,33 @@ class ModalForm extends Component {
207
209
  });
208
210
  }
209
211
 
212
+ /**
213
+ * Component did update lifecycle handler
214
+ * @param {Object} prevProps
215
+ * @param {Object} prevState
216
+ */
217
+ async componentDidUpdate(prevProps, prevState) {
218
+ if (this.props.onChangeFormData) {
219
+ if (!isEqual(prevState?.formData, this.state.formData)) {
220
+ this.props.onChangeFormData(this.state.formData);
221
+ }
222
+ }
223
+ if (!isEqual(prevProps.formData, this.props.formData)) {
224
+ let newFormData = {};
225
+ map(keys(this.props.formData), (field) => {
226
+ if (!isEqual(prevProps.formData[field], this.props.formData[field])) {
227
+ newFormData[field] = this.props.formData[field];
228
+ }
229
+ });
230
+ this.setState({
231
+ formData: {
232
+ ...this.state.formData,
233
+ ...newFormData,
234
+ },
235
+ });
236
+ }
237
+ }
238
+
210
239
  /**
211
240
  * Render method.
212
241
  * @method render
@@ -34,7 +34,7 @@ const ObjectBrowserNav = ({
34
34
  const intl = useIntl();
35
35
  const isSelected = (item) => {
36
36
  let ret = false;
37
- if (selected) {
37
+ if (selected && Array.isArray(selected)) {
38
38
  selected
39
39
  .filter((item) => item != null)
40
40
  .forEach((_item) => {
@@ -99,23 +99,44 @@ const currentSearchResults = {
99
99
  ],
100
100
  };
101
101
 
102
- test('renders a view image component', () => {
102
+ describe('ObjectBrowserNav', () => {
103
103
  const store = mockStore({
104
104
  intl: {
105
105
  locale: 'en',
106
106
  messages: {},
107
107
  },
108
108
  });
109
- const component = renderer.create(
110
- <Provider store={store}>
111
- <ObjectBrowserNav
112
- currentSearchResults={currentSearchResults}
113
- isSelectable={() => {
114
- return true;
115
- }}
116
- />
117
- </Provider>,
118
- );
119
- const json = component.toJSON();
120
- expect(json).toMatchSnapshot();
109
+
110
+ const baseProps = {
111
+ currentSearchResults,
112
+ isSelectable: () => true,
113
+ handleClickOnItem: jest.fn(),
114
+ handleDoubleClickOnItem: jest.fn(),
115
+ mode: 'link',
116
+ view: 'list',
117
+ navigateTo: jest.fn(),
118
+ };
119
+
120
+ it('renders a view image component', () => {
121
+ const component = renderer.create(
122
+ <Provider store={store}>
123
+ <ObjectBrowserNav {...baseProps} />
124
+ </Provider>,
125
+ );
126
+ expect(component.toJSON()).toMatchSnapshot();
127
+ });
128
+
129
+ it('handles selected as empty object without errors', () => {
130
+ const component = renderer.create(
131
+ <Provider store={store}>
132
+ <ObjectBrowserNav
133
+ {...baseProps}
134
+ selected={{}} // Empty object being tested
135
+ />
136
+ </Provider>,
137
+ );
138
+
139
+ const json = component.toJSON();
140
+ expect(json).toMatchSnapshot();
141
+ });
121
142
  });