From d01b1a03cba64c96ab63b7c14729410471c1a4bb Mon Sep 17 00:00:00 2001 From: LukeGus Date: Tue, 26 May 2026 23:02:19 -0500 Subject: [PATCH] feat: add snippets features --- src/ui/locales/en.json | 9 ++ src/ui/sidebar/SnippetsPanel.tsx | 264 ++++++++++++++++++++++++++++--- 2 files changed, 250 insertions(+), 23 deletions(-) diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index d0a732e4..acbb01b8 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1847,6 +1847,15 @@ "deleteFailed": "Failed to delete snippet", "folderCreateSuccess": "Folder created successfully", "folderCreateFailed": "Failed to create folder", + "editFolderTitle": "Edit Folder", + "editFolderDescription": "Rename or change the appearance of this folder", + "saveFolderButton": "Save Changes", + "editFolder": "Edit folder", + "deleteFolder": "Delete folder", + "folderDeleteSuccess": "Folder \"{{name}}\" deleted", + "folderDeleteFailed": "Failed to delete folder", + "folderEditSuccess": "Folder updated successfully", + "folderEditFailed": "Failed to update folder", "runSuccess": "Ran \"{{name}}\" in {{count}} terminal(s)", "copySuccess": "Copied \"{{name}}\" to clipboard", "shareTitle": "Share Snippet", diff --git a/src/ui/sidebar/SnippetsPanel.tsx b/src/ui/sidebar/SnippetsPanel.tsx index a8f78883..beb28def 100644 --- a/src/ui/sidebar/SnippetsPanel.tsx +++ b/src/ui/sidebar/SnippetsPanel.tsx @@ -7,6 +7,9 @@ import { deleteSnippet as apiDeleteSnippet, getSnippetFolders, createSnippetFolder as apiCreateSnippetFolder, + deleteSnippetFolder as apiDeleteSnippetFolder, + renameSnippetFolder as apiRenameSnippetFolder, + updateSnippetFolderMetadata as apiUpdateSnippetFolderMetadata, shareSnippet as apiShareSnippet, getSnippetAccess, revokeSnippetAccess, @@ -43,7 +46,14 @@ import { Trash2, UserPlus, X, + MoreHorizontal, } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/dropdown-menu"; import { toast } from "sonner"; import { FOLDER_COLORS } from "@/lib/theme"; import { FOLDER_ICONS } from "@/types/ui-types"; @@ -354,6 +364,130 @@ function CreateFolderDialog({ ); } +function EditFolderDialog({ + open, + onOpenChange, + folder, + onSave, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; + folder: SnippetFolder | null; + onSave: ( + oldName: string, + data: { name: string; color: string; icon: FolderIconId }, + ) => void; +}) { + const { t } = useTranslation(); + const [name, setName] = useState(""); + const [color, setColor] = useState(FOLDER_COLORS[0]); + const [icon, setIcon] = useState("folder"); + + useEffect(() => { + if (open && folder) { + setName(folder.name); + setColor(folder.color ?? FOLDER_COLORS[0]); + setIcon(folder.icon ?? "folder"); + } + }, [open, folder]); + + function handleSave() { + if (!name.trim() || !folder) return; + onSave(folder.name, { name: name.trim(), color, icon }); + onOpenChange(false); + } + + return ( + + + + + {t("newUi.sidebar.snippets.editFolderTitle")} + + + {t("newUi.sidebar.snippets.editFolderDescription")} + + +
+
+ + setName(e.target.value)} + /> +
+
+ +
+ {FOLDER_COLORS.map((c) => ( +
+
+
+ +
+ {FOLDER_ICONS.map((ic) => ( + + ))} +
+
+
+ +
+ + + {name || t("newUi.sidebar.snippets.folderNameFallback")} + +
+
+
+
+ + +
+
+
+ ); +} + type AccessRecord = { id: number; targetType: "user" | "role"; @@ -386,7 +520,6 @@ function ShareSnippetDialog({ setLoading(true); Promise.all([getUserList(), getRoles(), getSnippetAccess(snippet.id)]) .then(([usersData, rolesData, accessData]) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any setUsers( (usersData?.users || []).map((u: any) => ({ id: u.id, @@ -679,6 +812,8 @@ export function SnippetsPanel({ const [snippetFormOpen, setSnippetFormOpen] = useState(false); const [editingSnippet, setEditingSnippet] = useState(null); const [createFolderOpen, setCreateFolderOpen] = useState(false); + const [editFolder, setEditFolder] = useState(null); + const [editFolderOpen, setEditFolderOpen] = useState(false); const [shareSnippet, setShareSnippet] = useState(null); const [selectedTabIds, setSelectedTabIds] = useState>( () => @@ -762,7 +897,6 @@ export function SnippetsPanel({ async function handleCreateFolder(f: Omit) { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const created = (await apiCreateSnippetFolder({ name: f.name, color: f.color, @@ -781,6 +915,54 @@ export function SnippetsPanel({ ); } + async function handleDeleteFolder(folder: SnippetFolder) { + try { + await apiDeleteSnippetFolder(folder.name); + setFolders((prev) => prev.filter((f) => f.id !== folder.id)); + setSnippets((prev) => + prev.map((s) => + s.folder === folder.name ? { ...s, folder: null } : s, + ), + ); + toast.success( + t("newUi.sidebar.snippets.folderDeleteSuccess", { name: folder.name }), + ); + } catch { + toast.error(t("newUi.sidebar.snippets.folderDeleteFailed")); + } + } + + async function handleSaveFolder( + oldName: string, + data: { name: string; color: string; icon: FolderIconId }, + ) { + try { + const nameChanged = data.name !== oldName; + if (nameChanged) { + await apiRenameSnippetFolder(oldName, data.name); + setSnippets((prev) => + prev.map((s) => + s.folder === oldName ? { ...s, folder: data.name } : s, + ), + ); + } + await apiUpdateSnippetFolderMetadata(nameChanged ? data.name : oldName, { + color: data.color, + icon: data.icon, + }); + setFolders((prev) => + prev.map((f) => + f.name === oldName + ? { ...f, name: data.name, color: data.color, icon: data.icon } + : f, + ), + ); + toast.success(t("newUi.sidebar.snippets.folderEditSuccess")); + } catch { + toast.error(t("newUi.sidebar.snippets.folderEditFailed")); + } + } + async function handleDeleteSnippet(id: number) { try { await apiDeleteSnippet(id); @@ -956,28 +1138,55 @@ export function SnippetsPanel({ if (folderSnippets.length === 0 && snippetSearch) return null; return (
- + + + + {folder.name} + + + {folderSnippets.length} + + + + + + + + { + setEditFolder(folder); + setEditFolderOpen(true); + }} + > + + {t("newUi.sidebar.snippets.editFolder")} + + handleDeleteFolder(folder)} + className="text-destructive focus:text-destructive" + > + + {t("newUi.sidebar.snippets.deleteFolder")} + + + +
{folder.open && (
{folderSnippets.map((snippet) => ( @@ -1020,6 +1229,15 @@ export function SnippetsPanel({ onOpenChange={setCreateFolderOpen} onCreate={handleCreateFolder} /> + { + setEditFolderOpen(v); + if (!v) setEditFolder(null); + }} + folder={editFolder} + onSave={handleSaveFolder} + /> setShareSnippet(null)}