Skip to content

Commit a69b94e

Browse files
authored
ENG-882 Let user rearrange personal section and section items in place in left sidebar (#456)
* Let user rearrange personal section and section items in place in left sidebar * address coderabbit * address lint * add todo comment * using pnpm, fix bug * accendental commit from another branch * accendental commit from another branch * address review
1 parent 08e37b3 commit a69b94e

File tree

7 files changed

+601
-130
lines changed

7 files changed

+601
-130
lines changed

apps/roam/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@use-gesture/react": "^10.2.27",
5454
"@vercel/blob": "^1.1.1",
5555
"classnames": "^2.3.2",
56+
"@hello-pangea/dnd": "^18.0.1",
5657
"contrast-color": "^1.0.1",
5758
"core-js": "^3.45.0",
5859
"cytoscape": "^3.21.0",

apps/roam/src/components/LeftSidebarView.tsx

Lines changed: 190 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import React, {
77
useState,
88
} from "react";
99
import ReactDOM from "react-dom";
10+
import {
11+
DragDropContext,
12+
Droppable,
13+
Draggable,
14+
DropResult,
15+
DraggableProvided,
16+
DraggableStateSnapshot,
17+
DroppableProvided,
18+
DragStart,
19+
DraggableRubric,
20+
} from "@hello-pangea/dnd";
1021
import {
1122
Collapse,
1223
Icon,
@@ -147,8 +158,10 @@ const SectionChildren = ({
147158

148159
const PersonalSectionItem = ({
149160
section,
161+
activeDragSourceId,
150162
}: {
151163
section: LeftSidebarPersonalSectionConfig;
164+
activeDragSourceId: string | null;
152165
}) => {
153166
const titleRef = parseReference(section.text);
154167
const blockText = useMemo(
@@ -160,7 +173,33 @@ const PersonalSectionItem = ({
160173
const [isOpen, setIsOpen] = useState<boolean>(
161174
!!section.settings?.folded.value || false,
162175
);
163-
const alias = section.settings?.alias?.value;
176+
177+
const renderChild = (
178+
dragProvided: DraggableProvided,
179+
child: { text: string; uid: string },
180+
) => {
181+
const ref = parseReference(child.text);
182+
const label = truncate(ref.display, truncateAt);
183+
const onClick = (e: React.MouseEvent) => {
184+
return void openTarget(e, child.text);
185+
};
186+
return (
187+
<div
188+
ref={dragProvided.innerRef}
189+
{...dragProvided.draggableProps}
190+
{...dragProvided.dragHandleProps}
191+
style={dragProvided.draggableProps.style}
192+
className="pl-8 pr-2.5"
193+
>
194+
<div
195+
className="section-child-item page cursor-pointer rounded-sm leading-normal text-gray-600"
196+
onClick={onClick}
197+
>
198+
{label}
199+
</div>
200+
</div>
201+
);
202+
};
164203

165204
const handleChevronClick = () => {
166205
if (!section.settings) return;
@@ -187,7 +226,7 @@ const PersonalSectionItem = ({
187226
}
188227
}}
189228
>
190-
{(alias || blockText || titleRef.display).toUpperCase()}
229+
{(blockText || titleRef.display).toUpperCase()}
191230
</div>
192231
{(section.children?.length || 0) > 0 && (
193232
<span
@@ -200,28 +239,162 @@ const PersonalSectionItem = ({
200239
</div>
201240
</div>
202241
<Collapse isOpen={isOpen}>
203-
<SectionChildren
204-
childrenNodes={section.children || []}
205-
truncateAt={truncateAt}
206-
/>
242+
<Droppable
243+
droppableId={section.uid}
244+
type="ITEMS"
245+
isDropDisabled={
246+
!!activeDragSourceId && activeDragSourceId !== section.uid
247+
}
248+
renderClone={(
249+
provided: DraggableProvided,
250+
_: DraggableStateSnapshot,
251+
rubric: DraggableRubric,
252+
) => {
253+
const child = (section.children || [])[rubric.source.index];
254+
return renderChild(provided, child);
255+
}}
256+
>
257+
{(provided: DroppableProvided) => (
258+
<div ref={provided.innerRef} {...provided.droppableProps}>
259+
{(section.children || []).map((child, index) => (
260+
<Draggable
261+
key={child.uid}
262+
draggableId={child.uid}
263+
index={index}
264+
isDragDisabled={(section.children || []).length <= 1}
265+
>
266+
{(dragProvided: DraggableProvided) => {
267+
return renderChild(dragProvided, child);
268+
}}
269+
</Draggable>
270+
))}
271+
{provided.placeholder}
272+
</div>
273+
)}
274+
</Droppable>
207275
</Collapse>
208276
</>
209277
);
210278
};
211279

212280
const PersonalSections = ({
213281
config,
282+
setConfig,
214283
}: {
215-
config: LeftSidebarConfig["personal"];
284+
config: LeftSidebarConfig;
285+
setConfig: Dispatch<SetStateAction<LeftSidebarConfig>>;
216286
}) => {
217-
const sections = config.sections || [];
287+
const sections = config.personal.sections || [];
288+
const [activeDragSourceId, setActiveDragSourceId] = useState<string | null>(
289+
null,
290+
);
291+
218292
if (!sections.length) return null;
293+
294+
const handleDragStart = (start: DragStart) => {
295+
if (start.type === "ITEMS") {
296+
setActiveDragSourceId(start.source.droppableId);
297+
}
298+
};
299+
300+
const handleDragEnd = (result: DropResult) => {
301+
setActiveDragSourceId(null);
302+
const { source, destination, type } = result;
303+
304+
if (!destination) return;
305+
306+
if (type === "SECTIONS") {
307+
if (destination.index === source.index) return;
308+
309+
const newPersonalSections = Array.from(config.personal.sections);
310+
const [removed] = newPersonalSections.splice(source.index, 1);
311+
newPersonalSections.splice(destination.index, 0, removed);
312+
313+
setConfig({
314+
...config,
315+
personal: { ...config.personal, sections: newPersonalSections },
316+
});
317+
const finalIndex =
318+
destination.index > source.index
319+
? destination.index + 1
320+
: destination.index;
321+
void window.roamAlphaAPI.moveBlock({
322+
location: { "parent-uid": config.personal.uid, order: finalIndex },
323+
block: { uid: removed.uid },
324+
});
325+
return;
326+
}
327+
328+
if (type === "ITEMS") {
329+
if (source.droppableId !== destination.droppableId) {
330+
return;
331+
}
332+
333+
if (destination.index === source.index) {
334+
return;
335+
}
336+
337+
const newConfig = JSON.parse(JSON.stringify(config)) as LeftSidebarConfig;
338+
const { personal } = newConfig;
339+
340+
const listToReorder = personal.sections.find(
341+
(s) => s.uid === source.droppableId,
342+
);
343+
const parentUid = listToReorder?.childrenUid;
344+
const listToReorderChildren = listToReorder?.children;
345+
346+
if (!listToReorderChildren) return;
347+
348+
const [removedItem] = listToReorderChildren.splice(source.index, 1);
349+
listToReorderChildren.splice(destination.index, 0, removedItem);
350+
351+
setConfig(newConfig);
352+
const finalIndex =
353+
destination.index > source.index
354+
? destination.index + 1
355+
: destination.index;
356+
void window.roamAlphaAPI.moveBlock({
357+
location: { "parent-uid": parentUid || "", order: finalIndex },
358+
block: { uid: removedItem.uid },
359+
});
360+
}
361+
};
219362
return (
220-
<div className="personal-left-sidebar-sections">
221-
{sections.map((s) => (
222-
<PersonalSectionItem key={s.uid} section={s} />
223-
))}
224-
</div>
363+
<DragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
364+
<Droppable droppableId="personal-sections" type="SECTIONS">
365+
{(provided: DroppableProvided) => (
366+
<div
367+
ref={provided.innerRef}
368+
{...provided.droppableProps}
369+
className="personal-left-sidebar-sections"
370+
>
371+
{sections.map((section, index) => (
372+
<Draggable
373+
key={section.uid}
374+
draggableId={section.uid}
375+
index={index}
376+
isDragDisabled={sections.length <= 1}
377+
>
378+
{(dragProvided: DraggableProvided) => (
379+
<div
380+
ref={dragProvided.innerRef}
381+
{...dragProvided.draggableProps}
382+
{...dragProvided.dragHandleProps}
383+
style={dragProvided.draggableProps.style}
384+
>
385+
<PersonalSectionItem
386+
section={section}
387+
activeDragSourceId={activeDragSourceId}
388+
/>
389+
</div>
390+
)}
391+
</Draggable>
392+
))}
393+
{provided.placeholder}
394+
</div>
395+
)}
396+
</Droppable>
397+
</DragDropContext>
225398
);
226399
};
227400

@@ -312,7 +485,6 @@ const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
312485

313486
useEffect(() => {
314487
if (!isMenuOpen) return;
315-
console.log("handleGlobalPointerDownCapture");
316488
const opts = { capture: true } as AddEventListenerOptions;
317489
window.addEventListener(
318490
"mousedown",
@@ -336,7 +508,7 @@ const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
336508
opts,
337509
);
338510
};
339-
}, [handleGlobalPointerDownCapture]);
511+
}, [handleGlobalPointerDownCapture, isMenuOpen]);
340512

341513
const renderSettingsDialog = (tabId: TabId) => {
342514
renderOverlay({
@@ -400,12 +572,13 @@ const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
400572
};
401573

402574
const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
403-
const config = useConfig();
575+
const initialConfig = useConfig();
576+
const [config, setConfig] = useState(initialConfig);
404577
return (
405578
<>
406579
<FavouritesPopover onloadArgs={onloadArgs} />
407580
<GlobalSection config={config.global} />
408-
<PersonalSections config={config.personal} />
581+
<PersonalSections config={config} setConfig={setConfig} />
409582
</>
410583
);
411584
};

apps/roam/src/components/results-view/Kanban.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const KanbanCard = (card: {
7676
const cardView = card.viewsByColumn[displayKey];
7777
const displayUid = card.result[`${displayKey}-uid`];
7878

79+
// TODO - https://linear.app/discourse-graphs/issue/ENG-909/migrate-kanban-react-draggable-to-hello-pangeadnd
7980
return (
8081
<Draggable
8182
handle={isDragHandle ? ".embed-handle" : ""}

0 commit comments

Comments
 (0)