Add FolderUpdate model and folder update endpoint

This commit is contained in:
james fitzsimons 2025-11-30 19:40:10 +00:00
parent fb461df550
commit b27604130a
9 changed files with 230 additions and 93 deletions

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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<FolderTreeResponse>(
`${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<Folder[]>(`${API_URL}/folders`),
create: (folder: FolderCreate) =>
axios.post<Folder>(`${API_URL}/folders`, folder),
delete: (id: number) => axios.delete(`${API_URL}/folders/${id}`),
update: (id: number, updateData: FolderUpdate) =>
updateFolder(id, updateData),
};

View file

@ -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<React.SetStateAction<boolean>>;
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}
>
<i className="fadr fa-folder text-sm"></i>
{folder.name}
<div onClick={() => setCollapse(!collapse)} className="ml-auto">
<div
onClick={(e) => {
e.stopPropagation(); // Prevent dragging when clicking the collapse button
setCollapse(!collapse);
}}
className="ml-auto"
>
x
</div>
</div>

View file

@ -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<FolderTreeResponse | null>(null);
@ -47,7 +55,59 @@ 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 (
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
<div
className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-col gap-3"
onDragOver={(e) => e.preventDefault()}
@ -112,6 +172,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
</div>
)}
</div>
</DndContext>
);
};

View file

@ -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<HTMLInputElement>(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 (
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
{/* Sidebar */}
@ -188,7 +176,6 @@ function Home() {
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
<DiffSourceToggleWrapper />
</>
),
}),
@ -251,19 +238,6 @@ function Home() {
Create Note
</button>
)}
{/* Encryption toggle */}
{/*<label className="flex items-center gap-2 ml-auto cursor-pointer group">
<input
type="checkbox"
checked={encrypted}
onChange={() => setEncrypted(!encrypted)}
className="w-4 h-4 rounded border-ctp-surface2 text-ctp-mauve focus:ring-ctp-mauve focus:ring-offset-ctp-base cursor-pointer"
/>
<span className="text-sm text-ctp-subtext0 group-hover:text-ctp-text transition-colors">
Encrypt
</span>
</label>*/}
</div>
</div>
@ -286,7 +260,6 @@ function Home() {
)}
</div>
</div>
</DndContext>
);
}