Skip to content

Commit ce60276

Browse files
authored
feat: enable drag and drop reordering in references field (#752)
1 parent 4370cbe commit ce60276

File tree

7 files changed

+128
-24
lines changed

7 files changed

+128
-24
lines changed

.changeset/lovely-peaches-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@blinkk/root-cms': patch
3+
---
4+
5+
feat: enable drag and drop reordering in references field (#752)

docs/root-cms.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,4 +357,6 @@ export interface TemplateSandboxFields {
357357
file?: RootCMSFile;
358358
/** FileField (.txt only) */
359359
fileTxtOnly?: RootCMSFile;
360+
/** References */
361+
references?: RootCMSReference[];
360362
}

docs/templates/TemplateSandbox/TemplateSandbox.schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,9 @@ export default schema.define({
1919
label: 'FileField (.txt only)',
2020
exts: ['.txt'],
2121
}),
22+
schema.references({
23+
id: 'references',
24+
label: 'References',
25+
}),
2226
],
2327
});

packages/root-cms/ui/components/DocEditor/fields/ReferencesField.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,45 @@
1010
.ReferencesField__card {
1111
display: flex;
1212
gap: 8px;
13+
align-items: stretch;
14+
padding: 8px 0;
1315
border-bottom: 1px solid var(--color-border);
1416
}
1517

18+
.ReferencesField__card__preview {
19+
padding: 0 !important;
20+
}
21+
1622
.ReferencesField__card:first-child {
1723
border-top: 1px solid var(--color-border);
1824
}
1925

26+
.ReferencesField__card--dragging {
27+
background-color: #f8f9fa;
28+
border: 1px solid #dedede;
29+
border-radius: 4px;
30+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.05) 0 10px 15px -5px,
31+
rgba(0, 0, 0, 0.04) 0 7px 7px -5px;
32+
}
33+
34+
.ReferencesField__card__handle {
35+
display: flex;
36+
align-items: center;
37+
padding: 0 8px;
38+
color: var(--color-text-subtle);
39+
cursor: grab;
40+
}
41+
42+
.ReferencesField__card__handle:active {
43+
cursor: grabbing;
44+
}
45+
2046
.ReferencesField__card__preview {
2147
flex: 1;
2248
}
2349

2450
.ReferencesField__card__controls {
2551
display: flex;
2652
align-items: flex-start;
53+
padding: 0 8px;
2754
}

packages/root-cms/ui/components/DocEditor/fields/ReferencesField.tsx

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import './ReferencesField.css';
22

3+
import {
4+
DragDropContext,
5+
Droppable,
6+
Draggable,
7+
DropResult,
8+
} from '@hello-pangea/dnd';
39
import {ActionIcon, Button, Tooltip} from '@mantine/core';
4-
import {IconTrash} from '@tabler/icons-preact';
10+
import {IconGripVertical, IconTrash} from '@tabler/icons-preact';
511
import {useState} from 'preact/hooks';
612
import * as schema from '../../../../core/schema.js';
713
import {useDraftDoc, useDraftDocField} from '../../../hooks/useDraftDoc.js';
14+
import {joinClassNames} from '../../../utils/classes.js';
815
import {DocPreviewCard} from '../../DocPreviewCard/DocPreviewCard.js';
916
import {useDocSelectModal} from '../../DocSelectModal/DocSelectModal.js';
1017
import {FieldProps} from './FieldProps.js';
@@ -71,27 +78,71 @@ export function ReferencesField(props: FieldProps) {
7178
return (
7279
<div className="ReferencesField">
7380
{refIds.length > 0 ? (
74-
<div className="ReferencesField__refs">
75-
{refIds.map((id) => (
76-
<div key={id} className="ReferencesField__card">
77-
<DocPreviewCard
78-
className="ReferencesField__card__preview"
79-
docId={id}
80-
variant="compact"
81-
/>
82-
<div className="ReferencesField__card__controls">
83-
<Tooltip label="Remove">
84-
<ActionIcon
85-
className="ReferencesField__card__controls__remove"
86-
onClick={() => removeDoc(id)}
87-
>
88-
<IconTrash size={16} />
89-
</ActionIcon>
90-
</Tooltip>
81+
<DragDropContext
82+
onDragEnd={(result: DropResult) => {
83+
const {destination, source} = result;
84+
if (!destination || destination.index === source.index) {
85+
return;
86+
}
87+
const next = [...refIds];
88+
const [removed] = next.splice(source.index, 1);
89+
next.splice(destination.index, 0, removed);
90+
onChange(next);
91+
}}
92+
>
93+
<Droppable
94+
droppableId="ReferencesField__droppable"
95+
direction="vertical"
96+
>
97+
{(provided: any) => (
98+
<div
99+
ref={provided.innerRef}
100+
{...provided.droppableProps}
101+
className="ReferencesField__refs"
102+
>
103+
{refIds.map((refId, index) => (
104+
<Draggable key={refId} draggableId={refId} index={index}>
105+
{(provided, snapshot) => (
106+
<div
107+
ref={provided.innerRef}
108+
{...(provided.draggableProps as any)}
109+
className={joinClassNames(
110+
'ReferencesField__card',
111+
snapshot.isDragging &&
112+
'ReferencesField__card--dragging'
113+
)}
114+
>
115+
<div
116+
className="ReferencesField__card__handle"
117+
{...(provided.dragHandleProps as any)}
118+
>
119+
<IconGripVertical size={16} stroke={'1.5'} />
120+
</div>
121+
<DocPreviewCard
122+
className="ReferencesField__card__preview"
123+
docId={refId}
124+
variant="compact"
125+
clickable
126+
/>
127+
<div className="ReferencesField__card__controls">
128+
<Tooltip label="Remove">
129+
<ActionIcon
130+
className="ReferencesField__card__controls__remove"
131+
onClick={() => removeDoc(refId)}
132+
>
133+
<IconTrash size={16} />
134+
</ActionIcon>
135+
</Tooltip>
136+
</div>
137+
</div>
138+
)}
139+
</Draggable>
140+
))}
141+
{provided.placeholder}
91142
</div>
92-
</div>
93-
))}
94-
</div>
143+
)}
144+
</Droppable>
145+
</DragDropContext>
95146
) : (
96147
<div className="ReferencesField__none">None selected</div>
97148
)}

packages/root-cms/ui/components/DocPreviewCard/DocPreviewCard.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
overflow: hidden;
66
}
77

8+
a.DocPreviewCard {
9+
text-decoration: none;
10+
}
11+
812
.DocPreviewCard__image {
913
border: 1px solid var(--color-border);
1014
flex-shrink: 0;

packages/root-cms/ui/components/DocPreviewCard/DocPreviewCard.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import './DocPreviewCard.css';
2+
13
import {Image, Loader} from '@mantine/core';
24
import {useEffect, useState} from 'preact/hooks';
35
import {joinClassNames} from '../../utils/classes.js';
@@ -6,14 +8,14 @@ import {getDocServingUrl} from '../../utils/doc-urls.js';
68
import {notifyErrors} from '../../utils/notifications.js';
79
import {getNestedValue} from '../../utils/objects.js';
810
import {DocStatusBadges} from '../DocStatusBadges/DocStatusBadges.js';
9-
import './DocPreviewCard.css';
1011

1112
export interface DocPreviewCardProps {
1213
className?: string;
1314
variant?: 'default' | 'compact';
1415
docId: string;
1516
doc?: any;
1617
statusBadges?: boolean;
18+
clickable?: boolean;
1719
}
1820

1921
export function DocPreviewCard(props: DocPreviewCardProps) {
@@ -67,13 +69,22 @@ export function DocPreviewCard(props: DocPreviewCardProps) {
6769
slug: slug,
6870
});
6971

72+
let Component: 'div' | 'a' = 'div';
73+
const attrs: Record<string, any> = {};
74+
if (props.clickable) {
75+
Component = 'a';
76+
attrs.href = `/cms/content/${props.docId}`;
77+
attrs.target = '_blank';
78+
}
79+
7080
return (
71-
<div
81+
<Component
7282
className={joinClassNames(
7383
props.className,
7484
'DocPreviewCard',
7585
props.variant && `DocPreviewCard--${props.variant}`
7686
)}
87+
{...attrs}
7788
>
7889
<div className="DocPreviewCard__image">
7990
<Image
@@ -95,6 +106,6 @@ export function DocPreviewCard(props: DocPreviewCardProps) {
95106
<div className="DocPreviewCard__content__url">{docServingUrl}</div>
96107
)}
97108
</div>
98-
</div>
109+
</Component>
99110
);
100111
}

0 commit comments

Comments
 (0)