mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-06 15:10:13 +00:00
feat: add archiving functionality for ciphers
- Introduced `archive` and `unarchive` endpoints in the API for ciphers. - Implemented bulk archiving and unarchiving of ciphers in the vault. - Updated the storage schema to include `archived_at` timestamps for ciphers. - Enhanced user interface to support archiving actions in the vault. - Added necessary translations for archive-related actions. - Updated user and device models to accommodate new fields related to archiving.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Clipboard, Download, Eye, EyeOff, ExternalLink, Paperclip, Pencil, Trash2 } from 'lucide-preact';
|
||||
import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Paperclip, Pencil, RotateCcw, Trash2 } from 'lucide-preact';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
@@ -31,11 +31,14 @@ interface VaultDetailViewProps {
|
||||
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void;
|
||||
onStartEdit: () => void;
|
||||
onDelete: (cipher: Cipher) => void;
|
||||
onArchive: (cipher: Cipher) => Promise<void>;
|
||||
onUnarchive: (cipher: Cipher) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
const selectedAttachments = Array.isArray(props.selectedCipher.attachments) ? props.selectedCipher.attachments : [];
|
||||
const [showSshPrivateKey, setShowSshPrivateKey] = useState(false);
|
||||
const isArchived = !!(props.selectedCipher.archivedDate || (props.selectedCipher as { archivedAt?: string | null }).archivedAt);
|
||||
const formatDownloadLabel = (attachmentId: string) => {
|
||||
const downloadKey = `${props.selectedCipher.id}:${attachmentId}`;
|
||||
if (props.downloadingAttachmentKey !== downloadKey) return t('txt_download');
|
||||
@@ -62,6 +65,7 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
<div className="card">
|
||||
<h3 className="detail-title">{props.selectedCipher.decName || t('txt_no_name')}</h3>
|
||||
<div className="detail-sub">{props.folderName(props.selectedCipher.folderId)}</div>
|
||||
{isArchived && <div className="list-badge" style={{ marginTop: '8px', width: 'fit-content' }}>{t('txt_archived')}</div>}
|
||||
</div>
|
||||
|
||||
{props.selectedCipher.login && (
|
||||
@@ -351,6 +355,15 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
<button type="button" className="btn btn-secondary" onClick={props.onStartEdit}>
|
||||
<Pencil size={14} className="btn-icon" /> {t('txt_edit')}
|
||||
</button>
|
||||
{isArchived ? (
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void props.onUnarchive(props.selectedCipher)}>
|
||||
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void props.onArchive(props.selectedCipher)}>
|
||||
<Archive size={14} className="btn-icon" /> {t('txt_archive')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="btn btn-danger" onClick={() => props.onDelete(props.selectedCipher)}>
|
||||
<Trash2 size={14} className="btn-icon" /> {t('txt_delete')}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RefObject } from 'preact';
|
||||
import { ArrowUpDown, Check, CheckCheck, FolderInput, Plus, RefreshCw, Trash2, X } from 'lucide-preact';
|
||||
import { Archive, ArrowUpDown, Check, CheckCheck, FolderInput, Plus, RefreshCw, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
@@ -48,6 +48,8 @@ interface VaultListPanelProps {
|
||||
onToggleCreateMenu: () => void;
|
||||
onStartCreate: (type: number) => void;
|
||||
onBulkRestore: () => void;
|
||||
onBulkArchive: () => void;
|
||||
onBulkUnarchive: () => void;
|
||||
onOpenMove: () => void;
|
||||
onClearSelection: () => void;
|
||||
onScroll: (top: number) => void;
|
||||
@@ -139,7 +141,17 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
||||
<RefreshCw size={14} className="btn-icon" /> {t('txt_restore')}
|
||||
</button>
|
||||
)}
|
||||
{props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'duplicates' && (
|
||||
{props.selectedCount > 0 && props.sidebarFilter.kind === 'archive' && (
|
||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkUnarchive}>
|
||||
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
||||
</button>
|
||||
)}
|
||||
{props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkArchive}>
|
||||
<Archive size={14} className="btn-icon" /> {t('txt_archive_selected')}
|
||||
</button>
|
||||
)}
|
||||
{props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onOpenMove}>
|
||||
<FolderInput size={14} className="btn-icon" /> {t('txt_move')}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Archive,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Folder as FolderIcon,
|
||||
@@ -48,6 +49,9 @@ export default function VaultSidebar(props: VaultSidebarProps) {
|
||||
<button type="button" className={`tree-btn ${props.sidebarFilter.kind === 'favorite' ? 'active' : ''}`} onClick={() => props.onChangeFilter({ kind: 'favorite' })}>
|
||||
<Star size={14} className="tree-icon" /> <span className="tree-label">{t('txt_favorites')}</span>
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${props.sidebarFilter.kind === 'archive' ? 'active' : ''}`} onClick={() => props.onChangeFilter({ kind: 'archive' })}>
|
||||
<Archive size={14} className="tree-icon" /> <span className="tree-label">{t('txt_archive')}</span>
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${props.sidebarFilter.kind === 'trash' ? 'active' : ''}`} onClick={() => props.onChangeFilter({ kind: 'trash' })}>
|
||||
<Trash2 size={14} className="tree-icon" /> <span className="tree-label">{t('txt_trash')}</span>
|
||||
</button>
|
||||
|
||||
@@ -16,6 +16,7 @@ export type VaultSortMode = 'edited' | 'created' | 'name';
|
||||
export type SidebarFilter =
|
||||
| { kind: 'all' }
|
||||
| { kind: 'favorite' }
|
||||
| { kind: 'archive' }
|
||||
| { kind: 'trash' }
|
||||
| { kind: 'duplicates' }
|
||||
| { kind: 'type'; value: TypeFilter }
|
||||
@@ -71,6 +72,34 @@ export function cipherTypeKey(type: number): TypeFilter {
|
||||
return 'ssh';
|
||||
}
|
||||
|
||||
function cipherDeletedValue(cipher: Cipher): boolean {
|
||||
return !!(cipher.deletedDate || (cipher as { deletedAt?: string | null }).deletedAt);
|
||||
}
|
||||
|
||||
function cipherArchivedValue(cipher: Cipher): boolean {
|
||||
return !!(cipher.archivedDate || (cipher as { archivedAt?: string | null }).archivedAt);
|
||||
}
|
||||
|
||||
export function isCipherDeleted(cipher: Cipher): boolean {
|
||||
return cipherDeletedValue(cipher);
|
||||
}
|
||||
|
||||
export function isCipherArchived(cipher: Cipher): boolean {
|
||||
return cipherArchivedValue(cipher) && !cipherDeletedValue(cipher);
|
||||
}
|
||||
|
||||
export function isCipherVisibleInNormalVault(cipher: Cipher): boolean {
|
||||
return !cipherDeletedValue(cipher) && !cipherArchivedValue(cipher);
|
||||
}
|
||||
|
||||
export function isCipherVisibleInArchive(cipher: Cipher): boolean {
|
||||
return !cipherDeletedValue(cipher) && cipherArchivedValue(cipher);
|
||||
}
|
||||
|
||||
export function isCipherVisibleInTrash(cipher: Cipher): boolean {
|
||||
return cipherDeletedValue(cipher);
|
||||
}
|
||||
|
||||
export function cipherTypeLabel(type: number): string {
|
||||
if (type === 1) return t('txt_login');
|
||||
if (type === 3) return t('txt_card');
|
||||
|
||||
Reference in New Issue
Block a user