mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +00:00
feat: add functionality to select unique items from duplicates in Vault components
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user