Skip to content

Commit 79c9635

Browse files
committed
Add support for move file/folder
1 parent 75fcc5c commit 79c9635

11 files changed

+653
-337
lines changed

package-lock.json

+205-163
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/FileSystemAdapters/FileSystem/FileSystemItem.tsx

+91-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useRef, useState } from "react";
22
import DirectoryNode from "../../../models/DirectoryNode";
33
import { ChevronDown, ChevronRight, FilePlus, FolderPlus } from "lucide-react";
44
import RightClickMenu from "./RightClickMenu";
@@ -7,28 +7,108 @@ import * as ContextMenu from "@radix-ui/react-context-menu";
77
export function FileSystemItem({
88
node,
99
depth,
10+
forceRerenderCounter,
11+
setForceRerenderCounter,
12+
setSelectedFile,
1013
handleFileSelect,
1114
currentlySelectedFile,
1215
handleDeleteFile,
1316
handleCreateFile,
1417
handleCreateFolder,
1518
handleRenameFolder,
1619
handleRenameFile,
20+
draggable
1721
}: {
1822
node: DirectoryNode;
1923
depth: number;
24+
forceRerenderCounter: number;
25+
setForceRerenderCounter: (counter: number) => void;
26+
setSelectedFile: (file: DirectoryNode | null) => void;
2027
handleFileSelect: (file: DirectoryNode) => void;
2128
currentlySelectedFile: DirectoryNode | undefined;
2229
handleDeleteFile: (node: DirectoryNode) => void;
2330
handleCreateFile: (node: DirectoryNode) => void;
2431
handleCreateFolder: (node: DirectoryNode) => void;
2532
handleRenameFolder: (node: DirectoryNode) => void;
2633
handleRenameFile: (node: DirectoryNode) => void;
34+
draggable: boolean
2735
}) {
2836
const [expanded, setExpanded] = useState(depth === 0);
37+
const [isDragOver, setIsDragOver] = useState(false);
38+
const dragEnterCount = useRef(0);
39+
40+
const handleOnDragStart = (e: React.DragEvent<HTMLDivElement>, node: DirectoryNode) => {
41+
e.dataTransfer.setData("node_id", node.getId());
42+
console.log(e.dataTransfer.getData("node_id"));
43+
e.stopPropagation();
44+
};
45+
46+
const handleOnDragDrop = (e: React.DragEvent<HTMLDivElement>, newParent: DirectoryNode) => {
47+
console.log(e.dataTransfer.getData("node_id"));
48+
console.log('dropping');
49+
console.log(newParent);
50+
51+
const droppedNodeId = e.dataTransfer.getData("node_id");
52+
53+
54+
//get root node
55+
const rootNode = node.getRootNode();
56+
//get dropped node by id from rootNode
57+
const droppedNode = rootNode.getNodeById(droppedNodeId);
58+
59+
try{
60+
droppedNode?.moveNodeToNewParent(newParent);
61+
setForceRerenderCounter(forceRerenderCounter + 1);
62+
console.log("forced rerender");
63+
}
64+
catch(e){
65+
console.log(e);
66+
alert("Could not move file due to conflicting file names in destination.")
67+
}
68+
69+
setIsDragOver(false);
70+
dragEnterCount.current = 0;
71+
e.preventDefault();
72+
e.stopPropagation();
73+
};
74+
75+
const handleOnDragOver = (e: React.DragEvent<HTMLDivElement>) => {
76+
e.preventDefault();
77+
};
78+
79+
const handleOnDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
80+
setIsDragOver(true);
81+
82+
dragEnterCount.current += 1;
83+
84+
e.preventDefault();
85+
e.stopPropagation();
86+
};
87+
88+
const handleOnDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
89+
90+
dragEnterCount.current -= 1;
91+
92+
if (dragEnterCount.current <= 0) {
93+
setIsDragOver(false);
94+
}
95+
96+
e.preventDefault();
97+
e.stopPropagation();
98+
};
2999

30100
return (
31-
<div key={node.getName()} className="select-none">
101+
<div
102+
key={node.getName()}
103+
className="select-none outline-zinc-400 rounded outline-dashed outline-0"
104+
draggable={draggable}
105+
onDragStart={draggable ? (e) => handleOnDragStart(e, node) : undefined}
106+
onDrop={(e) => handleOnDragDrop(e, node)}
107+
onDragOver={handleOnDragOver}
108+
onDragEnter={handleOnDragEnter}
109+
onDragLeave={handleOnDragLeave}
110+
style={isDragOver? { outlineWidth: "1px" } : {}}
111+
>
32112
{depth === 0 ? null : (
33113
<div
34114
className="p-0.5 hover:bg-zinc-200/70 rounded flex items-center "
@@ -96,6 +176,8 @@ export function FileSystemItem({
96176
: ""
97177
}
98178
`}
179+
draggable={true}
180+
onDragStart={(e) => handleOnDragStart(e, child)}
99181
onClick={() => handleFileSelect(child)}
100182
>
101183
<div className="pl-1">
@@ -111,13 +193,17 @@ export function FileSystemItem({
111193
<FileSystemItem
112194
node={child}
113195
depth={depth + 1}
196+
forceRerenderCounter={forceRerenderCounter}
197+
setForceRerenderCounter={setForceRerenderCounter}
198+
setSelectedFile={setSelectedFile}
114199
handleFileSelect={handleFileSelect}
115200
currentlySelectedFile={currentlySelectedFile}
116201
handleDeleteFile={handleDeleteFile}
117202
handleCreateFile={handleCreateFile}
118203
handleCreateFolder={handleCreateFolder}
119204
handleRenameFolder={handleRenameFolder}
120205
handleRenameFile={handleRenameFile}
206+
draggable={true}
121207
/> // Recursively render directory
122208
)}
123209
</ContextMenu.Trigger>
@@ -134,9 +220,9 @@ export function FileSystemItem({
134220
}
135221
onDelete={() => handleDeleteFile(child)}
136222
onRename={
137-
child.isDirectory() ?
138-
() => handleRenameFolder(child) :
139-
() => handleRenameFile(child)
223+
child.isDirectory()
224+
? () => handleRenameFolder(child)
225+
: () => handleRenameFile(child)
140226
}
141227
/>
142228
</ContextMenu.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import DirectoryNode, { createDirectoryNode } from "../../../models/DirectoryNode";
2+
import { Button } from "../../ui/Button";
3+
import * as Separator from "@radix-ui/react-separator";
4+
5+
export function GettingStartedHelper({
6+
selectedFile,
7+
selectedDirectory,
8+
setSelectedDirectory
9+
}: {
10+
selectedFile: DirectoryNode | null;
11+
selectedDirectory: DirectoryNode | null;
12+
setSelectedDirectory: (node: DirectoryNode | null) => void;
13+
}) {
14+
15+
const handleDirectorySelect = async () => {
16+
try {
17+
//@ts-expect-error
18+
const directoryHandle = await window.showDirectoryPicker();
19+
if (!directoryHandle) return;
20+
21+
setSelectedDirectory(
22+
await createDirectoryNode(directoryHandle, undefined)
23+
);
24+
} catch (error) {
25+
console.error("Error selecting directory:", error);
26+
}
27+
};
28+
29+
return (
30+
<div
31+
className="z-20 flex fixed top-[50%] left-[50%]
32+
transform translate-x-[-10%] translate-y-[-35%]
33+
"
34+
>
35+
<div className="bg-white rounded shadow-md">
36+
<div className="mx-8 mt-8 mb-4">
37+
<div className="flex flex-col space-y-1.5 ">
38+
<div className="text-center ">
39+
<h1 className="font-semibold text-lg leading-none tracking-tight mb-0.5">
40+
Paginary
41+
</h1>
42+
<p className="text-muted-foreground text-sm">
43+
A local-first open-source note taking app.
44+
</p>
45+
</div>
46+
<div className="my-2">
47+
<Separator.Root className="bg-zinc-200 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full my-[15px]" />
48+
</div>
49+
50+
<div className="text-center">
51+
<div className="mb-2">
52+
<Button variant={"outline"} className="rounded w-full">
53+
Create a new note
54+
</Button>
55+
</div>
56+
57+
<div className="">
58+
<Button
59+
variant={"link"}
60+
className="rounded w-full font-normal "
61+
>
62+
Select a location to save your notes
63+
</Button>
64+
</div>
65+
<div className="">
66+
<Button
67+
variant={"link"}
68+
className="rounded w-full font-normal"
69+
onClick={handleDirectorySelect}
70+
>
71+
Open an existing folder
72+
</Button>
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
);
80+
}

src/components/FileSystemAdapters/FileSystem/LocalFileSystem.tsx

+25-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// and pass them down to the Folder component. The Folder component is then responsible for rendering
33
// and calling them
44

5-
import { useEffect } from "react";
5+
import { useEffect, useReducer, useState } from "react";
66
import DirectoryNode, {
77
createDirectoryNode,
88
} from "../../../models/DirectoryNode";
99
import { FileSystemItem } from "./FileSystemItem";
10-
import { FilePlus, FolderPlus, X } from "lucide-react";
10+
import { FilePlus, FolderPlus, LucideFile, LucideFilePlus, LucideFolderPlus, X } from "lucide-react";
11+
import { set } from "remirror";
12+
import { Button } from "../../ui/Button";
1113

1214
//sample react function
1315
export const LocalFileSystem = ({
@@ -27,6 +29,9 @@ export const LocalFileSystem = ({
2729
setPanelIsOpen: (isOpen: boolean) => void;
2830
panelIsOpen: boolean;
2931
}) => {
32+
33+
const [forceRerenderCounter, setForceRerenderCounter] = useState(0);
34+
3035
useEffect(() => {
3136
const fetchEntries = async () => {
3237
if (selectedDirectory?.directoryHandle) {
@@ -80,6 +85,8 @@ export const LocalFileSystem = ({
8085
} else {
8186
setSelectedFile(null);
8287
}
88+
89+
setForceRerenderCounter(forceRerenderCounter + 1);
8390
};
8491

8592
const createFileHandlingDuplicates = async (parent: DirectoryNode) => {
@@ -98,6 +105,8 @@ export const LocalFileSystem = ({
98105
}
99106
}
100107

108+
setForceRerenderCounter(forceRerenderCounter + 1);
109+
101110
throw new Error("Could not create file");
102111
};
103112

@@ -112,6 +121,8 @@ export const LocalFileSystem = ({
112121
"Error creating file, too many conflicts when trying to create the file."
113122
);
114123
}
124+
125+
setForceRerenderCounter(forceRerenderCounter + 1);
115126
};
116127

117128
const createFolderHandlingDuplicates = async (parent: DirectoryNode) => {
@@ -129,6 +140,8 @@ export const LocalFileSystem = ({
129140
}
130141
}
131142

143+
setForceRerenderCounter(forceRerenderCounter + 1);
144+
132145
throw new Error("Could not create folder");
133146
};
134147

@@ -148,6 +161,7 @@ export const LocalFileSystem = ({
148161
if (newName) {
149162
await node.renameFolder(newName);
150163
setSelectedFile(selectedFile?.getCopy() || null);
164+
setForceRerenderCounter(forceRerenderCounter + 1);
151165
}
152166
};
153167

@@ -159,6 +173,7 @@ export const LocalFileSystem = ({
159173
if (newName) {
160174
await node.renameFile(newName);
161175
setSelectedFile(selectedFile?.getCopy() || null);
176+
setForceRerenderCounter(forceRerenderCounter + 1);
162177
}
163178
};
164179

@@ -183,9 +198,7 @@ export const LocalFileSystem = ({
183198

184199
return (
185200
<div
186-
className={`bg-zinc-50 overflow-y-scroll ${
187-
hidden ? "hidden" : ""
188-
}
201+
className={`bg-zinc-50 overflow-y-scroll ${hidden ? "hidden" : ""}
189202
w-full h-full scrollbar scrollbar-thumb-zinc-200 scrollbar-track-zinc-100 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-track-rounded-full
190203
`}
191204
>
@@ -230,15 +243,22 @@ export const LocalFileSystem = ({
230243
</div>
231244
<div className="font-normal p-1">
232245
<FileSystemItem
246+
key={forceRerenderCounter} //react does a shallow compare on an object to determine if it needs to rerender. since we are updating the object directly when
247+
//updating name/moving files, etc. and we don't want to create copies of the root selected directory unnecessarily (causes rerenders of file, remirror editor, etc.)
248+
//we are incrementing a counter that we can use to force the rerender of this. by setting it as the key to the filesystemitem, we can force the update for this
233249
node={selectedDirectory}
234250
depth={0}
251+
forceRerenderCounter={forceRerenderCounter}
252+
setForceRerenderCounter={setForceRerenderCounter}
253+
setSelectedFile={setSelectedFile}
235254
handleFileSelect={handleFileSelect}
236255
currentlySelectedFile={selectedFile}
237256
handleDeleteFile={handleDeleteFile}
238257
handleCreateFile={handleCreateFile}
239258
handleCreateFolder={handleCreateFolder}
240259
handleRenameFolder={handleRenameFolder}
241260
handleRenameFile={handleRenameFile}
261+
draggable={false}
242262
/>
243263
</div>
244264
</div>

src/components/FileSystemAdapters/FileSystem/RightClickMenu.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const RightClickMenu = ({ onCreateFile, onCreateFolder, onDelete, onRename }:
1919
{
2020
onCreateFile && (
2121
<ContextMenu.Item
22-
className={`text-sm leading-none rounded-md
22+
className={`text-sm leading-none rounded-md cursor-pointer
2323
flex items-center h-[25px] px-[5px] relative select-none outline-none
2424
data-[disabled]:pointer-events-none
2525
data-[highlighted]:bg-zinc-100`}
@@ -35,7 +35,7 @@ const RightClickMenu = ({ onCreateFile, onCreateFolder, onDelete, onRename }:
3535
{
3636
onCreateFolder && (
3737
<ContextMenu.Item
38-
className={`text-sm leading-none rounded-md
38+
className={`text-sm leading-none rounded-md cursor-pointer
3939
flex items-center h-[25px] px-[5px] relative select-none outline-none
4040
data-[disabled]:pointer-events-none
4141
data-[highlighted]:bg-zinc-100`}
@@ -49,9 +49,10 @@ const RightClickMenu = ({ onCreateFile, onCreateFolder, onDelete, onRename }:
4949
)
5050
}
5151
<ContextMenu.Item
52-
className={`text-sm leading-none rounded-md
52+
className={`text-sm leading-none rounded-md cursor-pointer
5353
flex items-center h-[25px] px-[5px] relative select-none outline-none
5454
data-[disabled]:pointer-events-none
55+
cursor-pointer
5556
data-[highlighted]:bg-zinc-100`}
5657
onClick={() => {
5758
onRename();
@@ -61,7 +62,7 @@ const RightClickMenu = ({ onCreateFile, onCreateFolder, onDelete, onRename }:
6162
<div>Rename</div>
6263
</ContextMenu.Item>
6364
<ContextMenu.Item
64-
className={`text-sm leading-none rounded-md
65+
className={`text-sm leading-none rounded-md cursor-pointer
6566
flex items-center h-[25px] px-[5px] relative select-none outline-none
6667
data-[disabled]:pointer-events-none
6768
data-[highlighted]:bg-zinc-100`}

0 commit comments

Comments
 (0)