decap-cms-widget-list 2.11.0-beta.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.
- package/CHANGELOG.md +556 -0
- package/LICENSE +22 -0
- package/README.md +9 -0
- package/dist/decap-cms-widget-list.js +7 -0
- package/dist/decap-cms-widget-list.js.LICENSE.txt +23 -0
- package/dist/decap-cms-widget-list.js.map +1 -0
- package/dist/esm/ListControl.js +676 -0
- package/dist/esm/index.js +32 -0
- package/dist/esm/schema.js +35 -0
- package/dist/esm/typedListHelpers.js +44 -0
- package/package.json +39 -0
- package/src/ListControl.js +680 -0
- package/src/__tests__/ListControl.spec.js +792 -0
- package/src/__tests__/__snapshots__/ListControl.spec.js.snap +2213 -0
- package/src/index.js +19 -0
- package/src/schema.js +12 -0
- package/src/typedListHelpers.js +35 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
4
|
+
import styled from '@emotion/styled';
|
|
5
|
+
import { css, ClassNames } from '@emotion/core';
|
|
6
|
+
import { List, Map, fromJS } from 'immutable';
|
|
7
|
+
import { partial, isEmpty, uniqueId } from 'lodash';
|
|
8
|
+
import uuid from 'uuid/v4';
|
|
9
|
+
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
|
10
|
+
import DecapCmsWidgetObject from 'decap-cms-widget-object';
|
|
11
|
+
import {
|
|
12
|
+
ListItemTopBar,
|
|
13
|
+
ObjectWidgetTopBar,
|
|
14
|
+
colors,
|
|
15
|
+
lengths,
|
|
16
|
+
FieldLabel,
|
|
17
|
+
} from 'decap-cms-ui-default';
|
|
18
|
+
import { stringTemplate, validations } from 'decap-cms-lib-widgets';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
TYPES_KEY,
|
|
22
|
+
getTypedFieldForValue,
|
|
23
|
+
resolveFieldKeyType,
|
|
24
|
+
getErrorMessageForTypedFieldAndValue,
|
|
25
|
+
} from './typedListHelpers';
|
|
26
|
+
|
|
27
|
+
const ObjectControl = DecapCmsWidgetObject.controlComponent;
|
|
28
|
+
|
|
29
|
+
const ListItem = styled.div();
|
|
30
|
+
|
|
31
|
+
const SortableListItem = SortableElement(ListItem);
|
|
32
|
+
|
|
33
|
+
const StyledListItemTopBar = styled(ListItemTopBar)`
|
|
34
|
+
background-color: ${colors.textFieldBorder};
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const NestedObjectLabel = styled.div`
|
|
38
|
+
display: ${props => (props.collapsed ? 'block' : 'none')};
|
|
39
|
+
border-top: 0;
|
|
40
|
+
color: ${props => (props.error ? colors.errorText : 'inherit')};
|
|
41
|
+
background-color: ${colors.textFieldBorder};
|
|
42
|
+
padding: 13px;
|
|
43
|
+
border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const styleStrings = {
|
|
47
|
+
collapsedObjectControl: `
|
|
48
|
+
display: none;
|
|
49
|
+
`,
|
|
50
|
+
objectWidgetTopBarContainer: `
|
|
51
|
+
padding: ${lengths.objectWidgetTopBarContainerPadding};
|
|
52
|
+
`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const styles = {
|
|
56
|
+
listControlItem: css`
|
|
57
|
+
margin-top: 18px;
|
|
58
|
+
|
|
59
|
+
&:first-of-type {
|
|
60
|
+
margin-top: 26px;
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
listControlItemCollapsed: css`
|
|
64
|
+
padding-bottom: 0;
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const SortableList = SortableContainer(({ items, renderItem }) => {
|
|
69
|
+
return <div>{items.map(renderItem)}</div>;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const valueTypes = {
|
|
73
|
+
SINGLE: 'SINGLE',
|
|
74
|
+
MULTIPLE: 'MULTIPLE',
|
|
75
|
+
MIXED: 'MIXED',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function handleSummary(summary, entry, label, item) {
|
|
79
|
+
const data = stringTemplate.addFileTemplateFields(
|
|
80
|
+
entry.get('path'),
|
|
81
|
+
item.set('fields.label', label),
|
|
82
|
+
);
|
|
83
|
+
return stringTemplate.compileStringTemplate(summary, null, '', data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateItem(field, item) {
|
|
87
|
+
if (!Map.isMap(item)) {
|
|
88
|
+
console.warn(
|
|
89
|
+
`'${field.get('name')}' field item value value should be a map but is a '${typeof item}'`,
|
|
90
|
+
);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function LabelComponent({ field, isActive, hasErrors, uniqueFieldId, isFieldOptional, t }) {
|
|
97
|
+
const label = `${field.get('label', field.get('name'))}`;
|
|
98
|
+
return (
|
|
99
|
+
<FieldLabel isActive={isActive} hasErrors={hasErrors} htmlFor={uniqueFieldId}>
|
|
100
|
+
{label} {`${isFieldOptional ? ` (${t('editor.editorControl.field.optional')})` : ''}`}
|
|
101
|
+
</FieldLabel>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default class ListControl extends React.Component {
|
|
106
|
+
validations = [];
|
|
107
|
+
|
|
108
|
+
static propTypes = {
|
|
109
|
+
metadata: ImmutablePropTypes.map,
|
|
110
|
+
onChange: PropTypes.func.isRequired,
|
|
111
|
+
onChangeObject: PropTypes.func.isRequired,
|
|
112
|
+
onValidateObject: PropTypes.func.isRequired,
|
|
113
|
+
validate: PropTypes.func.isRequired,
|
|
114
|
+
value: ImmutablePropTypes.list,
|
|
115
|
+
field: PropTypes.object,
|
|
116
|
+
forID: PropTypes.string,
|
|
117
|
+
controlRef: PropTypes.func,
|
|
118
|
+
mediaPaths: ImmutablePropTypes.map.isRequired,
|
|
119
|
+
getAsset: PropTypes.func.isRequired,
|
|
120
|
+
onOpenMediaLibrary: PropTypes.func.isRequired,
|
|
121
|
+
onAddAsset: PropTypes.func.isRequired,
|
|
122
|
+
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
|
123
|
+
classNameWrapper: PropTypes.string.isRequired,
|
|
124
|
+
setActiveStyle: PropTypes.func.isRequired,
|
|
125
|
+
setInactiveStyle: PropTypes.func.isRequired,
|
|
126
|
+
editorControl: PropTypes.elementType.isRequired,
|
|
127
|
+
resolveWidget: PropTypes.func.isRequired,
|
|
128
|
+
clearFieldErrors: PropTypes.func.isRequired,
|
|
129
|
+
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
|
130
|
+
entry: ImmutablePropTypes.map.isRequired,
|
|
131
|
+
t: PropTypes.func,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
static defaultProps = {
|
|
135
|
+
value: List(),
|
|
136
|
+
parentIds: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
constructor(props) {
|
|
140
|
+
super(props);
|
|
141
|
+
const { field, value } = props;
|
|
142
|
+
const listCollapsed = field.get('collapsed', true);
|
|
143
|
+
const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
|
|
144
|
+
const keys = (value && Array.from({ length: value.size }, () => uuid())) || [];
|
|
145
|
+
|
|
146
|
+
this.state = {
|
|
147
|
+
listCollapsed,
|
|
148
|
+
itemsCollapsed,
|
|
149
|
+
value: this.valueToString(value),
|
|
150
|
+
keys,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
valueToString = value => {
|
|
155
|
+
let stringValue;
|
|
156
|
+
if (List.isList(value) || Array.isArray(value)) {
|
|
157
|
+
stringValue = value.join(',');
|
|
158
|
+
} else {
|
|
159
|
+
console.warn(
|
|
160
|
+
`Expected List value to be an array but received '${value}' with type of '${typeof value}'. Please check the value provided to the '${this.props.field.get(
|
|
161
|
+
'name',
|
|
162
|
+
)}' field`,
|
|
163
|
+
);
|
|
164
|
+
stringValue = String(value);
|
|
165
|
+
}
|
|
166
|
+
return stringValue.replace(/,([^\s]|$)/g, ', $1');
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
getValueType = () => {
|
|
170
|
+
const { field } = this.props;
|
|
171
|
+
if (field.get('fields')) {
|
|
172
|
+
return valueTypes.MULTIPLE;
|
|
173
|
+
} else if (field.get('field')) {
|
|
174
|
+
return valueTypes.SINGLE;
|
|
175
|
+
} else if (field.get(TYPES_KEY)) {
|
|
176
|
+
return valueTypes.MIXED;
|
|
177
|
+
} else {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`);
|
|
183
|
+
/**
|
|
184
|
+
* Always update so that each nested widget has the option to update. This is
|
|
185
|
+
* required because ControlHOC provides a default `shouldComponentUpdate`
|
|
186
|
+
* which only updates if the value changes, but every widget must be allowed
|
|
187
|
+
* to override this.
|
|
188
|
+
*/
|
|
189
|
+
shouldComponentUpdate() {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
handleChange = e => {
|
|
194
|
+
const { onChange } = this.props;
|
|
195
|
+
const oldValue = this.state.value;
|
|
196
|
+
const newValue = e.target.value.trim();
|
|
197
|
+
const listValue = newValue ? newValue.split(',') : [];
|
|
198
|
+
if (newValue.match(/,$/) && oldValue.match(/, $/)) {
|
|
199
|
+
listValue.pop();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const parsedValue = this.valueToString(listValue);
|
|
203
|
+
this.setState({ value: parsedValue });
|
|
204
|
+
onChange(List(listValue.map(val => val.trim())));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
handleFocus = () => {
|
|
208
|
+
this.props.setActiveStyle();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
handleBlur = e => {
|
|
212
|
+
const listValue = e.target.value
|
|
213
|
+
.split(',')
|
|
214
|
+
.map(el => el.trim())
|
|
215
|
+
.filter(el => el);
|
|
216
|
+
this.setState({ value: this.valueToString(listValue) });
|
|
217
|
+
this.props.setInactiveStyle();
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
handleAdd = e => {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
const { field } = this.props;
|
|
223
|
+
const parsedValue =
|
|
224
|
+
this.getValueType() === valueTypes.SINGLE
|
|
225
|
+
? this.singleDefault()
|
|
226
|
+
: fromJS(this.multipleDefault(field.get('fields')));
|
|
227
|
+
this.addItem(parsedValue);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
singleDefault = () => {
|
|
231
|
+
return this.props.field.getIn(['field', 'default'], null);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
multipleDefault = fields => {
|
|
235
|
+
return this.getFieldsDefault(fields);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
handleAddType = (type, typeKey) => {
|
|
239
|
+
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
|
240
|
+
this.addItem(parsedValue);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
mixedDefault = (typeKey, type) => {
|
|
244
|
+
const selectedType = this.props.field.get(TYPES_KEY).find(f => f.get('name') === type);
|
|
245
|
+
const fields = selectedType.get('fields') || [selectedType.get('field')];
|
|
246
|
+
|
|
247
|
+
return this.getFieldsDefault(fields, { [typeKey]: type });
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
getFieldsDefault = (fields, initialValue = {}) => {
|
|
251
|
+
return fields.reduce((acc, item) => {
|
|
252
|
+
const subfields = item.get('field') || item.get('fields');
|
|
253
|
+
const object = item.get('widget') == 'object';
|
|
254
|
+
const name = item.get('name');
|
|
255
|
+
const defaultValue = item.get('default', null);
|
|
256
|
+
|
|
257
|
+
if (List.isList(subfields) && object) {
|
|
258
|
+
const subDefaultValue = this.getFieldsDefault(subfields);
|
|
259
|
+
!isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
|
|
260
|
+
return acc;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (Map.isMap(subfields) && object) {
|
|
264
|
+
const subDefaultValue = this.getFieldsDefault([subfields]);
|
|
265
|
+
!isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
|
|
266
|
+
return acc;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (defaultValue !== null) {
|
|
270
|
+
acc[name] = defaultValue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return acc;
|
|
274
|
+
}, initialValue);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
addItem = parsedValue => {
|
|
278
|
+
const { value, onChange, field } = this.props;
|
|
279
|
+
const addToTop = field.get('add_to_top', false);
|
|
280
|
+
|
|
281
|
+
const itemKey = uuid();
|
|
282
|
+
this.setState({
|
|
283
|
+
itemsCollapsed: addToTop
|
|
284
|
+
? [false, ...this.state.itemsCollapsed]
|
|
285
|
+
: [...this.state.itemsCollapsed, false],
|
|
286
|
+
keys: addToTop ? [itemKey, ...this.state.keys] : [...this.state.keys, itemKey],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const listValue = value || List();
|
|
290
|
+
if (addToTop) {
|
|
291
|
+
onChange(listValue.unshift(parsedValue));
|
|
292
|
+
} else {
|
|
293
|
+
onChange(listValue.push(parsedValue));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
processControlRef = ref => {
|
|
298
|
+
if (!ref) return;
|
|
299
|
+
const {
|
|
300
|
+
validate,
|
|
301
|
+
props: { validationKey: key },
|
|
302
|
+
} = ref;
|
|
303
|
+
this.validations.push({ key, validate });
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
validate = () => {
|
|
307
|
+
if (this.getValueType()) {
|
|
308
|
+
this.validations.forEach(item => {
|
|
309
|
+
item.validate();
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
this.props.validate();
|
|
313
|
+
}
|
|
314
|
+
this.props.onValidateObject(this.props.forID, this.validateSize());
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
validateSize = () => {
|
|
318
|
+
const { field, value, t } = this.props;
|
|
319
|
+
const min = field.get('min');
|
|
320
|
+
const max = field.get('max');
|
|
321
|
+
const required = field.get('required', true);
|
|
322
|
+
|
|
323
|
+
if (!required && !value?.size) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const error = validations.validateMinMax(
|
|
328
|
+
t,
|
|
329
|
+
field.get('label', field.get('name')),
|
|
330
|
+
value,
|
|
331
|
+
min,
|
|
332
|
+
max,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return error ? [error] : [];
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* In case the `onChangeObject` function is frozen by a child widget implementation,
|
|
340
|
+
* e.g. when debounced, always get the latest object value instead of using
|
|
341
|
+
* `this.props.value` directly.
|
|
342
|
+
*/
|
|
343
|
+
getObjectValue = idx => this.props.value.get(idx) || Map();
|
|
344
|
+
|
|
345
|
+
handleChangeFor(index) {
|
|
346
|
+
return (f, newValue, newMetadata) => {
|
|
347
|
+
const { value, metadata, onChange, field } = this.props;
|
|
348
|
+
const collectionName = field.get('name');
|
|
349
|
+
const listFieldObjectWidget = field.getIn(['field', 'widget']) === 'object';
|
|
350
|
+
const withNameKey =
|
|
351
|
+
this.getValueType() !== valueTypes.SINGLE ||
|
|
352
|
+
(this.getValueType() === valueTypes.SINGLE && listFieldObjectWidget);
|
|
353
|
+
const newObjectValue = withNameKey
|
|
354
|
+
? this.getObjectValue(index).set(f.get('name'), newValue)
|
|
355
|
+
: newValue;
|
|
356
|
+
const parsedMetadata = {
|
|
357
|
+
[collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata || {}),
|
|
358
|
+
};
|
|
359
|
+
onChange(value.set(index, newObjectValue), parsedMetadata);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
handleRemove = (index, event) => {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
const { itemsCollapsed } = this.state;
|
|
366
|
+
const { value, metadata, onChange, field, clearFieldErrors } = this.props;
|
|
367
|
+
const collectionName = field.get('name');
|
|
368
|
+
const isSingleField = this.getValueType() === valueTypes.SINGLE;
|
|
369
|
+
|
|
370
|
+
const metadataRemovePath = isSingleField ? value.get(index) : value.get(index).valueSeq();
|
|
371
|
+
const parsedMetadata =
|
|
372
|
+
metadata && !metadata.isEmpty()
|
|
373
|
+
? { [collectionName]: metadata.removeIn(metadataRemovePath) }
|
|
374
|
+
: metadata;
|
|
375
|
+
|
|
376
|
+
itemsCollapsed.splice(index, 1);
|
|
377
|
+
// clear validations
|
|
378
|
+
this.validations = [];
|
|
379
|
+
|
|
380
|
+
this.setState({
|
|
381
|
+
itemsCollapsed: [...itemsCollapsed],
|
|
382
|
+
keys: Array.from({ length: value.size - 1 }, () => uuid()),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
onChange(value.remove(index), parsedMetadata);
|
|
386
|
+
clearFieldErrors();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
handleItemCollapseToggle = (index, event) => {
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
const { itemsCollapsed } = this.state;
|
|
392
|
+
const newItemsCollapsed = itemsCollapsed.map((collapsed, itemIndex) => {
|
|
393
|
+
if (index === itemIndex) {
|
|
394
|
+
return !collapsed;
|
|
395
|
+
}
|
|
396
|
+
return collapsed;
|
|
397
|
+
});
|
|
398
|
+
this.setState({
|
|
399
|
+
itemsCollapsed: newItemsCollapsed,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
handleCollapseAllToggle = e => {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
const { value, field } = this.props;
|
|
406
|
+
const { itemsCollapsed, listCollapsed } = this.state;
|
|
407
|
+
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
|
408
|
+
const listCollapsedByDefault = field.get('collapsed', true);
|
|
409
|
+
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
|
410
|
+
|
|
411
|
+
if (minimizeCollapsedItems) {
|
|
412
|
+
let updatedItemsCollapsed = itemsCollapsed;
|
|
413
|
+
// Only allow collapsing all items in this mode but not opening all at once
|
|
414
|
+
if (!listCollapsed || !listCollapsedByDefault) {
|
|
415
|
+
updatedItemsCollapsed = Array(value.size).fill(!listCollapsed);
|
|
416
|
+
}
|
|
417
|
+
this.setState({ listCollapsed: !listCollapsed, itemsCollapsed: updatedItemsCollapsed });
|
|
418
|
+
} else {
|
|
419
|
+
this.setState({ itemsCollapsed: Array(value.size).fill(!allItemsCollapsed) });
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
objectLabel(item) {
|
|
424
|
+
const { field, entry } = this.props;
|
|
425
|
+
const valueType = this.getValueType();
|
|
426
|
+
switch (valueType) {
|
|
427
|
+
case valueTypes.MIXED: {
|
|
428
|
+
if (!validateItem(field, item)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const itemType = getTypedFieldForValue(field, item);
|
|
432
|
+
const label = itemType.get('label', itemType.get('name'));
|
|
433
|
+
// each type can have its own summary, but default to the list summary if exists
|
|
434
|
+
const summary = itemType.get('summary', field.get('summary'));
|
|
435
|
+
const labelReturn = summary ? handleSummary(summary, entry, label, item) : label;
|
|
436
|
+
return labelReturn;
|
|
437
|
+
}
|
|
438
|
+
case valueTypes.SINGLE: {
|
|
439
|
+
const singleField = field.get('field');
|
|
440
|
+
const label = singleField.get('label', singleField.get('name'));
|
|
441
|
+
const summary = field.get('summary');
|
|
442
|
+
const data = fromJS({ [singleField.get('name')]: item });
|
|
443
|
+
const labelReturn = summary ? handleSummary(summary, entry, label, data) : label;
|
|
444
|
+
return labelReturn;
|
|
445
|
+
}
|
|
446
|
+
case valueTypes.MULTIPLE: {
|
|
447
|
+
if (!validateItem(field, item)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const multiFields = field.get('fields');
|
|
451
|
+
const labelField = multiFields && multiFields.first();
|
|
452
|
+
const value = item.get(labelField.get('name'));
|
|
453
|
+
const summary = field.get('summary');
|
|
454
|
+
const labelReturn = summary ? handleSummary(summary, entry, value, item) : value;
|
|
455
|
+
return (labelReturn || `No ${labelField.get('name')}`).toString();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return '';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
onSortEnd = ({ oldIndex, newIndex }) => {
|
|
462
|
+
const { value, clearFieldErrors } = this.props;
|
|
463
|
+
const { itemsCollapsed, keys } = this.state;
|
|
464
|
+
|
|
465
|
+
// Update value
|
|
466
|
+
const item = value.get(oldIndex);
|
|
467
|
+
const newValue = value.delete(oldIndex).insert(newIndex, item);
|
|
468
|
+
this.props.onChange(newValue);
|
|
469
|
+
|
|
470
|
+
// Update collapsing
|
|
471
|
+
const collapsed = itemsCollapsed[oldIndex];
|
|
472
|
+
itemsCollapsed.splice(oldIndex, 1);
|
|
473
|
+
const updatedItemsCollapsed = [...itemsCollapsed];
|
|
474
|
+
updatedItemsCollapsed.splice(newIndex, 0, collapsed);
|
|
475
|
+
|
|
476
|
+
// Reset item to ensure updated state
|
|
477
|
+
const updatedKeys = keys.map((key, keyIndex) => {
|
|
478
|
+
if (keyIndex === oldIndex || keyIndex === newIndex) {
|
|
479
|
+
return uuid();
|
|
480
|
+
}
|
|
481
|
+
return key;
|
|
482
|
+
});
|
|
483
|
+
this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys });
|
|
484
|
+
|
|
485
|
+
//clear error fields and remove old validations
|
|
486
|
+
clearFieldErrors();
|
|
487
|
+
this.validations = this.validations.filter(item => updatedKeys.includes(item.key));
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
hasError = index => {
|
|
491
|
+
const { fieldsErrors } = this.props;
|
|
492
|
+
if (fieldsErrors && fieldsErrors.size > 0) {
|
|
493
|
+
return Object.values(fieldsErrors.toJS()).some(arr =>
|
|
494
|
+
arr.some(err => err.parentIds && err.parentIds.includes(this.state.keys[index])),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// eslint-disable-next-line react/display-name
|
|
500
|
+
renderItem = (item, index) => {
|
|
501
|
+
const {
|
|
502
|
+
classNameWrapper,
|
|
503
|
+
editorControl,
|
|
504
|
+
onValidateObject,
|
|
505
|
+
metadata,
|
|
506
|
+
clearFieldErrors,
|
|
507
|
+
fieldsErrors,
|
|
508
|
+
controlRef,
|
|
509
|
+
resolveWidget,
|
|
510
|
+
parentIds,
|
|
511
|
+
forID,
|
|
512
|
+
t,
|
|
513
|
+
} = this.props;
|
|
514
|
+
|
|
515
|
+
const { itemsCollapsed, keys } = this.state;
|
|
516
|
+
const collapsed = itemsCollapsed[index];
|
|
517
|
+
const key = keys[index];
|
|
518
|
+
let field = this.props.field;
|
|
519
|
+
const hasError = this.hasError(index);
|
|
520
|
+
const isVariableTypesList = this.getValueType() === valueTypes.MIXED;
|
|
521
|
+
if (isVariableTypesList) {
|
|
522
|
+
field = getTypedFieldForValue(field, item);
|
|
523
|
+
if (!field) {
|
|
524
|
+
return this.renderErroneousTypedItem(index, item);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return (
|
|
528
|
+
<SortableListItem
|
|
529
|
+
css={[styles.listControlItem, collapsed && styles.listControlItemCollapsed]}
|
|
530
|
+
index={index}
|
|
531
|
+
key={key}
|
|
532
|
+
>
|
|
533
|
+
{isVariableTypesList && (
|
|
534
|
+
<LabelComponent
|
|
535
|
+
field={field}
|
|
536
|
+
isActive={false}
|
|
537
|
+
hasErrors={hasError}
|
|
538
|
+
uniqueFieldId={this.uniqueFieldId}
|
|
539
|
+
isFieldOptional={field.get('required') === false}
|
|
540
|
+
t={t}
|
|
541
|
+
/>
|
|
542
|
+
)}
|
|
543
|
+
<StyledListItemTopBar
|
|
544
|
+
collapsed={collapsed}
|
|
545
|
+
onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
|
|
546
|
+
onRemove={partial(this.handleRemove, index)}
|
|
547
|
+
dragHandleHOC={SortableHandle}
|
|
548
|
+
data-testid={`styled-list-item-top-bar-${key}`}
|
|
549
|
+
/>
|
|
550
|
+
<NestedObjectLabel collapsed={collapsed} error={hasError}>
|
|
551
|
+
{this.objectLabel(item)}
|
|
552
|
+
</NestedObjectLabel>
|
|
553
|
+
<ClassNames>
|
|
554
|
+
{({ css, cx }) => (
|
|
555
|
+
<ObjectControl
|
|
556
|
+
classNameWrapper={cx(classNameWrapper, {
|
|
557
|
+
[css`
|
|
558
|
+
${styleStrings.collapsedObjectControl};
|
|
559
|
+
`]: collapsed,
|
|
560
|
+
})}
|
|
561
|
+
value={item}
|
|
562
|
+
field={field}
|
|
563
|
+
onChangeObject={this.handleChangeFor(index)}
|
|
564
|
+
editorControl={editorControl}
|
|
565
|
+
resolveWidget={resolveWidget}
|
|
566
|
+
metadata={metadata}
|
|
567
|
+
forList
|
|
568
|
+
onValidateObject={onValidateObject}
|
|
569
|
+
clearFieldErrors={clearFieldErrors}
|
|
570
|
+
fieldsErrors={fieldsErrors}
|
|
571
|
+
ref={this.processControlRef}
|
|
572
|
+
controlRef={controlRef}
|
|
573
|
+
validationKey={key}
|
|
574
|
+
collapsed={collapsed}
|
|
575
|
+
data-testid={`object-control-${key}`}
|
|
576
|
+
hasError={hasError}
|
|
577
|
+
parentIds={[...parentIds, forID, key]}
|
|
578
|
+
/>
|
|
579
|
+
)}
|
|
580
|
+
</ClassNames>
|
|
581
|
+
</SortableListItem>
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
renderErroneousTypedItem(index, item) {
|
|
586
|
+
const field = this.props.field;
|
|
587
|
+
const errorMessage = getErrorMessageForTypedFieldAndValue(field, item);
|
|
588
|
+
const key = `item-${index}`;
|
|
589
|
+
return (
|
|
590
|
+
<SortableListItem
|
|
591
|
+
css={[styles.listControlItem, styles.listControlItemCollapsed]}
|
|
592
|
+
index={index}
|
|
593
|
+
key={key}
|
|
594
|
+
>
|
|
595
|
+
<StyledListItemTopBar
|
|
596
|
+
onCollapseToggle={null}
|
|
597
|
+
onRemove={partial(this.handleRemove, index, key)}
|
|
598
|
+
dragHandleHOC={SortableHandle}
|
|
599
|
+
/>
|
|
600
|
+
<NestedObjectLabel collapsed={true} error={true}>
|
|
601
|
+
{errorMessage}
|
|
602
|
+
</NestedObjectLabel>
|
|
603
|
+
</SortableListItem>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
renderListControl() {
|
|
608
|
+
const { value, forID, field, classNameWrapper, t } = this.props;
|
|
609
|
+
const { itemsCollapsed, listCollapsed } = this.state;
|
|
610
|
+
const items = value || List();
|
|
611
|
+
const label = field.get('label', field.get('name'));
|
|
612
|
+
const labelSingular = field.get('label_singular') || field.get('label', field.get('name'));
|
|
613
|
+
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
|
|
614
|
+
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
|
615
|
+
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
|
616
|
+
const selfCollapsed = allItemsCollapsed && (listCollapsed || !minimizeCollapsedItems);
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<ClassNames>
|
|
620
|
+
{({ cx, css }) => (
|
|
621
|
+
<div
|
|
622
|
+
id={forID}
|
|
623
|
+
className={cx(
|
|
624
|
+
classNameWrapper,
|
|
625
|
+
css`
|
|
626
|
+
${styleStrings.objectWidgetTopBarContainer}
|
|
627
|
+
`,
|
|
628
|
+
)}
|
|
629
|
+
>
|
|
630
|
+
<ObjectWidgetTopBar
|
|
631
|
+
allowAdd={field.get('allow_add', true)}
|
|
632
|
+
onAdd={this.handleAdd}
|
|
633
|
+
types={field.get(TYPES_KEY, null)}
|
|
634
|
+
onAddType={type => this.handleAddType(type, resolveFieldKeyType(field))}
|
|
635
|
+
heading={`${items.size} ${listLabel}`}
|
|
636
|
+
label={labelSingular.toLowerCase()}
|
|
637
|
+
onCollapseToggle={this.handleCollapseAllToggle}
|
|
638
|
+
collapsed={selfCollapsed}
|
|
639
|
+
t={t}
|
|
640
|
+
/>
|
|
641
|
+
{(!selfCollapsed || !minimizeCollapsedItems) && (
|
|
642
|
+
<SortableList
|
|
643
|
+
items={items}
|
|
644
|
+
renderItem={this.renderItem}
|
|
645
|
+
onSortEnd={this.onSortEnd}
|
|
646
|
+
useDragHandle
|
|
647
|
+
lockAxis="y"
|
|
648
|
+
/>
|
|
649
|
+
)}
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
652
|
+
</ClassNames>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
renderInput() {
|
|
657
|
+
const { forID, classNameWrapper } = this.props;
|
|
658
|
+
const { value } = this.state;
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<input
|
|
662
|
+
type="text"
|
|
663
|
+
id={forID}
|
|
664
|
+
value={value}
|
|
665
|
+
onChange={this.handleChange}
|
|
666
|
+
onFocus={this.handleFocus}
|
|
667
|
+
onBlur={this.handleBlur}
|
|
668
|
+
className={classNameWrapper}
|
|
669
|
+
/>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
render() {
|
|
674
|
+
if (this.getValueType() !== null) {
|
|
675
|
+
return this.renderListControl();
|
|
676
|
+
} else {
|
|
677
|
+
return this.renderInput();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|