feat: add functionality to select unique items from duplicates in Vault components

This commit is contained in:
shuaiplus
2026-07-09 20:21:45 +08:00
committed by Shuai
parent a1b12fc447
commit 39d9df78ea
3 changed files with 72 additions and 33 deletions
+15
View File
@@ -1078,6 +1078,20 @@ const folderName = useCallback((id: string | null | undefined): string => {
} }
setSelectedMap(map); setSelectedMap(map);
}, [filteredCiphers, duplicateSignatureInfo, duplicateMode]); }, [filteredCiphers, duplicateSignatureInfo, duplicateMode]);
const handleSelectUniqueFromDuplicates = useCallback(() => {
const map: Record<string, boolean> = {};
const seen = new Set<number>();
for (const cipher of filteredCiphers) {
const groupIndex = duplicateGroupIndexById.get(cipher.id);
if (groupIndex === undefined) continue;
if (seen.has(groupIndex)) {
map[cipher.id] = true;
} else {
seen.add(groupIndex);
}
}
setSelectedMap(map);
}, [filteredCiphers, duplicateGroupIndexById]);
const handleSelectAll = useCallback(() => { const handleSelectAll = useCallback(() => {
const map: Record<string, boolean> = {}; const map: Record<string, boolean> = {};
for (const cipher of filteredCiphers) map[cipher.id] = true; for (const cipher of filteredCiphers) map[cipher.id] = true;
@@ -1192,6 +1206,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
onSyncVault={handleSyncVault} onSyncVault={handleSyncVault}
onOpenBulkDelete={handleOpenBulkDelete} onOpenBulkDelete={handleOpenBulkDelete}
onSelectDuplicates={handleSelectDuplicates} onSelectDuplicates={handleSelectDuplicates}
onSelectUniqueFromDuplicates={handleSelectUniqueFromDuplicates}
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
onToggleCreateMenu={handleToggleCreateMenu} onToggleCreateMenu={handleToggleCreateMenu}
onStartCreate={startCreate} onStartCreate={startCreate}
+43 -33
View File
@@ -81,6 +81,7 @@ interface VaultListPanelProps {
onSyncVault: () => void; onSyncVault: () => void;
onOpenBulkDelete: () => void; onOpenBulkDelete: () => void;
onSelectDuplicates: () => void; onSelectDuplicates: () => void;
onSelectUniqueFromDuplicates: () => void;
onSelectAll: () => void; onSelectAll: () => void;
onToggleCreateMenu: () => void; onToggleCreateMenu: () => void;
onStartCreate: (type: number) => void; onStartCreate: (type: number) => void;
@@ -319,40 +320,43 @@ export default function VaultListPanel(props: VaultListPanelProps) {
</> </>
) : ( ) : (
<> <>
<div className="search-input-wrap"> {props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? (
{props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? ( <div className="duplicate-mode-head-menu mobile-duplicate-toolbar">
<div className="duplicate-mode-head-menu"> <div className="mobile-duplicate-mode-select-wrap">
{renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)} {renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)}
</div> </div>
) : ( <button type="button" className="btn btn-secondary small" onClick={props.onSelectUniqueFromDuplicates}>
<> <Check size={14} className="btn-icon" /> {t('txt_select_duplicate_items')}
<input </button>
className="search-input" </div>
placeholder={t('txt_search_items_count', { count: props.totalCipherCount })} ) : (
value={props.searchInput} <div className="search-input-wrap">
onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)} <input
onCompositionStart={props.onSearchCompositionStart} className="search-input"
onCompositionEnd={(e) => props.onSearchCompositionEnd((e.currentTarget as HTMLInputElement).value)} placeholder={t('txt_search_items_count', { count: props.totalCipherCount })}
onKeyDown={(e) => { value={props.searchInput}
if (e.key !== 'Escape' || !props.searchInput) return; onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)}
e.preventDefault(); onCompositionStart={props.onSearchCompositionStart}
props.onClearSearch(); onCompositionEnd={(e) => props.onSearchCompositionEnd((e.currentTarget as HTMLInputElement).value)}
}} onKeyDown={(e) => {
/> if (e.key !== 'Escape' || !props.searchInput) return;
{!!props.searchInput && ( e.preventDefault();
<button props.onClearSearch();
type="button" }}
className="search-clear-btn" />
aria-label={t('txt_clear_search')} {!!props.searchInput && (
title={t('txt_clear_search_esc')} <button
onClick={props.onClearSearch} type="button"
> className="search-clear-btn"
<X size={14} /> aria-label={t('txt_clear_search')}
</button> title={t('txt_clear_search_esc')}
)} onClick={props.onClearSearch}
</> >
)} <X size={14} />
</div> </button>
)}
</div>
)}
{props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && ( {props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && (
<div className="duplicate-mode-head-menu"> <div className="duplicate-mode-head-menu">
{renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)} {renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)}
@@ -387,7 +391,13 @@ export default function VaultListPanel(props: VaultListPanelProps) {
<button type="button" className="btn btn-secondary small list-icon-btn" disabled={props.busy || props.loading} onClick={props.onSyncVault}> <button type="button" className="btn btn-secondary small list-icon-btn" disabled={props.busy || props.loading} onClick={props.onSyncVault}>
<RefreshCw size={14} className="btn-icon" /> {t('txt_sync_vault')} <RefreshCw size={14} className="btn-icon" /> {t('txt_sync_vault')}
</button> </button>
{!props.isMobileLayout && props.sidebarFilter !== undefined && createMenu} {props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout ? (
<button type="button" className="btn btn-secondary small" onClick={props.onSelectUniqueFromDuplicates}>
<Check size={14} className="btn-icon" /> {t('txt_select_duplicate_items')}
</button>
) : (
!props.isMobileLayout && props.sidebarFilter !== undefined && createMenu
)}
</> </>
)} )}
</div> </div>
+14
View File
@@ -427,6 +427,20 @@
min-width: max(100%, 190px); min-width: max(100%, 190px);
} }
.list-head .mobile-duplicate-toolbar {
@apply flex min-w-0 items-center gap-1.5;
flex: none;
}
.list-head .mobile-duplicate-mode-select-wrap {
max-width: 130px;
flex-shrink: 0;
}
.list-head .mobile-duplicate-mode-select-wrap .mobile-vault-filter-trigger {
@apply w-full;
}
.toolbar.actions { .toolbar.actions {
@apply justify-end overflow-visible pb-0.5; @apply justify-end overflow-visible pb-0.5;
flex-wrap: unset; flex-wrap: unset;