Add FolderUpdate model and folder update endpoint
This commit is contained in:
parent
fb461df550
commit
b27604130a
9 changed files with 230 additions and 93 deletions
Binary file not shown.
|
|
@ -67,3 +67,8 @@ class NoteUpdate(SQLModel):
|
||||||
class FolderCreate(SQLModel):
|
class FolderCreate(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
parent_id: Optional[int] = None
|
parent_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FolderUpdate(SQLModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
parent_id: Optional[int] = None
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -9,6 +9,7 @@ from app.models import (
|
||||||
FolderCreate,
|
FolderCreate,
|
||||||
FolderTreeNode,
|
FolderTreeNode,
|
||||||
FolderTreeResponse,
|
FolderTreeResponse,
|
||||||
|
FolderUpdate,
|
||||||
Note,
|
Note,
|
||||||
NoteRead,
|
NoteRead,
|
||||||
)
|
)
|
||||||
|
|
@ -74,3 +75,52 @@ def delete_folder(folder_id: int, session: Session = Depends(get_session)):
|
||||||
session.delete(folder)
|
session.delete(folder)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {"message": "Folder deleted"}
|
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
|
||||||
|
|
|
||||||
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
|
|
@ -36,6 +36,11 @@ export interface FolderCreate {
|
||||||
parent_id: number | null;
|
parent_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FolderUpdate {
|
||||||
|
name?: string;
|
||||||
|
parent_id?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
const getFolderTree = async () => {
|
const getFolderTree = async () => {
|
||||||
const { data } = await axios.get<FolderTreeResponse>(
|
const { data } = await axios.get<FolderTreeResponse>(
|
||||||
`${API_URL}/folders/tree`,
|
`${API_URL}/folders/tree`,
|
||||||
|
|
@ -46,10 +51,24 @@ const getFolderTree = async () => {
|
||||||
return decryptedFolderTree;
|
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 = {
|
export const folderApi = {
|
||||||
tree: () => getFolderTree(),
|
tree: () => getFolderTree(),
|
||||||
list: () => axios.get<Folder[]>(`${API_URL}/folders`),
|
list: () => axios.get<Folder[]>(`${API_URL}/folders`),
|
||||||
create: (folder: FolderCreate) =>
|
create: (folder: FolderCreate) =>
|
||||||
axios.post<Folder>(`${API_URL}/folders`, folder),
|
axios.post<Folder>(`${API_URL}/folders`, folder),
|
||||||
delete: (id: number) => axios.delete(`${API_URL}/folders/${id}`),
|
delete: (id: number) => axios.delete(`${API_URL}/folders/${id}`),
|
||||||
|
update: (id: number, updateData: FolderUpdate) =>
|
||||||
|
updateFolder(id, updateData),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDroppable } from "@dnd-kit/core";
|
import { useDroppable, useDraggable } from "@dnd-kit/core";
|
||||||
import { Folder, NoteRead } from "../../api/folders";
|
import { Folder, NoteRead } from "../../api/folders";
|
||||||
|
|
||||||
export const DroppableFolder = ({
|
export const DroppableFolder = ({
|
||||||
|
|
@ -17,12 +17,33 @@ export const DroppableFolder = ({
|
||||||
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
collapse: boolean;
|
collapse: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { isOver, setNodeRef } = useDroppable({
|
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
||||||
id: folder.id!,
|
id: folder.id!,
|
||||||
data: { type: "folder", folder },
|
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 = {
|
const style = {
|
||||||
color: isOver ? "green" : undefined,
|
color: isOver ? "green" : undefined,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
transform: transform
|
||||||
|
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,10 +56,18 @@ export const DroppableFolder = ({
|
||||||
? "bg-ctp-surface1"
|
? "bg-ctp-surface1"
|
||||||
: "hover:bg-ctp-surface0"
|
: "hover:bg-ctp-surface0"
|
||||||
}`}
|
}`}
|
||||||
|
{...listeners}
|
||||||
|
{...attributes}
|
||||||
>
|
>
|
||||||
<i className="fadr fa-folder text-sm"></i>
|
<i className="fadr fa-folder text-sm"></i>
|
||||||
{folder.name}
|
{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
|
x
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@ import {
|
||||||
import { DraggableNote } from "./DraggableNote";
|
import { DraggableNote } from "./DraggableNote";
|
||||||
import { DroppableFolder } from "./DroppableFolder";
|
import { DroppableFolder } from "./DroppableFolder";
|
||||||
import { useNoteStore } from "../../stores/notesStore";
|
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 }) => {
|
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||||
|
|
@ -47,71 +55,124 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||||
setNewFolder(false);
|
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 (
|
return (
|
||||||
<div
|
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
|
||||||
className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-col gap-3"
|
<div
|
||||||
onDragOver={(e) => e.preventDefault()}
|
className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-col gap-3"
|
||||||
onTouchMove={(e) => e.preventDefault()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
>
|
onTouchMove={(e) => e.preventDefault()}
|
||||||
<SidebarHeader
|
>
|
||||||
clearSelection={clearSelection}
|
<SidebarHeader
|
||||||
setNewFolder={setNewFolder}
|
clearSelection={clearSelection}
|
||||||
/>
|
setNewFolder={setNewFolder}
|
||||||
{/* New folder input */}
|
/>
|
||||||
{newFolder && (
|
{/* New folder input */}
|
||||||
<div className="mb-2">
|
{newFolder && (
|
||||||
<input
|
<div className="mb-2">
|
||||||
onBlur={() => setNewFolder(false)}
|
<input
|
||||||
onChange={(e) => setNewFolderText(e.target.value)}
|
onBlur={() => setNewFolder(false)}
|
||||||
value={newFolderText}
|
onChange={(e) => setNewFolderText(e.target.value)}
|
||||||
type="text"
|
value={newFolderText}
|
||||||
placeholder="Folder name..."
|
type="text"
|
||||||
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"
|
placeholder="Folder name..."
|
||||||
ref={newFolderRef}
|
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"
|
||||||
onKeyDown={(e) => {
|
ref={newFolderRef}
|
||||||
if (e.key === "Enter") {
|
onKeyDown={(e) => {
|
||||||
handleCreateFolder();
|
if (e.key === "Enter") {
|
||||||
}
|
handleCreateFolder();
|
||||||
if (e.key === "Escape") {
|
}
|
||||||
setNewFolder(false);
|
if (e.key === "Escape") {
|
||||||
}
|
setNewFolder(false);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Folder tree */}
|
{/* Folder tree */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{folderTree?.folders.map((folder) => (
|
{folderTree?.folders.map((folder) => (
|
||||||
<RenderFolder
|
<RenderFolder
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
depth={0}
|
depth={0}
|
||||||
setSelectedFolder={setSelectedFolder}
|
setSelectedFolder={setSelectedFolder}
|
||||||
selectedFolder={selectedFolder}
|
selectedFolder={selectedFolder}
|
||||||
selectedNote={selectedNote}
|
|
||||||
selectNote={setSelectedNote}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Orphaned notes */}
|
|
||||||
{folderTree?.orphaned_notes && folderTree.orphaned_notes.length > 0 && (
|
|
||||||
<div className="mt-4 flex flex-col gap-1">
|
|
||||||
{/*<div className="text-ctp-subtext0 text-sm font-medium mb-1 px-2">
|
|
||||||
Unsorted
|
|
||||||
</div>*/}
|
|
||||||
{folderTree.orphaned_notes.map((note) => (
|
|
||||||
<DraggableNote
|
|
||||||
key={note.id}
|
|
||||||
note={note}
|
|
||||||
selectNote={setSelectedNote}
|
|
||||||
selectedNote={selectedNote}
|
selectedNote={selectedNote}
|
||||||
|
selectNote={setSelectedNote}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* Orphaned notes */}
|
||||||
|
{folderTree?.orphaned_notes && folderTree.orphaned_notes.length > 0 && (
|
||||||
|
<div className="mt-4 flex flex-col gap-1">
|
||||||
|
{/*<div className="text-ctp-subtext0 text-sm font-medium mb-1 px-2">
|
||||||
|
Unsorted
|
||||||
|
</div>*/}
|
||||||
|
{folderTree.orphaned_notes.map((note) => (
|
||||||
|
<DraggableNote
|
||||||
|
key={note.id}
|
||||||
|
note={note}
|
||||||
|
selectNote={setSelectedNote}
|
||||||
|
selectedNote={selectedNote}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ import {
|
||||||
import "@mdxeditor/editor/style.css";
|
import "@mdxeditor/editor/style.css";
|
||||||
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
|
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
|
||||||
import { DraggableNote } from "../components/sidebar/DraggableNote";
|
import { DraggableNote } from "../components/sidebar/DraggableNote";
|
||||||
|
// @ts-ignore
|
||||||
import CheckIcon from "../assets/fontawesome/svg/circle-check.svg?react";
|
import CheckIcon from "../assets/fontawesome/svg/circle-check.svg?react";
|
||||||
|
// @ts-ignore
|
||||||
import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react";
|
import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react";
|
||||||
import { useNoteStore } from "../stores/notesStore";
|
import { useNoteStore } from "../stores/notesStore";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
@ -83,12 +85,7 @@ function Home() {
|
||||||
selectedNote,
|
selectedNote,
|
||||||
} = useNoteStore();
|
} = useNoteStore();
|
||||||
|
|
||||||
const pointer = useSensor(PointerSensor, {
|
|
||||||
activationConstraint: {
|
|
||||||
distance: 30,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const sensors = useSensors(pointer);
|
|
||||||
|
|
||||||
const newFolderRef = useRef<HTMLInputElement>(null);
|
const newFolderRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -139,19 +136,10 @@ function Home() {
|
||||||
setContent("");
|
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 (
|
return (
|
||||||
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
|
|
||||||
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
|
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
|
|
||||||
|
|
@ -188,7 +176,6 @@ function Home() {
|
||||||
<>
|
<>
|
||||||
<UndoRedo />
|
<UndoRedo />
|
||||||
<BoldItalicUnderlineToggles />
|
<BoldItalicUnderlineToggles />
|
||||||
<DiffSourceToggleWrapper />
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
@ -251,19 +238,6 @@ function Home() {
|
||||||
Create Note
|
Create Note
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -286,7 +260,6 @@ function Home() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DndContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue