mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
refactor: optimize TOTP and vault components with useMemo for performance improvements
This commit is contained in:
@@ -70,8 +70,7 @@ function hostFromUri(uri: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TotpListIcon({ cipher }: { cipher: Cipher }) {
|
function TotpListIcon({ cipher }: { cipher: Cipher }) {
|
||||||
const uri = firstCipherUri(cipher);
|
const host = useMemo(() => hostFromUri(firstCipherUri(cipher)), [cipher]);
|
||||||
const host = hostFromUri(uri);
|
|
||||||
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
|
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const markIconError = () => {
|
const markIconError = () => {
|
||||||
@@ -226,16 +225,21 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
|
|||||||
await copyTextWithFeedback(value, { successMessage: t('txt_code_copied') });
|
await copyTextWithFeedback(value, { successMessage: t('txt_code_copied') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameCollator = useMemo(
|
||||||
|
() => new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const baseTotpItems = useMemo(
|
const baseTotpItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
props.ciphers
|
props.ciphers
|
||||||
.filter((cipher) => isCipherVisibleInNormalVault(cipher) && !!cipher.login?.decTotp)
|
.filter((cipher) => isCipherVisibleInNormalVault(cipher) && !!cipher.login?.decTotp)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const nameA = (a.decName || a.name || '').trim().toLowerCase();
|
const nameA = (a.decName || a.name || '').trim();
|
||||||
const nameB = (b.decName || b.name || '').trim().toLowerCase();
|
const nameB = (b.decName || b.name || '').trim();
|
||||||
return nameA.localeCompare(nameB);
|
return nameCollator.compare(nameA, nameB);
|
||||||
}),
|
}),
|
||||||
[props.ciphers]
|
[props.ciphers, nameCollator]
|
||||||
);
|
);
|
||||||
|
|
||||||
const totpItems = useMemo(() => {
|
const totpItems = useMemo(() => {
|
||||||
@@ -247,11 +251,13 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
|
|||||||
if (orderA != null && orderB != null) return orderA - orderB;
|
if (orderA != null && orderB != null) return orderA - orderB;
|
||||||
if (orderA != null) return -1;
|
if (orderA != null) return -1;
|
||||||
if (orderB != null) return 1;
|
if (orderB != null) return 1;
|
||||||
const nameA = (a.decName || a.name || '').trim().toLowerCase();
|
const nameA = (a.decName || a.name || '').trim();
|
||||||
const nameB = (b.decName || b.name || '').trim().toLowerCase();
|
const nameB = (b.decName || b.name || '').trim();
|
||||||
return nameA.localeCompare(nameB);
|
return nameCollator.compare(nameA, nameB);
|
||||||
});
|
});
|
||||||
}, [baseTotpItems, orderedIds]);
|
}, [baseTotpItems, orderedIds, nameCollator]);
|
||||||
|
|
||||||
|
const sortableTotpItems = useMemo(() => totpItems.map((cipher) => cipher.id), [totpItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!baseTotpItems.length) return;
|
if (!baseTotpItems.length) return;
|
||||||
@@ -361,7 +367,7 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
|
|||||||
>
|
>
|
||||||
{!totpItems.length && !props.loading && <div className="empty">{t('txt_no_verification_codes')}</div>}
|
{!totpItems.length && !props.loading && <div className="empty">{t('txt_no_verification_codes')}</div>}
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext items={totpItems.map((cipher) => cipher.id)} strategy={rectSortingStrategy}>
|
<SortableContext items={sortableTotpItems} strategy={rectSortingStrategy}>
|
||||||
{totpItems.map((cipher) => (
|
{totpItems.map((cipher) => (
|
||||||
<SortableTotpRow
|
<SortableTotpRow
|
||||||
key={cipher.id}
|
key={cipher.id}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
const suppressNextSortScrollRef = useRef(false);
|
const suppressNextSortScrollRef = useRef(false);
|
||||||
const sshSeedTicketRef = useRef(0);
|
const sshSeedTicketRef = useRef(0);
|
||||||
const sshFingerprintTicketRef = useRef(0);
|
const sshFingerprintTicketRef = useRef(0);
|
||||||
|
const listScrollBucketRef = useRef(0);
|
||||||
const [listScrollTop, setListScrollTop] = useState(0);
|
const [listScrollTop, setListScrollTop] = useState(0);
|
||||||
const [listViewportHeight, setListViewportHeight] = useState(0);
|
const [listViewportHeight, setListViewportHeight] = useState(0);
|
||||||
|
|
||||||
@@ -326,29 +327,75 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
void recalculateSshFingerprint(draft.sshPublicKey);
|
void recalculateSshFingerprint(draft.sshPublicKey);
|
||||||
}, [isEditing, draft?.id, draft?.type]);
|
}, [isEditing, draft?.id, draft?.type]);
|
||||||
|
|
||||||
const duplicateSignatureCounts = useMemo(() => {
|
const cipherMetaById = useMemo(() => {
|
||||||
|
const meta = new Map<string, {
|
||||||
|
name: string;
|
||||||
|
searchText: string;
|
||||||
|
firstUri: string;
|
||||||
|
typeKey: string;
|
||||||
|
sortTime: number;
|
||||||
|
creationTime: number;
|
||||||
|
}>();
|
||||||
|
for (const cipher of props.ciphers) {
|
||||||
|
const name = String(cipher.decName || cipher.name || '');
|
||||||
|
const username = String(cipher.login?.decUsername || '');
|
||||||
|
const uri = firstCipherUri(cipher);
|
||||||
|
meta.set(cipher.id, {
|
||||||
|
name,
|
||||||
|
searchText: `${name}\n${username}\n${uri}`.toLowerCase(),
|
||||||
|
firstUri: uri,
|
||||||
|
typeKey: cipherTypeKey(Number(cipher.type || 1)),
|
||||||
|
sortTime: sortTimeValue(cipher),
|
||||||
|
creationTime: creationTimeValue(cipher),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
}, [props.ciphers]);
|
||||||
|
|
||||||
|
const cipherById = useMemo(() => {
|
||||||
|
const map = new Map<string, Cipher>();
|
||||||
|
for (const cipher of props.ciphers) map.set(cipher.id, cipher);
|
||||||
|
return map;
|
||||||
|
}, [props.ciphers]);
|
||||||
|
|
||||||
|
const folderById = useMemo(() => {
|
||||||
|
const map = new Map<string, Folder>();
|
||||||
|
for (const folder of props.folders) map.set(folder.id, folder);
|
||||||
|
return map;
|
||||||
|
}, [props.folders]);
|
||||||
|
|
||||||
|
const nameCollator = useMemo(
|
||||||
|
() => new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateSignatureInfo = useMemo(() => {
|
||||||
|
if (sidebarFilter.kind !== 'duplicates') return null;
|
||||||
|
const byId = new Map<string, string>();
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>();
|
||||||
for (const cipher of props.ciphers) {
|
for (const cipher of props.ciphers) {
|
||||||
if (!isCipherVisibleInNormalVault(cipher)) continue;
|
if (!isCipherVisibleInNormalVault(cipher)) continue;
|
||||||
const signature = buildCipherDuplicateSignature(cipher);
|
const signature = buildCipherDuplicateSignature(cipher);
|
||||||
|
byId.set(cipher.id, signature);
|
||||||
counts.set(signature, (counts.get(signature) || 0) + 1);
|
counts.set(signature, (counts.get(signature) || 0) + 1);
|
||||||
}
|
}
|
||||||
return counts;
|
return { byId, counts };
|
||||||
}, [props.ciphers]);
|
}, [props.ciphers, sidebarFilter.kind]);
|
||||||
|
|
||||||
const filteredCiphers = useMemo(() => {
|
const filteredCiphers = useMemo(() => {
|
||||||
const next = props.ciphers.filter((cipher) => {
|
const next = props.ciphers.filter((cipher) => {
|
||||||
|
const meta = cipherMetaById.get(cipher.id);
|
||||||
if (sidebarFilter.kind === 'trash') {
|
if (sidebarFilter.kind === 'trash') {
|
||||||
if (!isCipherVisibleInTrash(cipher)) return false;
|
if (!isCipherVisibleInTrash(cipher)) return false;
|
||||||
} else if (sidebarFilter.kind === 'archive') {
|
} else if (sidebarFilter.kind === 'archive') {
|
||||||
if (!isCipherVisibleInArchive(cipher)) return false;
|
if (!isCipherVisibleInArchive(cipher)) return false;
|
||||||
} else {
|
} else {
|
||||||
if (!isCipherVisibleInNormalVault(cipher)) return false;
|
if (!isCipherVisibleInNormalVault(cipher)) return false;
|
||||||
if (sidebarFilter.kind === 'duplicates' && (duplicateSignatureCounts.get(buildCipherDuplicateSignature(cipher)) || 0) < 2) {
|
if (sidebarFilter.kind === 'duplicates' && ((duplicateSignatureInfo?.counts.get(duplicateSignatureInfo.byId.get(cipher.id) || '') || 0) < 2)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (sidebarFilter.kind === 'favorite' && !cipher.favorite) return false;
|
if (sidebarFilter.kind === 'favorite' && !cipher.favorite) return false;
|
||||||
if (sidebarFilter.kind === 'type' && cipherTypeKey(Number(cipher.type || 1)) !== sidebarFilter.value) return false;
|
if (sidebarFilter.kind === 'type' && meta?.typeKey !== sidebarFilter.value) return false;
|
||||||
if (sidebarFilter.kind === 'folder') {
|
if (sidebarFilter.kind === 'folder') {
|
||||||
if (sidebarFilter.folderId === null) {
|
if (sidebarFilter.folderId === null) {
|
||||||
if (cipher.folderId) return false;
|
if (cipher.folderId) return false;
|
||||||
@@ -358,15 +405,14 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!searchQuery) return true;
|
if (!searchQuery) return true;
|
||||||
const name = (cipher.decName || '').toLowerCase();
|
return !!meta?.searchText.includes(searchQuery);
|
||||||
const username = (cipher.login?.decUsername || '').toLowerCase();
|
|
||||||
const uri = firstCipherUri(cipher).toLowerCase();
|
|
||||||
return name.includes(searchQuery) || username.includes(searchQuery) || uri.includes(searchQuery);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderMap = new Map(vaultOrderedIds.map((id, index) => [id, index]));
|
const orderMap = sortMode === 'manual' ? new Map(vaultOrderedIds.map((id, index) => [id, index])) : null;
|
||||||
next.sort((a, b) => {
|
next.sort((a, b) => {
|
||||||
if (sortMode === 'manual') {
|
const metaA = cipherMetaById.get(a.id);
|
||||||
|
const metaB = cipherMetaById.get(b.id);
|
||||||
|
if (sortMode === 'manual' && orderMap) {
|
||||||
const orderA = orderMap.get(a.id);
|
const orderA = orderMap.get(a.id);
|
||||||
const orderB = orderMap.get(b.id);
|
const orderB = orderMap.get(b.id);
|
||||||
if (orderA != null && orderB != null) {
|
if (orderA != null && orderB != null) {
|
||||||
@@ -376,16 +422,13 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
if (orderA != null) return -1;
|
if (orderA != null) return -1;
|
||||||
if (orderB != null) return 1;
|
if (orderB != null) return 1;
|
||||||
} else if (sortMode === 'edited') {
|
} else if (sortMode === 'edited') {
|
||||||
const diff = sortTimeValue(b) - sortTimeValue(a);
|
const diff = (metaB?.sortTime || 0) - (metaA?.sortTime || 0);
|
||||||
if (diff !== 0) return diff;
|
if (diff !== 0) return diff;
|
||||||
} else if (sortMode === 'created') {
|
} else if (sortMode === 'created') {
|
||||||
const diff = creationTimeValue(b) - creationTimeValue(a);
|
const diff = (metaB?.creationTime || 0) - (metaA?.creationTime || 0);
|
||||||
if (diff !== 0) return diff;
|
if (diff !== 0) return diff;
|
||||||
} else {
|
} else {
|
||||||
const nameDiff = String(a.decName || a.name || '').localeCompare(String(b.decName || b.name || ''), undefined, {
|
const nameDiff = nameCollator.compare(metaA?.name || '', metaB?.name || '');
|
||||||
sensitivity: 'base',
|
|
||||||
numeric: true,
|
|
||||||
});
|
|
||||||
if (nameDiff !== 0) return nameDiff;
|
if (nameDiff !== 0) return nameDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +436,13 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}, [props.ciphers, sidebarFilter, searchQuery, sortMode, duplicateSignatureCounts, vaultOrderedIds]);
|
}, [props.ciphers, cipherMetaById, sidebarFilter, searchQuery, sortMode, duplicateSignatureInfo, vaultOrderedIds, nameCollator]);
|
||||||
|
|
||||||
|
const filteredCipherIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const cipher of filteredCiphers) ids.add(cipher.id);
|
||||||
|
return ids;
|
||||||
|
}, [filteredCiphers]);
|
||||||
|
|
||||||
const sidebarFilterKey = useMemo(() => {
|
const sidebarFilterKey = useMemo(() => {
|
||||||
if (sidebarFilter.kind === 'folder') return `folder:${sidebarFilter.folderId ?? 'none'}`;
|
if (sidebarFilter.kind === 'folder') return `folder:${sidebarFilter.folderId ?? 'none'}`;
|
||||||
@@ -407,6 +456,7 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setListScrollTop(0);
|
setListScrollTop(0);
|
||||||
|
listScrollBucketRef.current = 0;
|
||||||
listPanelRef.current?.scrollTo({ top: 0 });
|
listPanelRef.current?.scrollTo({ top: 0 });
|
||||||
}, [searchQuery, sortMode, sidebarFilterKey]);
|
}, [searchQuery, sortMode, sidebarFilterKey]);
|
||||||
|
|
||||||
@@ -456,15 +506,12 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
if (selectedCipherId) setSelectedCipherId('');
|
if (selectedCipherId) setSelectedCipherId('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedCipherId || !filteredCiphers.some((x) => x.id === selectedCipherId)) {
|
if (!selectedCipherId || !filteredCipherIds.has(selectedCipherId)) {
|
||||||
setSelectedCipherId(filteredCiphers[0].id);
|
setSelectedCipherId(filteredCiphers[0].id);
|
||||||
}
|
}
|
||||||
}, [filteredCiphers, selectedCipherId, isCreating]);
|
}, [filteredCiphers, filteredCipherIds, selectedCipherId, isCreating]);
|
||||||
|
|
||||||
const selectedCipher = useMemo(
|
const selectedCipher = useMemo(() => cipherById.get(selectedCipherId) || null, [cipherById, selectedCipherId]);
|
||||||
() => props.ciphers.find((x) => x.id === selectedCipherId) || null,
|
|
||||||
[props.ciphers, selectedCipherId]
|
|
||||||
);
|
|
||||||
const virtualRange = useMemo(() => {
|
const virtualRange = useMemo(() => {
|
||||||
if (!filteredCiphers.length) {
|
if (!filteredCiphers.length) {
|
||||||
return { start: 0, end: 0, padTop: 0, padBottom: 0 };
|
return { start: 0, end: 0, padTop: 0, padBottom: 0 };
|
||||||
@@ -530,17 +577,24 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
|
|
||||||
function folderName(id: string | null | undefined): string {
|
function folderName(id: string | null | undefined): string {
|
||||||
if (!id) return t('txt_no_folder');
|
if (!id) return t('txt_no_folder');
|
||||||
const folder = props.folders.find((x) => x.id === id);
|
const folder = folderById.get(id);
|
||||||
return folder?.decName || folder?.name || id;
|
return folder?.decName || folder?.name || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listSubtitle(cipher: Cipher): string {
|
function listSubtitle(cipher: Cipher): string {
|
||||||
if (Number(cipher.type || 1) === 1) {
|
if (Number(cipher.type || 1) === 1) {
|
||||||
return cipher.login?.decUsername || firstCipherUri(cipher) || '';
|
return cipher.login?.decUsername || cipherMetaById.get(cipher.id)?.firstUri || '';
|
||||||
}
|
}
|
||||||
return cipherTypeLabel(Number(cipher.type || 1));
|
return cipherTypeLabel(Number(cipher.type || 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleListScroll(top: number): void {
|
||||||
|
const bucket = Math.floor(Math.max(0, top) / VAULT_LIST_ROW_HEIGHT);
|
||||||
|
if (bucket === listScrollBucketRef.current) return;
|
||||||
|
listScrollBucketRef.current = bucket;
|
||||||
|
setListScrollTop(top);
|
||||||
|
}
|
||||||
|
|
||||||
function startCreate(type: number): void {
|
function startCreate(type: number): void {
|
||||||
setDraft(createEmptyDraft(type));
|
setDraft(createEmptyDraft(type));
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
@@ -1024,7 +1078,7 @@ function folderName(id: string | null | undefined): string {
|
|||||||
const map: Record<string, boolean> = {};
|
const map: Record<string, boolean> = {};
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const cipher of filteredCiphers) {
|
for (const cipher of filteredCiphers) {
|
||||||
const signature = buildCipherDuplicateSignature(cipher);
|
const signature = duplicateSignatureInfo?.byId.get(cipher.id) || buildCipherDuplicateSignature(cipher);
|
||||||
if (seen.has(signature)) {
|
if (seen.has(signature)) {
|
||||||
map[cipher.id] = true;
|
map[cipher.id] = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -1049,12 +1103,15 @@ function folderName(id: string | null | undefined): string {
|
|||||||
}}
|
}}
|
||||||
onClearSelection={() => setSelectedMap({})}
|
onClearSelection={() => setSelectedMap({})}
|
||||||
onReorderCipher={handleReorderVaultCipher}
|
onReorderCipher={handleReorderVaultCipher}
|
||||||
onScroll={setListScrollTop}
|
onScroll={handleListScroll}
|
||||||
onToggleSelected={(cipherId, checked) =>
|
onToggleSelected={(cipherId, checked) =>
|
||||||
setSelectedMap((prev) => ({
|
setSelectedMap((prev) => {
|
||||||
...prev,
|
if (checked) return { ...prev, [cipherId]: true };
|
||||||
[cipherId]: checked,
|
if (!prev[cipherId]) return prev;
|
||||||
}))
|
const next = { ...prev };
|
||||||
|
delete next[cipherId];
|
||||||
|
return next;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
onSelectCipher={(cipherId) => {
|
onSelectCipher={(cipherId) => {
|
||||||
if (isEditing || isCreating) {
|
if (isEditing || isCreating) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { JSX, RefObject } from 'preact';
|
import type { JSX, RefObject } from 'preact';
|
||||||
import { createPortal } from 'preact/compat';
|
import { createPortal } from 'preact/compat';
|
||||||
import { useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import { Archive, ArrowUpDown, Check, CheckCheck, FolderInput, GripVertical, Plus, RefreshCw, RotateCcw, Trash2, X } from 'lucide-preact';
|
import { Archive, ArrowUpDown, Check, CheckCheck, FolderInput, GripVertical, Plus, RefreshCw, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -186,6 +186,28 @@ function SortableCipherListItem(props: SortableCipherListItemProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlainCipherListItem(props: SortableCipherListItemProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`list-item ${props.selected ? 'active' : ''}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target.closest('.row-check') || target.closest('.cipher-drag-btn')) return;
|
||||||
|
props.onSelectCipher(props.cipher.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CipherListItemBody
|
||||||
|
cipher={props.cipher}
|
||||||
|
checked={props.checked}
|
||||||
|
canReorder={false}
|
||||||
|
subtitle={props.subtitle}
|
||||||
|
onToggleSelected={props.onToggleSelected}
|
||||||
|
onSelectCipher={props.onSelectCipher}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function VaultListPanel(props: VaultListPanelProps) {
|
export default function VaultListPanel(props: VaultListPanelProps) {
|
||||||
const [activeDragId, setActiveDragId] = useState('');
|
const [activeDragId, setActiveDragId] = useState('');
|
||||||
const [activeDragWidth, setActiveDragWidth] = useState<number | null>(null);
|
const [activeDragWidth, setActiveDragWidth] = useState<number | null>(null);
|
||||||
@@ -203,7 +225,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortableItems = props.filteredCiphers.map((cipher) => cipher.id);
|
const sortableItems = useMemo(() => props.visibleCiphers.map((cipher) => cipher.id), [props.visibleCiphers]);
|
||||||
const renderedCiphers = props.visibleCiphers;
|
const renderedCiphers = props.visibleCiphers;
|
||||||
const activeDragCipher = activeDragId ? props.filteredCiphers.find((cipher) => cipher.id === activeDragId) || null : null;
|
const activeDragCipher = activeDragId ? props.filteredCiphers.find((cipher) => cipher.id === activeDragId) || null : null;
|
||||||
|
|
||||||
@@ -250,6 +272,22 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const listItems = renderedCiphers.map((cipher) => {
|
||||||
|
const ItemComponent = props.canReorder ? SortableCipherListItem : PlainCipherListItem;
|
||||||
|
return (
|
||||||
|
<ItemComponent
|
||||||
|
key={cipher.id}
|
||||||
|
cipher={cipher}
|
||||||
|
selected={props.selectedCipherId === cipher.id}
|
||||||
|
checked={!!props.selectedMap[cipher.id]}
|
||||||
|
canReorder={props.canReorder}
|
||||||
|
subtitle={props.listSubtitle(cipher)}
|
||||||
|
onToggleSelected={props.onToggleSelected}
|
||||||
|
onSelectCipher={props.onSelectCipher}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="list-col">
|
<section className="list-col">
|
||||||
<div className="list-head">
|
<div className="list-head">
|
||||||
@@ -357,20 +395,10 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
<div className="list-panel" ref={props.listPanelRef} onScroll={(event) => props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}>
|
<div className="list-panel" ref={props.listPanelRef} onScroll={(event) => props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}>
|
||||||
{!!props.filteredCiphers.length && (
|
{!!props.filteredCiphers.length && (
|
||||||
<div style={{ paddingTop: `${props.virtualRange.padTop}px`, paddingBottom: `${props.virtualRange.padBottom}px` }}>
|
<div style={{ paddingTop: `${props.virtualRange.padTop}px`, paddingBottom: `${props.virtualRange.padBottom}px` }}>
|
||||||
|
{props.canReorder ? (
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel}>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel}>
|
||||||
<SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
|
<SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
|
||||||
{renderedCiphers.map((cipher) => (
|
{listItems}
|
||||||
<SortableCipherListItem
|
|
||||||
key={cipher.id}
|
|
||||||
cipher={cipher}
|
|
||||||
selected={props.selectedCipherId === cipher.id}
|
|
||||||
checked={!!props.selectedMap[cipher.id]}
|
|
||||||
canReorder={props.canReorder}
|
|
||||||
subtitle={props.listSubtitle(cipher)}
|
|
||||||
onToggleSelected={props.onToggleSelected}
|
|
||||||
onSelectCipher={props.onSelectCipher}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<DragOverlay adjustScale={false}>
|
<DragOverlay adjustScale={false}>
|
||||||
{activeDragCipher ? (
|
{activeDragCipher ? (
|
||||||
@@ -385,6 +413,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
) : listItems}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
{!props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
FileKey2,
|
FileKey2,
|
||||||
@@ -438,8 +438,7 @@ export function firstPasskeyCreationTime(cipher: Cipher | null): string | null {
|
|||||||
const failedIconHosts = new Set<string>();
|
const failedIconHosts = new Set<string>();
|
||||||
|
|
||||||
export function VaultListIcon({ cipher }: { cipher: Cipher }) {
|
export function VaultListIcon({ cipher }: { cipher: Cipher }) {
|
||||||
const uri = firstCipherUri(cipher);
|
const host = useMemo(() => hostFromUri(firstCipherUri(cipher)), [cipher]);
|
||||||
const host = hostFromUri(uri);
|
|
||||||
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
|
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const markIconError = () => {
|
const markIconError = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user