mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: implement drag-and-drop reordering for vault items and enhance sorting functionality
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
MOBILE_LAYOUT_QUERY,
|
||||
VAULT_LIST_OVERSCAN,
|
||||
VAULT_LIST_ROW_HEIGHT,
|
||||
VAULT_ORDER_STORAGE_KEY,
|
||||
FOLDER_SORT_STORAGE_KEY,
|
||||
VAULT_SORT_STORAGE_KEY,
|
||||
cipherTypeKey,
|
||||
@@ -72,6 +73,15 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchComposing, setSearchComposing] = useState(false);
|
||||
const [sortMode, setSortMode] = useState<VaultSortMode>('edited');
|
||||
const [vaultOrderedIds, setVaultOrderedIds] = useState<string[]>(() => {
|
||||
if (typeof localStorage === 'undefined') return [];
|
||||
try {
|
||||
const parsed = JSON.parse(String(localStorage.getItem(VAULT_ORDER_STORAGE_KEY) || '[]'));
|
||||
return Array.isArray(parsed) ? parsed.map((id) => String(id || '').trim()).filter(Boolean) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [sortMenuOpen, setSortMenuOpen] = useState(false);
|
||||
const [folderSortMode, setFolderSortMode] = useState<VaultSortMode>('name');
|
||||
const [folderSortMenuOpen, setFolderSortMenuOpen] = useState(false);
|
||||
@@ -117,6 +127,7 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
const folderSortMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const listPanelRef = useRef<HTMLDivElement | null>(null);
|
||||
const suppressNextSortScrollRef = useRef(false);
|
||||
const sshSeedTicketRef = useRef(0);
|
||||
const sshFingerprintTicketRef = useRef(0);
|
||||
const [listScrollTop, setListScrollTop] = useState(0);
|
||||
@@ -151,7 +162,7 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = String(localStorage.getItem(VAULT_SORT_STORAGE_KEY) || '').trim() as VaultSortMode;
|
||||
if (saved === 'edited' || saved === 'created' || saved === 'name') {
|
||||
if (saved === 'manual' || saved === 'edited' || saved === 'created' || saved === 'name') {
|
||||
setSortMode(saved);
|
||||
}
|
||||
} catch {
|
||||
@@ -167,6 +178,36 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
}
|
||||
}, [sortMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.loading) return;
|
||||
const cipherById = new Map(props.ciphers.map((cipher) => [cipher.id, cipher]));
|
||||
const validIds = new Set(cipherById.keys());
|
||||
setVaultOrderedIds((prev) => {
|
||||
const filtered = prev.filter((id) => validIds.has(id));
|
||||
const existing = new Set(filtered);
|
||||
const missing = props.ciphers
|
||||
.filter((cipher) => !existing.has(cipher.id))
|
||||
.sort((a, b) => {
|
||||
const diff = creationTimeValue(b) - creationTimeValue(a);
|
||||
if (diff !== 0) return diff;
|
||||
return String(b.id || '').localeCompare(String(a.id || ''));
|
||||
})
|
||||
.map((cipher) => cipher.id);
|
||||
const next = [...missing, ...filtered];
|
||||
if (next.length === prev.length && next.every((id, index) => id === prev[index])) return prev;
|
||||
return next;
|
||||
});
|
||||
}, [props.ciphers, props.loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.loading) return;
|
||||
try {
|
||||
localStorage.setItem(VAULT_ORDER_STORAGE_KEY, JSON.stringify(vaultOrderedIds));
|
||||
} catch {
|
||||
// ignore storage write failures
|
||||
}
|
||||
}, [vaultOrderedIds, props.loading]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = String(localStorage.getItem(FOLDER_SORT_STORAGE_KEY) || '').trim() as VaultSortMode;
|
||||
@@ -321,8 +362,18 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
return name.includes(searchQuery) || username.includes(searchQuery) || uri.includes(searchQuery);
|
||||
});
|
||||
|
||||
const orderMap = new Map(vaultOrderedIds.map((id, index) => [id, index]));
|
||||
next.sort((a, b) => {
|
||||
if (sortMode === 'edited') {
|
||||
if (sortMode === 'manual') {
|
||||
const orderA = orderMap.get(a.id);
|
||||
const orderB = orderMap.get(b.id);
|
||||
if (orderA != null && orderB != null) {
|
||||
const diff = orderA - orderB;
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
if (orderA != null) return -1;
|
||||
if (orderB != null) return 1;
|
||||
} else if (sortMode === 'edited') {
|
||||
const diff = sortTimeValue(b) - sortTimeValue(a);
|
||||
if (diff !== 0) return diff;
|
||||
} else if (sortMode === 'created') {
|
||||
@@ -340,7 +391,7 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
});
|
||||
|
||||
return next;
|
||||
}, [props.ciphers, sidebarFilter, searchQuery, sortMode, duplicateSignatureCounts]);
|
||||
}, [props.ciphers, sidebarFilter, searchQuery, sortMode, duplicateSignatureCounts, vaultOrderedIds]);
|
||||
|
||||
const sidebarFilterKey = useMemo(() => {
|
||||
if (sidebarFilter.kind === 'folder') return `folder:${sidebarFilter.folderId ?? 'none'}`;
|
||||
@@ -349,6 +400,10 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
}, [sidebarFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (suppressNextSortScrollRef.current) {
|
||||
suppressNextSortScrollRef.current = false;
|
||||
return;
|
||||
}
|
||||
setListScrollTop(0);
|
||||
listPanelRef.current?.scrollTo({ top: 0 });
|
||||
}, [searchQuery, sortMode, sidebarFilterKey]);
|
||||
@@ -359,6 +414,40 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
}
|
||||
}, [sidebarFilter.kind, sortMode]);
|
||||
|
||||
const canReorderVaultList =
|
||||
!searchQuery &&
|
||||
sidebarFilter.kind !== 'duplicates' &&
|
||||
sidebarFilter.kind !== 'trash' &&
|
||||
sidebarFilter.kind !== 'archive' &&
|
||||
!props.loading &&
|
||||
!busy;
|
||||
|
||||
function handleReorderVaultCipher(activeId: string, overId: string): void {
|
||||
if (!canReorderVaultList || activeId === overId) return;
|
||||
const currentIds = filteredCiphers.map((cipher) => cipher.id);
|
||||
const fromIndex = currentIds.indexOf(activeId);
|
||||
const toIndex = currentIds.indexOf(overId);
|
||||
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
|
||||
const nextVisibleIds = [...currentIds];
|
||||
const [moved] = nextVisibleIds.splice(fromIndex, 1);
|
||||
nextVisibleIds.splice(toIndex, 0, moved);
|
||||
|
||||
setVaultOrderedIds((prev) => {
|
||||
const validIds = new Set(props.ciphers.map((cipher) => cipher.id));
|
||||
const nextVisibleSet = new Set(nextVisibleIds);
|
||||
const existingHiddenIds = prev.filter((id) => validIds.has(id) && !nextVisibleSet.has(id));
|
||||
const fallbackHiddenIds = props.ciphers
|
||||
.map((cipher) => cipher.id)
|
||||
.filter((id) => validIds.has(id) && !nextVisibleSet.has(id) && !existingHiddenIds.includes(id));
|
||||
const next = [...nextVisibleIds, ...existingHiddenIds, ...fallbackHiddenIds];
|
||||
return next;
|
||||
});
|
||||
if (sortMode !== 'manual') {
|
||||
suppressNextSortScrollRef.current = true;
|
||||
setSortMode('manual');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreating) return;
|
||||
if (!filteredCiphers.length) {
|
||||
@@ -910,6 +999,7 @@ function folderName(id: string | null | undefined): string {
|
||||
sidebarFilter={sidebarFilter}
|
||||
isMobileLayout={isMobileLayout}
|
||||
mobileFabVisible={!isMobileLayout || mobilePanel === 'list'}
|
||||
canReorder={canReorderVaultList}
|
||||
createMenuOpen={createMenuOpen}
|
||||
createMenuRef={createMenuRef}
|
||||
sortMenuRef={sortMenuRef}
|
||||
@@ -956,6 +1046,7 @@ function folderName(id: string | null | undefined): string {
|
||||
setMoveOpen(true);
|
||||
}}
|
||||
onClearSelection={() => setSelectedMap({})}
|
||||
onReorderCipher={handleReorderVaultCipher}
|
||||
onScroll={setListScrollTop}
|
||||
onToggleSelected={(cipherId, checked) =>
|
||||
setSelectedMap((prev) => ({
|
||||
|
||||
Reference in New Issue
Block a user