diff --git a/backend/app/__pycache__/models.cpython-314.pyc b/backend/app/__pycache__/models.cpython-314.pyc index 001efb1..b6ce6ea 100644 Binary files a/backend/app/__pycache__/models.cpython-314.pyc and b/backend/app/__pycache__/models.cpython-314.pyc differ diff --git a/backend/app/models.py b/backend/app/models.py index b68f89c..a01a0dc 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -67,3 +67,8 @@ class NoteUpdate(SQLModel): class FolderCreate(SQLModel): name: str parent_id: Optional[int] = None + + +class FolderUpdate(SQLModel): + name: Optional[str] = None + parent_id: Optional[int] = None diff --git a/backend/app/routes/__pycache__/folders.cpython-314.pyc b/backend/app/routes/__pycache__/folders.cpython-314.pyc index 892c122..ab040a2 100644 Binary files a/backend/app/routes/__pycache__/folders.cpython-314.pyc and b/backend/app/routes/__pycache__/folders.cpython-314.pyc differ diff --git a/backend/app/routes/folders.py b/backend/app/routes/folders.py index e5b2432..9b4f976 100644 --- a/backend/app/routes/folders.py +++ b/backend/app/routes/folders.py @@ -9,6 +9,7 @@ from app.models import ( FolderCreate, FolderTreeNode, FolderTreeResponse, + FolderUpdate, Note, NoteRead, ) @@ -74,3 +75,52 @@ def delete_folder(folder_id: int, session: Session = Depends(get_session)): session.delete(folder) session.commit() return {"message": "Folder deleted"} + + +@router.patch("/{folder_id}") +def update_folder( + folder_id: int, folder_update: FolderUpdate, session: Session = Depends(get_session) +): + """Update a folder""" + print(f"=== UPDATE FOLDER CALLED ===") + print(f"Folder ID: {folder_id}") + print(f"Update data received: {folder_update}") + + folder = session.get(Folder, folder_id) + if not folder: + raise HTTPException(status_code=404, detail="Folder not found") + + print( + f"Found folder: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}" + ) + + # Update folder attributes from the request body + update_data = folder_update.model_dump(exclude_unset=True) + print(f"Update data dict (exclude_unset): {update_data}") + print(f"Update data keys: {list(update_data.keys())}") + + for key, value in update_data.items(): + print(f"Setting {key} = {value}") + setattr(folder, key, value) + + print( + f"After setattr: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}" + ) + + session.add(folder) + session.commit() + print(f"Committed changes to database") + + session.refresh(folder) + print( + f"After refresh: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}" + ) + + # Verify the change persisted + verification = session.get(Folder, folder_id) + print( + f"Verification query: id={verification.id}, name={verification.name}, parent_id={verification.parent_id}" + ) + print(f"=== UPDATE COMPLETE ===") + + return folder diff --git a/backend/notes.db b/backend/notes.db index d087600..fd73d29 100644 Binary files a/backend/notes.db and b/backend/notes.db differ diff --git a/frontend/src/api/folders.tsx b/frontend/src/api/folders.tsx index 6e5ab22..5f4f287 100644 --- a/frontend/src/api/folders.tsx +++ b/frontend/src/api/folders.tsx @@ -36,6 +36,11 @@ export interface FolderCreate { parent_id: number | null; } +export interface FolderUpdate { + name?: string; + parent_id?: number | null; +} + const getFolderTree = async () => { const { data } = await axios.get( `${API_URL}/folders/tree`, @@ -46,10 +51,24 @@ const getFolderTree = async () => { return decryptedFolderTree; }; +const updateFolder = async (id: number, folder: FolderUpdate) => { + console.log(`Updating folder ${id} with:`, folder); + try { + const response = await axios.patch(`${API_URL}/folders/${id}`, folder); + console.log(`Folder ${id} update response:`, response.data); + return response; + } catch (error) { + console.error(`Failed to update folder ${id}:`, error); + throw error; + } +}; + export const folderApi = { tree: () => getFolderTree(), list: () => axios.get(`${API_URL}/folders`), create: (folder: FolderCreate) => axios.post(`${API_URL}/folders`, folder), delete: (id: number) => axios.delete(`${API_URL}/folders/${id}`), + update: (id: number, updateData: FolderUpdate) => + updateFolder(id, updateData), }; diff --git a/frontend/src/components/sidebar/DroppableFolder.tsx b/frontend/src/components/sidebar/DroppableFolder.tsx index 8618800..35cfba2 100644 --- a/frontend/src/components/sidebar/DroppableFolder.tsx +++ b/frontend/src/components/sidebar/DroppableFolder.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useDroppable } from "@dnd-kit/core"; +import { useDroppable, useDraggable } from "@dnd-kit/core"; import { Folder, NoteRead } from "../../api/folders"; export const DroppableFolder = ({ @@ -17,12 +17,33 @@ export const DroppableFolder = ({ setCollapse: React.Dispatch>; collapse: boolean; }) => { - const { isOver, setNodeRef } = useDroppable({ + const { isOver, setNodeRef: setDroppableRef } = useDroppable({ id: folder.id!, data: { type: "folder", folder }, }); + + const { + attributes, + listeners, + setNodeRef: setDraggableRef, + transform, + isDragging, + } = useDraggable({ + id: `folder-${folder.id}`, + data: { type: "folder", folder }, + }); + + const setNodeRef = (node: HTMLElement | null) => { + setDroppableRef(node); + setDraggableRef(node); + }; + const style = { color: isOver ? "green" : undefined, + opacity: isDragging ? 0.5 : 1, + transform: transform + ? `translate3d(${transform.x}px, ${transform.y}px, 0)` + : undefined, }; return ( @@ -35,10 +56,18 @@ export const DroppableFolder = ({ ? "bg-ctp-surface1" : "hover:bg-ctp-surface0" }`} + {...listeners} + {...attributes} > {folder.name} -
setCollapse(!collapse)} className="ml-auto"> +
{ + e.stopPropagation(); // Prevent dragging when clicking the collapse button + setCollapse(!collapse); + }} + className="ml-auto" + > x
diff --git a/frontend/src/components/sidebar/SideBar.tsx b/frontend/src/components/sidebar/SideBar.tsx index f3985cb..c3165a0 100644 --- a/frontend/src/components/sidebar/SideBar.tsx +++ b/frontend/src/components/sidebar/SideBar.tsx @@ -9,6 +9,14 @@ import { import { DraggableNote } from "./DraggableNote"; import { DroppableFolder } from "./DroppableFolder"; import { useNoteStore } from "../../stores/notesStore"; +import { + DndContext, + DragEndEvent, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { notesApi } from "../../api/notes"; export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => { // const [folderTree, setFolderTree] = useState(null); @@ -47,71 +55,124 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => { setNewFolder(false); }; + const pointer = useSensor(PointerSensor, { + activationConstraint: { + distance: 30, + }, + }); + const sensors = useSensors(pointer); + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + if (!over) return; + + console.log("Drag ended:", { + activeId: active.id, + activeType: active.data.current?.type, + activeFolder: active.data.current?.folder, + overId: over.id, + overType: over.data.current?.type, + }); + + if (active.data.current?.type === "note") { + console.log("Updating note", active.id, "to folder", over.id); + await notesApi.update(active.id as number, { + folder_id: over.id as number, + }); + } else if (active.data.current?.type === "folder") { + // Prevent dropping folder into itself + if (active.data.current.folder.id === over.id) { + console.log("Cannot drop folder into itself"); + return; + } + + console.log( + "Updating folder", + active.data.current.folder.id, + "parent to", + over.id, + ); + try { + const response = await folderApi.update(active.data.current.folder.id, { + parent_id: over.id as number, + }); + console.log("Folder update response:", response); + } catch (error) { + console.error("Failed to update folder:", error); + return; + } + } + + loadFolderTree(); + }; + return ( -
e.preventDefault()} - onTouchMove={(e) => e.preventDefault()} - > - - {/* New folder input */} - {newFolder && ( -
- setNewFolder(false)} - onChange={(e) => setNewFolderText(e.target.value)} - value={newFolderText} - type="text" - placeholder="Folder name..." - className="border border-ctp-mauve rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-ctp-mauve bg-ctp-base text-ctp-text placeholder:text-ctp-overlay0" - ref={newFolderRef} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCreateFolder(); - } - if (e.key === "Escape") { - setNewFolder(false); - } - }} - /> -
- )} + +
e.preventDefault()} + onTouchMove={(e) => e.preventDefault()} + > + + {/* New folder input */} + {newFolder && ( +
+ setNewFolder(false)} + onChange={(e) => setNewFolderText(e.target.value)} + value={newFolderText} + type="text" + placeholder="Folder name..." + className="border border-ctp-mauve rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-ctp-mauve bg-ctp-base text-ctp-text placeholder:text-ctp-overlay0" + ref={newFolderRef} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateFolder(); + } + if (e.key === "Escape") { + setNewFolder(false); + } + }} + /> +
+ )} - {/* Folder tree */} -
- {folderTree?.folders.map((folder) => ( - - ))} -
- - {/* Orphaned notes */} - {folderTree?.orphaned_notes && folderTree.orphaned_notes.length > 0 && ( -
- {/*
- Unsorted -
*/} - {folderTree.orphaned_notes.map((note) => ( - + {folderTree?.folders.map((folder) => ( + ))}
- )} -
+ + {/* Orphaned notes */} + {folderTree?.orphaned_notes && folderTree.orphaned_notes.length > 0 && ( +
+ {/*
+ Unsorted +
*/} + {folderTree.orphaned_notes.map((note) => ( + + ))} +
+ )} +
+ ); }; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 4b7fb3d..097423a 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -39,7 +39,9 @@ import { import "@mdxeditor/editor/style.css"; import { DroppableFolder } from "../components/sidebar/DroppableFolder"; import { DraggableNote } from "../components/sidebar/DraggableNote"; +// @ts-ignore import CheckIcon from "../assets/fontawesome/svg/circle-check.svg?react"; +// @ts-ignore import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react"; import { useNoteStore } from "../stores/notesStore"; import { create } from "zustand"; @@ -83,12 +85,7 @@ function Home() { selectedNote, } = useNoteStore(); - const pointer = useSensor(PointerSensor, { - activationConstraint: { - distance: 30, - }, - }); - const sensors = useSensors(pointer); + const newFolderRef = useRef(null); @@ -139,19 +136,10 @@ function Home() { setContent(""); }; - const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - if (!over) return; - await notesApi.update(active.id as number, { - folder_id: over.id as number, - }); - - loadFolderTree(); - }; return ( - +
{/* Sidebar */} @@ -188,7 +176,6 @@ function Home() { <> - ), }), @@ -251,19 +238,6 @@ function Home() { Create Note )} - - {/* Encryption toggle */} - {/**/}
@@ -286,7 +260,6 @@ function Home() { )} -
); }