Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Commit 6476979

Browse files
demshymartinjagodic
authored andcommitted
chore(deps): replace react-sortable-hoc with dnd-kit (decaporg#6860)
* chore: replace react-sortable-hoc with dnd-kit in file control * chore: update yarn.lock * fix: allow multiple images with same path * style: cleanup console logs * chore: replace sortable hoc on list control * test: update snapshots * chore: replace dnd implementation in relation control * chore: add dnd-kit deps to appropriate packages * chore: harmonize sensors and clean up * fix: remove sortable for single relation controls * test: add restaurants entity for dnd implementations on one screen * fix: fix file control when value is not an array or list * refactor: merge listitem with parent, update snapshots * test: update selector in field validation test
1 parent 7126b02 commit 6476979

File tree

13 files changed

+683
-565
lines changed

13 files changed

+683
-565
lines changed

cypress/utils/steps.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ function validateListFields({ name, description }) {
486486
cy.contains('button', 'Save').click();
487487
assertNotification(notifications.error.missingField);
488488
assertFieldErrorStatus('Authors', colorError);
489-
cy.get('div[class*=ListControl]')
489+
cy.get('div[class*=SortableListItem]')
490490
.eq(2)
491491
.as('listControl');
492492
assertFieldErrorStatus('Name', colorError, { scope: cy.get('@listControl') });
@@ -527,7 +527,7 @@ function validateNestedListFields() {
527527
cy.contains('button', 'cities').click();
528528
cy.contains('label', 'Cities')
529529
.next()
530-
.find('div[class*=ListControl]')
530+
.find('div[class*=SortableListItem]')
531531
.eq(2)
532532
.as('secondCitiesListControl');
533533
cy.get('@secondCitiesListControl')
@@ -560,22 +560,22 @@ function validateNestedListFields() {
560560
// list control aliases
561561
cy.contains('label', 'Hotel Locations')
562562
.next()
563-
.find('div[class*=ListControl]')
563+
.find('div[class*=SortableListItem]')
564564
.first()
565565
.as('hotelLocationsListControl');
566566
cy.contains('label', 'Cities')
567567
.next()
568-
.find('div[class*=ListControl]')
568+
.find('div[class*=SortableListItem]')
569569
.eq(0)
570570
.as('firstCitiesListControl');
571571
cy.contains('label', 'City Locations')
572572
.next()
573-
.find('div[class*=ListControl]')
573+
.find('div[class*=SortableListItem]')
574574
.eq(0)
575575
.as('firstCityLocationsListControl');
576576
cy.contains('label', 'Cities')
577577
.next()
578-
.find('div[class*=ListControl]')
578+
.find('div[class*=SortableListItem]')
579579
.eq(3)
580580
.as('secondCityLocationsListControl');
581581

dev-test/config.yml

+23
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ collections: # A list of collections the CMS should be able to edit
5252

5353
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
5454

55+
- name: 'restaurants' # Used in routes, ie.: /admin/collections/:slug/edit
56+
label: 'Restaurants' # Used in the UI
57+
label_singular: 'Restaurant' # Used in the UI, ie: "New Post"
58+
description: >
59+
Restaurants is an entry type used for testing galleries, relations and other widgets.
60+
The tests must be written in such way that adding new fields does not affect previous flows.
61+
folder: '_restaurants'
62+
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
63+
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
64+
create: true # Allow users to create new documents in this collection
65+
fields: # The fields each document in this collection have
66+
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
67+
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
68+
- { name: 'gallery', widget: 'image', choose_url: true, media_library: {config: {multiple: true, max_files: 999}}}
69+
- { name: 'post', widget: relation, collection: posts, multiple: true, search_fields: [ "title" ], display_fields: [ "title" ], value_field: "{{slug}}"}
70+
- name: authors
71+
label: Authors
72+
label_singular: 'Author'
73+
widget: list
74+
fields:
75+
- { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
76+
- { label: 'Description', name: 'description', widget: 'markdown' }
77+
5578
- name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
5679
label: 'FAQ' # Used in the UI
5780
folder: '_faqs'

packages/decap-cms-core/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"react-redux": "^7.2.0",
5757
"react-router-dom": "^5.2.0",
5858
"react-scroll-sync": "^0.9.0",
59-
"react-sortable-hoc": "^2.0.0",
6059
"react-split-pane": "^0.1.85",
6160
"react-toastify": "^9.1.1",
6261
"react-topbar-progress-indicator": "^4.0.0",

packages/decap-cms-ui-default/src/ListItemTopBar.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,26 @@ const DragIconContainer = styled(TopBarButtonSpan)`
3535
cursor: move;
3636
`;
3737

38-
function DragHandle({ dragHandleHOC }) {
39-
const Handle = dragHandleHOC(() => (
40-
<DragIconContainer>
41-
<Icon type="drag-handle" size="small" />
42-
</DragIconContainer>
43-
));
44-
return <Handle />;
38+
function DragHandle({ Wrapper, id }) {
39+
return (
40+
<Wrapper id={id}>
41+
<DragIconContainer>
42+
<Icon type="drag-handle" size="small" />
43+
</DragIconContainer>
44+
</Wrapper>
45+
);
4546
}
4647

47-
function ListItemTopBar({ className, collapsed, onCollapseToggle, onRemove, dragHandleHOC }) {
48+
function ListItemTopBar(props) {
49+
const { className, collapsed, onCollapseToggle, onRemove, dragHandle, id } = props;
4850
return (
4951
<TopBar className={className}>
5052
{onCollapseToggle ? (
5153
<TopBarButton onClick={onCollapseToggle}>
5254
<Icon type="chevron" size="small" direction={collapsed ? 'right' : 'down'} />
5355
</TopBarButton>
5456
) : null}
55-
{dragHandleHOC ? <DragHandle dragHandleHOC={dragHandleHOC} /> : null}
57+
{dragHandle ? <DragHandle Wrapper={dragHandle} id={id} /> : null}
5658
{onRemove ? (
5759
<TopBarButton onClick={onRemove}>
5860
<Icon type="close" size="small" />

packages/decap-cms-widget-file/package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
2323
},
2424
"dependencies": {
25+
"@dnd-kit/core": "^6.0.8",
26+
"@dnd-kit/modifiers": "^6.0.1",
27+
"@dnd-kit/sortable": "^7.0.2",
2528
"array-move": "4.0.0",
26-
"common-tags": "^1.8.0",
27-
"react-sortable-hoc": "^2.0.0"
29+
"common-tags": "^1.8.0"
2830
},
2931
"peerDependencies": {
3032
"@emotion/core": "^10.0.35",

packages/decap-cms-widget-file/src/withFileControl.js

+95-32
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,18 @@ import {
1717
IconButton,
1818
} from 'decap-cms-ui-default';
1919
import { basename } from 'decap-cms-lib-util';
20-
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
2120
import { arrayMoveImmutable as arrayMove } from 'array-move';
21+
import {
22+
DndContext,
23+
MouseSensor,
24+
TouchSensor,
25+
closestCenter,
26+
useSensor,
27+
useSensors,
28+
} from '@dnd-kit/core';
29+
import { SortableContext, useSortable } from '@dnd-kit/sortable';
30+
import { CSS } from '@dnd-kit/utilities';
31+
import { restrictToParentElement } from '@dnd-kit/modifiers';
2232

2333
const MAX_DISPLAY_LENGTH = 50;
2434

@@ -64,9 +74,20 @@ function SortableImageButtons({ onRemove, onReplace }) {
6474
);
6575
}
6676

67-
const SortableImage = SortableElement(({ itemValue, getAsset, field, onRemove, onReplace }) => {
77+
function SortableImage(props) {
78+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
79+
id: props.id,
80+
});
81+
82+
const style = {
83+
transform: CSS.Transform.toString(transform),
84+
transition,
85+
};
86+
87+
const { itemValue, getAsset, field, onRemove, onReplace } = props;
88+
6889
return (
69-
<div>
90+
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
7091
<ImageWrapper sortable>
7192
<Image src={getAsset(itemValue, field) || ''} />
7293
</ImageWrapper>
@@ -77,32 +98,61 @@ const SortableImage = SortableElement(({ itemValue, getAsset, field, onRemove, o
7798
></SortableImageButtons>
7899
</div>
79100
);
80-
});
81-
82-
const SortableMultiImageWrapper = SortableContainer(
83-
({ items, getAsset, field, onRemoveOne, onReplaceOne }) => {
84-
return (
85-
<div
86-
css={css`
87-
display: flex;
88-
flex-wrap: wrap;
89-
`}
101+
}
102+
103+
function SortableMultiImageWrapper({
104+
items,
105+
getAsset,
106+
field,
107+
onSortEnd,
108+
onRemoveOne,
109+
onReplaceOne,
110+
}) {
111+
const activationConstraint = { distance: 4 };
112+
const sensors = useSensors(
113+
useSensor(MouseSensor, { activationConstraint }),
114+
useSensor(TouchSensor, { activationConstraint }),
115+
);
116+
117+
function handleSortEnd({ active, over }) {
118+
onSortEnd({
119+
oldIndex: items.findIndex(item => item.id === active.id),
120+
newIndex: items.findIndex(item => item.id === over.id),
121+
});
122+
}
123+
124+
return (
125+
<div
126+
// eslint-disable-next-line react/no-unknown-property
127+
css={css`
128+
display: flex;
129+
flex-wrap: wrap;
130+
`}
131+
>
132+
<DndContext
133+
modifiers={[restrictToParentElement]}
134+
collisionDetection={closestCenter}
135+
sensors={sensors}
136+
onDragEnd={handleSortEnd}
90137
>
91-
{items.map((itemValue, index) => (
92-
<SortableImage
93-
key={`item-${itemValue}`}
94-
index={index}
95-
itemValue={itemValue}
96-
getAsset={getAsset}
97-
field={field}
98-
onRemove={onRemoveOne(index)}
99-
onReplace={onReplaceOne(index)}
100-
/>
101-
))}
102-
</div>
103-
);
104-
},
105-
);
138+
<SortableContext items={items}>
139+
{items.map((item, index) => (
140+
<SortableImage
141+
key={item.id}
142+
id={item.id}
143+
index={index}
144+
itemValue={item.value}
145+
getAsset={getAsset}
146+
field={field}
147+
onRemove={onRemoveOne(index)}
148+
onReplace={onReplaceOne(index)}
149+
></SortableImage>
150+
))}
151+
</SortableContext>
152+
</DndContext>
153+
</div>
154+
);
155+
}
106156

107157
const FileLink = styled.a`
108158
margin-bottom: 20px;
@@ -152,7 +202,20 @@ function sizeOfValue(value) {
152202
}
153203

154204
function valueListToArray(value) {
155-
return List.isList(value) ? value.toArray() : value;
205+
return List.isList(value) ? value.toArray() : value ?? [];
206+
}
207+
208+
function valueListToSortableArray(value) {
209+
if (!isMultiple(value)) {
210+
return value;
211+
}
212+
213+
const valueArray = valueListToArray(value).map(value => ({
214+
id: uuid(),
215+
value,
216+
}));
217+
218+
return valueArray;
156219
}
157220

158221
const warnDeprecatedOptions = once(field =>
@@ -259,7 +322,7 @@ export default function withFileControl({ forImage } = {}) {
259322
};
260323

261324
onRemoveOne = index => () => {
262-
const { value } = this.props;
325+
const value = valueListToArray(this.props.value);
263326
value.splice(index, 1);
264327
return this.props.onChange(sizeOfValue(value) > 0 ? [...value] : null);
265328
};
@@ -346,11 +409,11 @@ export default function withFileControl({ forImage } = {}) {
346409

347410
renderImages = () => {
348411
const { getAsset, value, field } = this.props;
349-
412+
const items = valueListToSortableArray(value);
350413
if (isMultiple(value)) {
351414
return (
352415
<SortableMultiImageWrapper
353-
items={value}
416+
items={items}
354417
onSortEnd={this.onSortEnd}
355418
onRemoveOne={this.onRemoveOne}
356419
onReplaceOne={this.onReplaceOne}

packages/decap-cms-widget-image/src/ImagePreview.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ function StyledImageAsset({ getAsset, value, field }) {
1717
function ImagePreviewContent(props) {
1818
const { value, getAsset, field } = props;
1919
if (Array.isArray(value) || List.isList(value)) {
20-
return value.map(val => (
21-
<StyledImageAsset key={val} value={val} getAsset={getAsset} field={field} />
20+
return value.map((val, index) => (
21+
<StyledImageAsset key={index} value={val} getAsset={getAsset} field={field} />
2222
));
2323
}
2424
return <StyledImageAsset {...props} />;

packages/decap-cms-widget-list/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
2222
},
2323
"dependencies": {
24-
"react-sortable-hoc": "^2.0.0"
24+
"@dnd-kit/core": "^6.0.8",
25+
"@dnd-kit/modifiers": "^6.0.1",
26+
"@dnd-kit/sortable": "^7.0.2"
2527
},
2628
"peerDependencies": {
2729
"@emotion/core": "^10.0.35",

0 commit comments

Comments
 (0)