mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +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);
|
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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user