mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add folder creation date and sorting functionality in Vault components
This commit is contained in:
@@ -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<VaultSortMode>('edited');
|
||||
const [sortMenuOpen, setSortMenuOpen] = useState(false);
|
||||
const [folderSortMode, setFolderSortMode] = useState<VaultSortMode>('name');
|
||||
const [folderSortMenuOpen, setFolderSortMenuOpen] = useState(false);
|
||||
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>({ kind: 'all' });
|
||||
const [selectedCipherId, setSelectedCipherId] = useState('');
|
||||
const [selectedMap, setSelectedMap] = useState<Record<string, boolean>>({});
|
||||
@@ -111,6 +114,7 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const createMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const sortMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const folderSortMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const listPanelRef = useRef<HTMLDivElement | null>(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);
|
||||
}}
|
||||
/>
|
||||
|
||||
<VaultListPanel
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import type { RefObject } from 'preact';
|
||||
import {
|
||||
Archive,
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Folder as FolderIcon,
|
||||
@@ -17,7 +21,7 @@ import {
|
||||
} from 'lucide-preact';
|
||||
import type { Folder } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import type { SidebarFilter } from '@/components/vault/vault-page-helpers';
|
||||
import { VAULT_SORT_OPTIONS, type SidebarFilter, type VaultSortMode } from '@/components/vault/vault-page-helpers';
|
||||
|
||||
interface VaultSidebarProps {
|
||||
folders: Folder[];
|
||||
@@ -25,15 +29,53 @@ interface VaultSidebarProps {
|
||||
busy: boolean;
|
||||
isMobileLayout: boolean;
|
||||
mobileSidebarOpen: boolean;
|
||||
folderSortMode: VaultSortMode;
|
||||
folderSortMenuOpen: boolean;
|
||||
folderSortMenuRef: RefObject<HTMLDivElement>;
|
||||
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 (
|
||||
<aside className={`sidebar ${props.isMobileLayout ? 'mobile-sidebar-sheet' : ''} ${props.isMobileLayout && props.mobileSidebarOpen ? 'open' : ''}`}>
|
||||
{props.isMobileLayout && (
|
||||
@@ -85,6 +127,32 @@ export default function VaultSidebar(props: VaultSidebarProps) {
|
||||
<div className="sidebar-title-row">
|
||||
<div className="sidebar-title">{t('txt_folders')}</div>
|
||||
<div className="folder-title-actions">
|
||||
<div className="sort-menu-wrap" ref={props.folderSortMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`folder-sort-btn ${props.folderSortMenuOpen ? 'active' : ''}`}
|
||||
title={t('txt_sort')}
|
||||
aria-label={t('txt_sort')}
|
||||
onClick={props.onToggleFolderSortMenu}
|
||||
>
|
||||
<ArrowUpDown size={13} />
|
||||
</button>
|
||||
{props.folderSortMenuOpen && (
|
||||
<div className="sort-menu">
|
||||
{VAULT_SORT_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`sort-menu-item ${props.folderSortMode === option.value ? 'active' : ''}`}
|
||||
onClick={() => props.onSelectFolderSortMode(option.value)}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
{props.folderSortMode === option.value ? <Check size={14} /> : <span className="sort-menu-check-placeholder" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="folder-delete-btn"
|
||||
@@ -103,7 +171,7 @@ export default function VaultSidebar(props: VaultSidebarProps) {
|
||||
<button type="button" className={`tree-btn ${props.sidebarFilter.kind === 'folder' && props.sidebarFilter.folderId === null ? 'active' : ''}`} onClick={() => props.onChangeFilter({ kind: 'folder', folderId: null })}>
|
||||
<FolderX size={14} className="tree-icon" /> <span className="tree-label">{t('txt_no_folder')}</span>
|
||||
</button>
|
||||
{props.folders.map((folder) => (
|
||||
{sortedFolders.map((folder) => (
|
||||
<div key={folder.id} className="folder-row">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -36,6 +36,7 @@ export const CREATE_TYPE_OPTIONS: TypeOption[] = [
|
||||
];
|
||||
|
||||
export const VAULT_SORT_STORAGE_KEY = 'nodewarden.vault.sort.v1';
|
||||
export const FOLDER_SORT_STORAGE_KEY = 'nodewarden.folder-sort.v1';
|
||||
export const MOBILE_LAYOUT_QUERY = '(max-width: 1180px)';
|
||||
export const VAULT_LIST_ROW_HEIGHT = 74;
|
||||
export const VAULT_LIST_OVERSCAN = 10;
|
||||
|
||||
Reference in New Issue
Block a user