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);
}, [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 map: Record<string, boolean> = {};
for (const cipher of filteredCiphers) map[cipher.id] = true;
@@ -1192,6 +1206,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
onSyncVault={handleSyncVault}
onOpenBulkDelete={handleOpenBulkDelete}
onSelectDuplicates={handleSelectDuplicates}
onSelectUniqueFromDuplicates={handleSelectUniqueFromDuplicates}
onSelectAll={handleSelectAll}
onToggleCreateMenu={handleToggleCreateMenu}
onStartCreate={startCreate}
+43 -33
View File
@@ -81,6 +81,7 @@ interface VaultListPanelProps {
onSyncVault: () => void;
onOpenBulkDelete: () => void;
onSelectDuplicates: () => void;
onSelectUniqueFromDuplicates: () => void;
onSelectAll: () => void;
onToggleCreateMenu: () => 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 ? (
<div className="duplicate-mode-head-menu">
{props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? (
<div className="duplicate-mode-head-menu mobile-duplicate-toolbar">
<div className="mobile-duplicate-mode-select-wrap">
{renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)}
</div>
) : (
<>
<input
className="search-input"
placeholder={t('txt_search_items_count', { count: props.totalCipherCount })}
value={props.searchInput}
onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)}
onCompositionStart={props.onSearchCompositionStart}
onCompositionEnd={(e) => props.onSearchCompositionEnd((e.currentTarget as HTMLInputElement).value)}
onKeyDown={(e) => {
if (e.key !== 'Escape' || !props.searchInput) return;
e.preventDefault();
props.onClearSearch();
}}
/>
{!!props.searchInput && (
<button
type="button"
className="search-clear-btn"
aria-label={t('txt_clear_search')}
title={t('txt_clear_search_esc')}
onClick={props.onClearSearch}
>
<X size={14} />
</button>
)}
</>
)}
</div>
<button type="button" className="btn btn-secondary small" onClick={props.onSelectUniqueFromDuplicates}>
<Check size={14} className="btn-icon" /> {t('txt_select_duplicate_items')}
</button>
</div>
) : (
<div className="search-input-wrap">
<input
className="search-input"
placeholder={t('txt_search_items_count', { count: props.totalCipherCount })}
value={props.searchInput}
onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)}
onCompositionStart={props.onSearchCompositionStart}
onCompositionEnd={(e) => props.onSearchCompositionEnd((e.currentTarget as HTMLInputElement).value)}
onKeyDown={(e) => {
if (e.key !== 'Escape' || !props.searchInput) return;
e.preventDefault();
props.onClearSearch();
}}
/>
{!!props.searchInput && (
<button
type="button"
className="search-clear-btn"
aria-label={t('txt_clear_search')}
title={t('txt_clear_search_esc')}
onClick={props.onClearSearch}
>
<X size={14} />
</button>
)}
</div>
)}
{props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && (
<div className="duplicate-mode-head-menu">
{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}>
<RefreshCw size={14} className="btn-icon" /> {t('txt_sync_vault')}
</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>
+14
View File
@@ -427,6 +427,20 @@
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 {
@apply justify-end overflow-visible pb-0.5;
flex-wrap: unset;