From 64b4da40353d08668118796d631f1ed9db65ea55 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sun, 26 Apr 2026 19:28:49 +0800 Subject: [PATCH] feat: add folder creation date and sorting functionality in Vault components --- src/handlers/folders.ts | 1 + src/handlers/sync.ts | 1 + src/types/index.ts | 1 + webapp/src/components/VaultPage.tsx | 50 +++++++++++++ webapp/src/components/vault/VaultSidebar.tsx | 72 ++++++++++++++++++- .../components/vault/vault-page-helpers.tsx | 1 + webapp/src/lib/types.ts | 2 + webapp/src/styles/dark.css | 1 + webapp/src/styles/vault.css | 21 ++++++ 9 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/handlers/folders.ts b/src/handlers/folders.ts index 0cfd6da..b82dd2b 100644 --- a/src/handlers/folders.ts +++ b/src/handlers/folders.ts @@ -21,6 +21,7 @@ function folderToResponse(folder: Folder): FolderResponse { id: folder.id, name: folder.name, revisionDate: folder.updatedAt, + creationDate: folder.createdAt, object: 'folder', }; } diff --git a/src/handlers/sync.ts b/src/handlers/sync.ts index e7b1d31..6720a21 100644 --- a/src/handlers/sync.ts +++ b/src/handlers/sync.ts @@ -93,6 +93,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr id: folder.id, name: folder.name, revisionDate: folder.updatedAt, + creationDate: folder.createdAt, object: 'folder', }); } diff --git a/src/types/index.ts b/src/types/index.ts index 3803041..583c5ee 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -450,6 +450,7 @@ export interface FolderResponse { id: string; name: string; revisionDate: string; + creationDate: string; object: string; } diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index b8fb51e..3819eee 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -8,6 +8,7 @@ import { MOBILE_LAYOUT_QUERY, VAULT_LIST_OVERSCAN, VAULT_LIST_ROW_HEIGHT, + FOLDER_SORT_STORAGE_KEY, VAULT_SORT_STORAGE_KEY, cipherTypeKey, cipherTypeLabel, @@ -72,6 +73,8 @@ export default function VaultPage(props: VaultPageProps) { const [searchComposing, setSearchComposing] = useState(false); const [sortMode, setSortMode] = useState('edited'); const [sortMenuOpen, setSortMenuOpen] = useState(false); + const [folderSortMode, setFolderSortMode] = useState('name'); + const [folderSortMenuOpen, setFolderSortMenuOpen] = useState(false); const [sidebarFilter, setSidebarFilter] = useState({ kind: 'all' }); const [selectedCipherId, setSelectedCipherId] = useState(''); const [selectedMap, setSelectedMap] = useState>({}); @@ -111,6 +114,7 @@ export default function VaultPage(props: VaultPageProps) { const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const createMenuRef = useRef(null); const sortMenuRef = useRef(null); + const folderSortMenuRef = useRef(null); const attachmentInputRef = useRef(null); const listPanelRef = useRef(null); const sshSeedTicketRef = useRef(0); @@ -163,6 +167,25 @@ export default function VaultPage(props: VaultPageProps) { } }, [sortMode]); + useEffect(() => { + try { + const saved = String(localStorage.getItem(FOLDER_SORT_STORAGE_KEY) || '').trim() as VaultSortMode; + if (saved === 'edited' || saved === 'created' || saved === 'name') { + setFolderSortMode(saved); + } + } catch { + // ignore storage read failures + } + }, []); + + useEffect(() => { + try { + localStorage.setItem(FOLDER_SORT_STORAGE_KEY, folderSortMode); + } catch { + // ignore storage write failures + } + }, [folderSortMode]); + useEffect(() => { const node = listPanelRef.current; if (!node) return; @@ -211,6 +234,25 @@ export default function VaultPage(props: VaultPageProps) { }; }, [sortMenuOpen]); + useEffect(() => { + const onPointerDown = (event: Event) => { + if (!folderSortMenuOpen) return; + const target = event.target as Node | null; + if (folderSortMenuRef.current && target && !folderSortMenuRef.current.contains(target)) { + setFolderSortMenuOpen(false); + } + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setFolderSortMenuOpen(false); + }; + document.addEventListener('pointerdown', onPointerDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('pointerdown', onPointerDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [folderSortMenuOpen]); + useEffect(() => { setRepromptApprovedCipherId(null); setRepromptPassword(''); @@ -833,6 +875,9 @@ function folderName(id: string | null | undefined): string { busy={busy} isMobileLayout={isMobileLayout} mobileSidebarOpen={mobileSidebarOpen} + folderSortMode={folderSortMode} + folderSortMenuOpen={folderSortMenuOpen} + folderSortMenuRef={folderSortMenuRef} onCloseMobileSidebar={() => setMobileSidebarOpen(false)} onChangeFilter={setSidebarFilter} onOpenDeleteAllFolders={() => setDeleteAllFoldersOpen(true)} @@ -842,6 +887,11 @@ function folderName(id: string | null | undefined): string { setRenameFolderName(folder.decName || folder.name || ''); }} onOpenDeleteFolder={setPendingDeleteFolder} + onToggleFolderSortMenu={() => setFolderSortMenuOpen((open) => !open)} + onSelectFolderSortMode={(value) => { + setFolderSortMode(value); + setFolderSortMenuOpen(false); + }} /> ; onCloseMobileSidebar: () => void; onChangeFilter: (filter: SidebarFilter) => void; onOpenDeleteAllFolders: () => void; onOpenCreateFolder: () => void; onOpenRenameFolder: (folder: Folder) => void; onOpenDeleteFolder: (folder: Folder) => void; + onToggleFolderSortMenu: () => void; + onSelectFolderSortMode: (value: VaultSortMode) => void; } export default function VaultSidebar(props: VaultSidebarProps) { + const sortedFolders = useMemo(() => { + const sorted = [...props.folders]; + sorted.sort((a, b) => { + if (props.folderSortMode === 'edited') { + const aTime = new Date(String(a.revisionDate || a.creationDate || '')).getTime(); + const bTime = new Date(String(b.revisionDate || b.creationDate || '')).getTime(); + const aValid = Number.isFinite(aTime); + const bValid = Number.isFinite(bTime); + if (aValid && bValid) { + const diff = bTime - aTime; + if (diff !== 0) return diff; + } + if (aValid !== bValid) return aValid ? -1 : 1; + } else if (props.folderSortMode === 'created') { + const aTime = new Date(String(a.creationDate || '')).getTime(); + const bTime = new Date(String(b.creationDate || '')).getTime(); + const aValid = Number.isFinite(aTime); + const bValid = Number.isFinite(bTime); + if (aValid && bValid) { + const diff = bTime - aTime; + if (diff !== 0) return diff; + } + if (aValid !== bValid) return aValid ? -1 : 1; + } + const nameDiff = String(a.decName || a.name || '').localeCompare( + String(b.decName || b.name || ''), undefined, { sensitivity: 'base', numeric: true } + ); + if (nameDiff !== 0) return nameDiff; + return String(a.id || '').localeCompare(String(b.id || '')); + }); + return sorted; + }, [props.folders, props.folderSortMode]); + return (