From b27604130a843affba9b2dca7844e91b55b47b2a Mon Sep 17 00:00:00 2001 From: james fitzsimons Date: Sun, 30 Nov 2025 19:40:10 +0000 Subject: [PATCH] Add FolderUpdate model and folder update endpoint --- .../app/__pycache__/models.cpython-314.pyc | Bin 5720 -> 6196 bytes backend/app/models.py | 5 + .../__pycache__/folders.cpython-314.pyc | Bin 4649 -> 7062 bytes backend/app/routes/folders.py | 50 +++++ backend/notes.db | Bin 110592 -> 110592 bytes frontend/src/api/folders.tsx | 19 ++ .../components/sidebar/DroppableFolder.tsx | 35 +++- frontend/src/components/sidebar/SideBar.tsx | 179 ++++++++++++------ frontend/src/pages/Home.tsx | 35 +--- 9 files changed, 230 insertions(+), 93 deletions(-) diff --git a/backend/app/__pycache__/models.cpython-314.pyc b/backend/app/__pycache__/models.cpython-314.pyc index 001efb13a71da49f4ec3560c4f61db24f34c2284..b6ce6eac9d16f565af1d0de4d8144952f2ec0a91 100644 GIT binary patch delta 374 zcmcbiv&Dc148SOyrYbVqu)9@kU0JL6IRyOq?M|JeJ*r8Ki=Np+q1^ zLYYBRaPv_U}Ri5xkzLuTO?4rC~~r)s4`>ZWJNB?$&oyQCLSfsVhs5V zL5w9VK+GD(V9XfAB+ih-QNk9)T*4m4P{I+!2sDu87LQwgPD*M~XhF(kK@q9RXZXU{ zi=shBhD{FOR}7MbODC43>cz(==H=y=0GaXe9zaFK%0S{rJ3|x0O)jB{#=W{1S>!%4 zGcYnf%n7gtVRE?&;{je&_#rAQkn F006MK8A1R6 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 892c1223fb9e8fbf8df42aa06e1fd7891c26f075..ab040a2f0f99a742616197d98bbbee90c095b9be 100644 GIT binary patch delta 3029 zcmb7GZ%kX)6~FI~pN;>5F?PTN9>FD;kgz6XGzMp*0TO1KxNhUXl!UqV1FRUE-1kfq z^1<*asWeTXH?3|JtxTFU*rat5U-GeQzoeh~C|-%IOv|eM(WHHVC0e9?*mlmdJq%k@ z?Mgnr_uTX6ckVs!oV)J(QJ?*o-DV~*zI?YkS>sr-A7Tl+(s{l6f>Uv}l51QWi3kzn z2_|CdV^NL}r9M*QArZ5Oq-qt{wfgX}m0u1U+&EFgOoM7;&Kf+hdKp&3hDMm3R^Mj^ z4PKa?f!T4d2hqDTml>n_3fphBj$~2^iC&&d#FUMjEXx?0 zK-jf$i~A|_(qWj=79_1mYJn8(vzdgHnw*cNlAt0{Q5`h2FmCmGhDJvtN*$>XmwM0e z8-RLUa5o(Srpy32LUkc@LXotpKMA;FubJ*dj+C}|zcWxUspvU_VP zDOdcQIvCG7$H4(tu7UA1Aa z7iMwH9{l0J`$W~J18R@GCl}$a^2o-~!$F4gR6j&?Rh$lvCSymjKfmn7+=tW&nk-hcL>7@uNL3BIbytuN>hVU**Xf z<@ZNXwP(AEUyDAXe7Ig6v@|tU9PL+v?FQV=)shjYId4PsG+JfJ9yI`9M-*V6R4b*@t3$-sgk+5FS!Is*^TdIzx_rTGxVf^?Gj3~DU2(HkJ zalExJ3+na>U?Dy*s!dd3cD@Mnfm?L`7gSFDr_kJ6l}g4c)Ado~S0;#Y&$JN}B)8vm zl@(W2VECyvEI9P$BZ)C67ubsynuEchcsV>Y_~x)Ua%pU6I3k`K92*-R5@BP(Qm!wf zLjiHYUDkz9J0?;oE+yxsL_nmuSK+wmIV3|s7V1W@5|teU#AG74VC)vtu~{j&VC@#? zVpK{i&}@Ql`kA6lx)o1l6VhZhElbM6k!l}_WLyzD_X+p`hqGGdhIBh81Jm?|q$tVs zw79^DL68}o!hQ)RV~RpcAP%3)%+4kiMM{YAnOJ&Sl0_vWVl;2XWT~P;r70@OGbI&I zRHDhLWIU!MGimW=R-(7d8&$`aJ9p`|@YpaMATa%oqG66=HNSudqW34PpmeXlpMYi2 zTx#u!?Ce~T#}-xOAr!f2NGj5-Tr`%E(*WvXJ*NXe{H9V-A45hb5@@{awS+SH2bOHpz@RL7-TCzxY3V>+#mRwM`mw+7 z-dMk}cI`p;NAug~uWX*bqK%Aerio`9YY=wKq+Z;1c5OPl9=J4T*Ov3_Vn{cAV(re_ ztY#i4@Iigua$i{uZTosReZ5=0Z*H~rX)Uj4<}(F8@WkD=?LNBcKKg~*|Bg9#k9%yg zEt~T7c|-n<)rM80X74PR{EtnJWg&n03sd{9fq1%}nIN!7=H`6#gNgSh@}bqY*XBRG zvkpEV3_KLJFN|+q7}w5EXr}L$aDuq?ZfGgAEEIUpBi?fN;?l(e@6w_93;eMsz`C?_ zX?Y}9;Jv{8KTMm6%lmHsO8@=&)!SS3FE5Vi>TRptKkNBPkJfhbS4SUoZwEq~fsl4) z?BSR;A#G1&HYYOL_?%|Csl$6F=+^}I@{;X|rEc*xy)j<&x2Q|C^SRPfQlObEd=aGw zo}QZc4Ab93MSdYXIZ<5o$BfdWp-*eW)u-4}U=&nv&YN~FdX9w8+9XDZ3cbk`*@1?hx!@nO2 WJq&FPjsG3n`csauEq`Mx4gTMUppY5> delta 998 zcmah|K~EDw6rR~-yKQN=g^FDYLJL^CqL4shBM8PwNj!+8XgooK>>!&gT{5$9@S@@F zfs8RRFd8EDvwm*5d(qnaSkrn! zbD0xMJ% zWS%nrjL`_aCcpukB)*W=&qSV<%dp`UfdADrDNCLGv><0Y_iv9PA&!6#&c~3dgerL! zY=KW)K1-i+9nsD&_zk$3kbSA^9R;xDmT|v|%?*nP13;WkJUFk#Q}Qr14029VN7F?h z1nR172eCD8bJOtyowIe$#D=B*iyS2EC+*IKkBtf?HFDYv@NddqqXzLB#!94U993ZB zEhSK$rhl%YIeB8BA~SPjI7x5QMxmQFrF45Xhwt#6m+v3EeE;C*FOI;gXY6Az820mIpcXmAZ^!)7j%{S*qzntD3pZ$q^dH=h?qk%a%{r%+ay{9K%Kf3wkuYq~|`XT@E zyZbL*Rpg!d;1lA98Af;1hg2#PKn}iGON7`r-KG z;b8aU^IMJ`U-v5s;ssGhwF7Dr?ncOqcC_O)PCV@6hJm1U)tSm7?z-CQv{t1Ybf>z$ zK2lY`v4MUFOc984j&;mB(L#)jReWY)A_^>uaT~2-7dgV~bknh2H}gE%@8~S8nTwe# zNLF;cWVmcpnp(e_G0A17ul=h7RyJv{oRvAMPE$w9AZ6~S5xVS)RjMO0rpt-k1Tu&4 z24G6vnR?-+S57bQXEsf&&s>g^vvp(t7;bH`rKV_qs+X z{nP=(`eJ8PYSJxRd6Bn9AW?$cdrScd@$9M!xz_i$_)_p(99j6*a?Z}@CBK1le+5&a zUY3G8M)J-UE2=7>SkrXViA_VAT{J=g==pkDY}}?*P^^dz1aqH7?x8WNH8;h?d3Isr zIafndUE;_9e|}rdKfnEzKmFG)YJyS|j2w~x1}Js+j(>aanh!|@3Oc7%$C4=NJu@b9 zl?^peBRV;w<;_&pow!N2GMdY)f(caLDm5pu>8VVGE$;Uv#G5vy^AaTN;gNJ{*J31= zm!*m+y6ab;vrreB_5JUkrXA8qJ}y^ zQJ{-~2A=u{vURD4{DN#*I&{y^DA1)w*Ec-i4e#~G$FDy={_U%iS8u)^eg3L<^^@Q4 z^?IilXT8rZFV5b5dwKGE|NiXaAM~3C{IqBF`hT6jKYelj^6BlT|Msl2w@>&-A3pj1 z&6d2ke*E*~8OB_q_yD5@1abvX5~0ZZ7sF>KUt-iHh7JHTz`zxSVkl1k(m#LFtAFM1 zgb{Ufv|Xjna@|gpb>YZr8;|W8(oKtv`5{(7K1Sri=s2B*f?bq$0HcMdIkie-mYW4q zUgzq}RGe0{GRO=TtGV0k^4ZKjG#g8qMjQk%6gJAacaX1FoZ;zAWfOI<6ZmVTSwc55 zTEJ6{G@G@CfiaxNdbvai?kc1TYmNPcrRO3*A5tiBI%G zPG)`yAml;4Kh_;yio$VYZ;zf?MKst0Lf+4SVH$8O%$Ie8M^X*&N-(*0LA5(43j+&R zG(dv$2PSw{FinKaHnZpn-H3-QwD4C1vy&T5y((ybmtA&#Dxx|@qb?eAO ziLH#(<#UKD7uj&(4}n=SxMO1Y#1sQ{C#}<>i}oPVo5X&Z!`4um=SxQykEq2NmCBpG zUvEWW&*RAtccu9J?hn3y_l3}ZM3W3a?mzJFPj4IurnWVGXkyVUc?7T}fFrI3t57ka zfEDL5G8BU#@qmb};*1qo6QWv5-vx_i7fH6aUtYW3(q9yuv~H2u!^P})tw^)33tewi zx2Q&Jr%6LFwq|a_`dBK06eoCRJ4ZL!1SOZIhEYIhco3TrvqouWXEGbVQz);ZNXJu` zZW1Az++wIio|u~E>@mviS^;O7PI8s*D6ok)3*{-&LRC3&k^n%qU?B0xS4sukZN=~Y mj|L}C58 { 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() { )} -
); }