decap-cms-widget-list 3.5.0 → 3.6.1
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 +13 -0
- package/dist/decap-cms-widget-list.js +4 -4
- package/dist/decap-cms-widget-list.js.LICENSE.txt +19 -0
- package/dist/decap-cms-widget-list.js.map +1 -1
- package/dist/esm/ListControl.js +137 -112
- package/package.json +19 -15
- package/src/ListControl.js +18 -7
- package/src/__tests__/ListControl.spec.js +74 -4
- package/src/__tests__/__snapshots__/ListControl.spec.js.snap +9 -0
- package/LICENSE +0 -22
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-widget-list",
|
|
3
3
|
"description": "Widget for editing lists in Decap CMS.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.6.1",
|
|
5
5
|
"homepage": "https://www.decapcms.org/docs/widgets/#list",
|
|
6
|
-
"repository":
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list"
|
|
9
|
+
},
|
|
7
10
|
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
8
11
|
"module": "dist/esm/index.js",
|
|
9
12
|
"main": "dist/decap-cms-widget-list.js",
|
|
@@ -15,27 +18,28 @@
|
|
|
15
18
|
"object"
|
|
16
19
|
],
|
|
17
20
|
"sideEffects": false,
|
|
18
|
-
"scripts": {
|
|
19
|
-
"develop": "npm run build:esm -- --watch",
|
|
20
|
-
"build": "cross-env NODE_ENV=production webpack",
|
|
21
|
-
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
|
22
|
-
},
|
|
23
21
|
"dependencies": {
|
|
24
22
|
"@dnd-kit/core": "^6.0.8",
|
|
25
23
|
"@dnd-kit/modifiers": "^6.0.1",
|
|
26
|
-
"@dnd-kit/sortable": "^7.0.2"
|
|
24
|
+
"@dnd-kit/sortable": "^7.0.2",
|
|
25
|
+
"@dnd-kit/utilities": "^3.2.2"
|
|
27
26
|
},
|
|
28
27
|
"peerDependencies": {
|
|
29
28
|
"@emotion/react": "^11.11.1",
|
|
30
29
|
"@emotion/styled": "^11.11.0",
|
|
31
|
-
"
|
|
32
|
-
"decap-cms-ui-default": "^3.0.0",
|
|
33
|
-
"decap-cms-widget-object": "^3.0.0",
|
|
34
|
-
"immutable": "^3.7.6",
|
|
30
|
+
"immutable": "^4.3.9",
|
|
35
31
|
"lodash": "^4.17.11",
|
|
36
32
|
"prop-types": "^15.7.2",
|
|
37
33
|
"react": "^19.1.0",
|
|
38
|
-
"react-immutable-proptypes": "^2.1.0"
|
|
34
|
+
"react-immutable-proptypes": "^2.1.0",
|
|
35
|
+
"uuid": "^8.3.2",
|
|
36
|
+
"decap-cms-lib-widgets": "3.4.1",
|
|
37
|
+
"decap-cms-ui-default": "3.9.1",
|
|
38
|
+
"decap-cms-widget-object": "3.7.1"
|
|
39
39
|
},
|
|
40
|
-
"
|
|
41
|
-
|
|
40
|
+
"scripts": {
|
|
41
|
+
"develop": "pnpm run build:esm --watch",
|
|
42
|
+
"build": "cross-env NODE_ENV=production webpack",
|
|
43
|
+
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/ListControl.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Component } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
4
4
|
import styled from '@emotion/styled';
|
|
@@ -7,7 +7,6 @@ import { List, Map, fromJS } from 'immutable';
|
|
|
7
7
|
import partial from 'lodash/partial';
|
|
8
8
|
import isEmpty from 'lodash/isEmpty';
|
|
9
9
|
import uniqueId from 'lodash/uniqueId';
|
|
10
|
-
import { v4 as uuid } from 'uuid';
|
|
11
10
|
import DecapCmsWidgetObject from 'decap-cms-widget-object';
|
|
12
11
|
import {
|
|
13
12
|
DndContext,
|
|
@@ -172,7 +171,7 @@ function LabelComponent({ field, isActive, hasErrors, uniqueFieldId, isFieldOpti
|
|
|
172
171
|
);
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
export default class ListControl extends
|
|
174
|
+
export default class ListControl extends Component {
|
|
176
175
|
childRefs = {};
|
|
177
176
|
|
|
178
177
|
static propTypes = {
|
|
@@ -211,7 +210,7 @@ export default class ListControl extends React.Component {
|
|
|
211
210
|
const { field, value } = props;
|
|
212
211
|
const listCollapsed = field.get('collapsed', true);
|
|
213
212
|
const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
|
|
214
|
-
const keys = (value && Array.from({ length: value.size }, () =>
|
|
213
|
+
const keys = (value && Array.from({ length: value.size }, () => crypto.randomUUID())) || [];
|
|
215
214
|
|
|
216
215
|
this.state = {
|
|
217
216
|
listCollapsed,
|
|
@@ -353,7 +352,7 @@ export default class ListControl extends React.Component {
|
|
|
353
352
|
const { value, onChange, field } = this.props;
|
|
354
353
|
const addToTop = field.get('add_to_top', false);
|
|
355
354
|
|
|
356
|
-
const itemKey =
|
|
355
|
+
const itemKey = crypto.randomUUID();
|
|
357
356
|
this.setState({
|
|
358
357
|
itemsCollapsed: addToTop
|
|
359
358
|
? [false, ...this.state.itemsCollapsed]
|
|
@@ -420,20 +419,32 @@ export default class ListControl extends React.Component {
|
|
|
420
419
|
getObjectValue = idx => this.props.value.get(idx) || Map();
|
|
421
420
|
|
|
422
421
|
handleChangeFor(index) {
|
|
422
|
+
const key = this.state.keys[index];
|
|
423
|
+
|
|
423
424
|
return (f, newValue, newMetadata) => {
|
|
424
425
|
const { value, metadata, onChange, field } = this.props;
|
|
426
|
+
|
|
427
|
+
// Resolve the item's position when the change fires rather than when this
|
|
428
|
+
// handler was created. If an item has been removed or moved in between,
|
|
429
|
+
// `index` now points at a different item, or past the end of the list.
|
|
430
|
+
const currentIndex = this.state.keys.indexOf(key);
|
|
431
|
+
|
|
432
|
+
if (currentIndex === -1) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
425
436
|
const collectionName = field.get('name');
|
|
426
437
|
const listFieldObjectWidget = field.getIn(['field', 'widget']) === 'object';
|
|
427
438
|
const withNameKey =
|
|
428
439
|
this.getValueType() !== valueTypes.SINGLE ||
|
|
429
440
|
(this.getValueType() === valueTypes.SINGLE && listFieldObjectWidget);
|
|
430
441
|
const newObjectValue = withNameKey
|
|
431
|
-
? this.getObjectValue(
|
|
442
|
+
? this.getObjectValue(currentIndex).set(f.get('name'), newValue)
|
|
432
443
|
: newValue;
|
|
433
444
|
const parsedMetadata = {
|
|
434
445
|
[collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata || {}),
|
|
435
446
|
};
|
|
436
|
-
onChange(value.set(
|
|
447
|
+
onChange(value.set(currentIndex, newObjectValue), parsedMetadata);
|
|
437
448
|
};
|
|
438
449
|
}
|
|
439
450
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import { fireEvent, render } from '@testing-library/react';
|
|
3
2
|
import { fromJS } from 'immutable';
|
|
4
3
|
|
|
@@ -34,7 +33,6 @@ jest.mock('decap-cms-ui-default', () => {
|
|
|
34
33
|
ListItemTopBar,
|
|
35
34
|
};
|
|
36
35
|
});
|
|
37
|
-
jest.mock('uuid');
|
|
38
36
|
|
|
39
37
|
describe('ListControl', () => {
|
|
40
38
|
const props = {
|
|
@@ -63,9 +61,8 @@ describe('ListControl', () => {
|
|
|
63
61
|
|
|
64
62
|
beforeEach(() => {
|
|
65
63
|
jest.clearAllMocks();
|
|
66
|
-
const uuid = require('uuid');
|
|
67
64
|
let id = 0;
|
|
68
|
-
|
|
65
|
+
global.crypto.randomUUID = jest.fn(() => {
|
|
69
66
|
return id++;
|
|
70
67
|
});
|
|
71
68
|
});
|
|
@@ -789,4 +786,77 @@ describe('ListControl', () => {
|
|
|
789
786
|
listControl.validate();
|
|
790
787
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
|
|
791
788
|
});
|
|
789
|
+
|
|
790
|
+
const typedField = fromJS({
|
|
791
|
+
name: 'list',
|
|
792
|
+
label: 'List',
|
|
793
|
+
types: [
|
|
794
|
+
{
|
|
795
|
+
name: 'type_one',
|
|
796
|
+
widget: 'object',
|
|
797
|
+
fields: [{ name: 'text', widget: 'markdown', label: 'Text' }],
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should apply a change to the item it was made on after an earlier item is removed', () => {
|
|
803
|
+
const value = fromJS([
|
|
804
|
+
{ type: 'type_one', text: 'one' },
|
|
805
|
+
{ type: 'type_one', text: 'two' },
|
|
806
|
+
{ type: 'type_one', text: 'three' },
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
let control;
|
|
810
|
+
const { getAllByText, rerender } = render(
|
|
811
|
+
<ListControl ref={ref => (control = ref)} {...props} field={typedField} value={value} />,
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const changeThirdItem = control.handleChangeFor(2);
|
|
815
|
+
|
|
816
|
+
fireEvent.click(getAllByText('Remove')[0]);
|
|
817
|
+
const afterRemoval = props.onChange.mock.calls[0][0];
|
|
818
|
+
rerender(
|
|
819
|
+
<ListControl
|
|
820
|
+
ref={ref => (control = ref)}
|
|
821
|
+
{...props}
|
|
822
|
+
field={typedField}
|
|
823
|
+
value={afterRemoval}
|
|
824
|
+
/>,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
changeThirdItem(fromJS({ name: 'text' }), 'three edited');
|
|
828
|
+
|
|
829
|
+
const written = props.onChange.mock.calls[1][0].toJS();
|
|
830
|
+
expect(written).toHaveLength(2);
|
|
831
|
+
expect(written[1]).toEqual({ type: 'type_one', text: 'three edited' });
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should discard a change made on an item that has since been removed', () => {
|
|
835
|
+
const value = fromJS([
|
|
836
|
+
{ type: 'type_one', text: 'one' },
|
|
837
|
+
{ type: 'type_one', text: 'two' },
|
|
838
|
+
]);
|
|
839
|
+
|
|
840
|
+
let control;
|
|
841
|
+
const { getAllByText, rerender } = render(
|
|
842
|
+
<ListControl ref={ref => (control = ref)} {...props} field={typedField} value={value} />,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const changeSecondItem = control.handleChangeFor(1);
|
|
846
|
+
|
|
847
|
+
fireEvent.click(getAllByText('Remove')[1]);
|
|
848
|
+
const afterRemoval = props.onChange.mock.calls[0][0];
|
|
849
|
+
rerender(
|
|
850
|
+
<ListControl
|
|
851
|
+
ref={ref => (control = ref)}
|
|
852
|
+
{...props}
|
|
853
|
+
field={typedField}
|
|
854
|
+
value={afterRemoval}
|
|
855
|
+
/>,
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
changeSecondItem(fromJS({ name: 'text' }), 'text for a removed item');
|
|
859
|
+
|
|
860
|
+
expect(props.onChange).toHaveBeenCalledTimes(1);
|
|
861
|
+
});
|
|
792
862
|
});
|
|
@@ -162,6 +162,7 @@ exports[`ListControl should add to list when add button is clicked 1`] = `
|
|
|
162
162
|
class="emotion-3 emotion-4"
|
|
163
163
|
>
|
|
164
164
|
<button
|
|
165
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
165
166
|
class="emotion-5 emotion-6"
|
|
166
167
|
data-testid="expand-button"
|
|
167
168
|
>
|
|
@@ -414,6 +415,7 @@ exports[`ListControl should remove from list when remove button is clicked 1`] =
|
|
|
414
415
|
class="emotion-3 emotion-4"
|
|
415
416
|
>
|
|
416
417
|
<button
|
|
418
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
417
419
|
class="emotion-5 emotion-6"
|
|
418
420
|
data-testid="expand-button"
|
|
419
421
|
>
|
|
@@ -696,6 +698,7 @@ exports[`ListControl should remove from list when remove button is clicked 2`] =
|
|
|
696
698
|
class="emotion-3 emotion-4"
|
|
697
699
|
>
|
|
698
700
|
<button
|
|
701
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
699
702
|
class="emotion-5 emotion-6"
|
|
700
703
|
data-testid="expand-button"
|
|
701
704
|
>
|
|
@@ -961,6 +964,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|
|
961
964
|
class="emotion-3 emotion-4"
|
|
962
965
|
>
|
|
963
966
|
<button
|
|
967
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
964
968
|
class="emotion-5 emotion-6"
|
|
965
969
|
data-testid="expand-button"
|
|
966
970
|
>
|
|
@@ -1243,6 +1247,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|
|
1243
1247
|
class="emotion-3 emotion-4"
|
|
1244
1248
|
>
|
|
1245
1249
|
<button
|
|
1250
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
1246
1251
|
class="emotion-5 emotion-6"
|
|
1247
1252
|
data-testid="expand-button"
|
|
1248
1253
|
>
|
|
@@ -1525,6 +1530,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|
|
1525
1530
|
class="emotion-3 emotion-4"
|
|
1526
1531
|
>
|
|
1527
1532
|
<button
|
|
1533
|
+
aria-label="editor.editorWidgets.object.expand"
|
|
1528
1534
|
class="emotion-5 emotion-6"
|
|
1529
1535
|
data-testid="expand-button"
|
|
1530
1536
|
>
|
|
@@ -1790,6 +1796,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|
|
1790
1796
|
class="emotion-3 emotion-4"
|
|
1791
1797
|
>
|
|
1792
1798
|
<button
|
|
1799
|
+
aria-label="editor.editorWidgets.object.expand"
|
|
1793
1800
|
class="emotion-5 emotion-6"
|
|
1794
1801
|
data-testid="expand-button"
|
|
1795
1802
|
>
|
|
@@ -1993,6 +2000,7 @@ exports[`ListControl should render list with nested object 1`] = `
|
|
|
1993
2000
|
class="emotion-3 emotion-4"
|
|
1994
2001
|
>
|
|
1995
2002
|
<button
|
|
2003
|
+
aria-label="editor.editorWidgets.object.expand"
|
|
1996
2004
|
class="emotion-5 emotion-6"
|
|
1997
2005
|
data-testid="expand-button"
|
|
1998
2006
|
>
|
|
@@ -2279,6 +2287,7 @@ exports[`ListControl should render list with nested object with collapse = false
|
|
|
2279
2287
|
class="emotion-3 emotion-4"
|
|
2280
2288
|
>
|
|
2281
2289
|
<button
|
|
2290
|
+
aria-label="editor.editorWidgets.object.collapse"
|
|
2282
2291
|
class="emotion-5 emotion-6"
|
|
2283
2292
|
data-testid="expand-button"
|
|
2284
2293
|
>
|
package/LICENSE
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2016 Netlify <decap@p-m.si>
|
|
2
|
-
|
|
3
|
-
MIT License
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
a copy of this software and associated documentation files (the
|
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
-
the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
included in all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|