diff --git a/.gitignore b/.gitignore index a1a24f0..58926dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules frontend/src/assets/fontawesome/svg/* frontend/src/assets/fontawesome/svg/0.svg *.db +.zed/settings.json diff --git a/backend/app/routes/__pycache__/tags.cpython-314.pyc b/backend/app/routes/__pycache__/tags.cpython-314.pyc index b35bedb..bd6a99d 100644 Binary files a/backend/app/routes/__pycache__/tags.cpython-314.pyc and b/backend/app/routes/__pycache__/tags.cpython-314.pyc differ diff --git a/backend/app/routes/tags.py b/backend/app/routes/tags.py index 8e103ed..566c4eb 100644 --- a/backend/app/routes/tags.py +++ b/backend/app/routes/tags.py @@ -1,7 +1,20 @@ +from tkinter.constants import TOP + from app.auth import require_auth from app.database import get_session -from app.models import Note, NoteCreate, NoteTag, NoteUpdate, Tag, TagCreate, User +from app.models import ( + Note, + NoteCreate, + NoteTag, + NoteUpdate, + Tag, + TagCreate, + TagTreeNode, + TagTreeResponse, + User, +) from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import selectinload from sqlmodel import Session, select router = APIRouter(prefix="/tags", tags=["tags"]) @@ -27,6 +40,27 @@ def create_tag( return db_tag +def build_tag_tree_node(tag: Tag) -> TagTreeNode: + return TagTreeNode( + id= tag.id, + name = tag.name, + children = [build_tag_tree_node(child) for child in tag.children] + ) + + + +@router.get("/tree") +def get_tag_tree(session: Session = Depends(get_session)): + top_level_tags = session.exec( + select(Tag) + .options(selectinload(Tag.children)) + .where(Tag.parent_id == None) + ).all() + + tree = [build_tag_tree_node(tag) for tag in top_level_tags] + return TagTreeResponse(tags=tree) + + @router.post("/note/{note_id}/tag/{tag_id}") def add_tag_to_note( note_id: int, diff --git a/frontend/src/api/encryption.tsx b/frontend/src/api/encryption.tsx index 3929f72..cd10770 100644 --- a/frontend/src/api/encryption.tsx +++ b/frontend/src/api/encryption.tsx @@ -1,4 +1,5 @@ import { FolderTreeResponse, FolderTreeNode } from "./folders"; +import { Tag } from "./tags"; export async function deriveKey(password: string, salt: string) { const enc = new TextEncoder(); @@ -147,3 +148,25 @@ export async function decryptFolderTree( ), }; } + +export const decryptTagTree = async ( + tags: Tag[], + key: CryptoKey, + parentPath = "", +): Promise => { + return Promise.all( + tags.map(async (tag) => { + const decryptedName = await decryptString(tag.name, key); + const currentPath = parentPath + ? `${parentPath} › ${decryptedName}` + : decryptedName; + + return { + ...tag, + name: decryptedName, + parent_path: parentPath, + children: await decryptTagTree(tag.children, key, currentPath), + }; + }), + ); +}; diff --git a/frontend/src/api/notes.tsx b/frontend/src/api/notes.tsx index 2f4a481..8bb6caa 100644 --- a/frontend/src/api/notes.tsx +++ b/frontend/src/api/notes.tsx @@ -51,10 +51,12 @@ const fetchNotes = async () => { ...note, title: await decryptString(note.title, encryptionKey), content: await decryptString(note.content, encryptionKey), - tags: note.tags.map(async (tag) => ({ - ...tag, - name: await decryptString(tag.name, encryptionKey), - })), + tags: await Promise.all( + note.tags.map(async (tag) => ({ + ...tag, + name: await decryptString(tag.name, encryptionKey), + })), + ), })), ); return decryptedNotes; diff --git a/frontend/src/api/tags.tsx b/frontend/src/api/tags.tsx index d4422ef..6544137 100644 --- a/frontend/src/api/tags.tsx +++ b/frontend/src/api/tags.tsx @@ -1,5 +1,5 @@ import axios from "axios"; -import { encryptString, decryptString } from "./encryption"; +import { encryptString, decryptTagTree } from "./encryption"; import { useAuthStore } from "../stores/authStore"; axios.defaults.withCredentials = true; const API_URL = (import.meta as any).env.PROD @@ -20,42 +20,13 @@ export interface TagCreate { parent_id?: number; } -const buildTagTree = ( - tags: Tag[], - parent_id: string | number | null = null, - parentPath = "", -): Tag[] => { - const result: Tag[] = []; - for (const tag of tags) { - if (tag.parent_id == parent_id) { - tag.parent_path = parentPath; - - const currentPath = parentPath ? `${parentPath} › ${tag.name}` : tag.name; - - tag.children = buildTagTree(tags, tag.id, currentPath); - result.push(tag); - } - } - return result; -}; - const fetchTags = async () => { const encryptionKey = useAuthStore.getState().encryptionKey; if (!encryptionKey) throw new Error("Not authenticated"); - const { data } = await axios.get(`${API_URL}/tags/`); - - const decryptedTags = await Promise.all( - data.map(async (tag: Tag) => ({ - ...tag, - name: await decryptString(tag.name, encryptionKey), - })), - ); - - const tags = buildTagTree(decryptedTags); - - console.log(tags); - + const { data } = await axios.get(`${API_URL}/tags/tree`); + const tags = decryptTagTree(data.tags, encryptionKey); + console.log(await tags); return tags; }; diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx index a4fdd51..0cdd481 100644 --- a/frontend/src/pages/Home/Home.tsx +++ b/frontend/src/pages/Home/Home.tsx @@ -105,20 +105,19 @@ function Home() { {showModal && } + {/*
setTagName(e.target.value)} /> - {tags.map((tag) => ( ))}
*/} - {/* Main editor area */}
@@ -178,16 +177,16 @@ const Modal = () => { ); }; -const TagSelector = () => { +export const TagSelector = () => { const { tagTree } = useTagStore(); const [value, setValue] = useState(""); return (
- setValue(e.target.value)} - /> + />*/} {tagTree && tagTree.map((tag) => )}
); diff --git a/frontend/src/pages/Home/components/sidebar/SideBar.tsx b/frontend/src/pages/Home/components/sidebar/SideBar.tsx index 9e8578a..5f31d45 100644 --- a/frontend/src/pages/Home/components/sidebar/SideBar.tsx +++ b/frontend/src/pages/Home/components/sidebar/SideBar.tsx @@ -2,6 +2,8 @@ import React, { useState, useRef, useEffect, SetStateAction } from "react"; // @ts-ignore import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react"; +// @ts-ignore +import TagsIcon from "@/assets/fontawesome/svg/tags.svg?react"; import { DraggableNote } from "./subcomponents/DraggableNote"; import { @@ -19,6 +21,7 @@ import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx"; import { useAuthStore } from "@/stores/authStore.ts"; import { useNoteStore } from "@/stores/notesStore.ts"; import { useUIStore } from "@/stores/uiStore.ts"; +import { TagSelector } from "../../Home.tsx"; export const Sidebar = () => { const [newFolder, setNewFolder] = useState(false); @@ -39,7 +42,8 @@ export const Sidebar = () => { const { encryptionKey } = useAuthStore(); - const { setSideBarResize, sideBarResize } = useUIStore(); + const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } = + useUIStore(); useEffect(() => { if (newFolder && newFolderRef.current) { newFolderRef.current.focus(); @@ -169,65 +173,73 @@ export const Sidebar = () => { style={{ width: `${sideBarResize}px` }} > -
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="standard-input" - ref={newFolderRef} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCreateFolder(); - } - if (e.key === "Escape") { - setNewFolder(false); - } - }} - /> -
- )} + {sideBarView == "folders" ? ( + <> +
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="standard-input" + 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 && ( -
- {folderTree.orphaned_notes.map((note) => ( - + {/* Folder tree */} +
+ {folderTree?.folders.map((folder) => ( + ))}
- )} -
- - {activeItem?.type === "note" && ( -
- {activeItem.data.title} + {/* Orphaned notes */} + {folderTree?.orphaned_notes && + folderTree.orphaned_notes.length > 0 && ( +
+ {folderTree.orphaned_notes.map((note) => ( + + ))} +
+ )}
- )} - {activeItem?.type === "folder" && ( -
- - {activeItem.data.name} -
- )} -
+ + + {activeItem?.type === "note" && ( +
+ {activeItem.data.title} +
+ )} + {activeItem?.type === "folder" && ( +
+ + {activeItem.data.name} +
+ )} +
+ + ) : ( +
+ +
+ )}
diff --git a/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx b/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx index 2af39d4..218edfc 100644 --- a/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx +++ b/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx @@ -2,8 +2,11 @@ import { SetStateAction } from "react"; // @ts-ignore import FolderPlusIcon from "@assets/fontawesome/svg/folder-plus.svg?react"; // @ts-ignore +import TagsIcon from "@assets/fontawesome/svg/tags.svg?react"; +// @ts-ignore import FileCirclePlusIcon from "@assets/fontawesome/svg/file-circle-plus.svg?react"; import { useNoteStore } from "@/stores/notesStore"; +import { useUIStore } from "@/stores/uiStore"; export const SidebarHeader = ({ setNewFolder, @@ -11,6 +14,7 @@ export const SidebarHeader = ({ setNewFolder: React.Dispatch>; }) => { const { createNote, selectedFolder } = useNoteStore(); + const { setSideBarView, sideBarView } = useUIStore(); const handleCreate = async () => { await createNote({ title: "Untitled", @@ -22,17 +26,26 @@ export const SidebarHeader = ({
+
); diff --git a/frontend/src/stores/notesStore.ts b/frontend/src/stores/notesStore.ts index 37dfd4e..abca07b 100644 --- a/frontend/src/stores/notesStore.ts +++ b/frontend/src/stores/notesStore.ts @@ -78,7 +78,7 @@ export const useNoteStore = create()( (set, get) => ({ loadFolderTree: async () => { const data = await folderApi.tree(); - console.log(data); + // console.log(data); set({ folderTree: data }); }, folderTree: null, diff --git a/frontend/src/stores/uiStore.ts b/frontend/src/stores/uiStore.ts index 9f36006..83e5659 100644 --- a/frontend/src/stores/uiStore.ts +++ b/frontend/src/stores/uiStore.ts @@ -10,6 +10,9 @@ interface UIState { sideBarResize: number; setSideBarResize: (size: number) => void; + + sideBarView: string; + setSideBarView: (view: string) => void; } export const useUIStore = create()( @@ -27,6 +30,10 @@ export const useUIStore = create()( setSideBarResize: (size) => { set({ sideBarResize: size }); }, + sideBarView: "folders", + setSideBarView: (view) => { + set({ sideBarView: view }); + }, }), { name: "ui-store",