mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
refactor: enhance manual chunking in Vite config for better code splitting
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
import { Clipboard, Download, Eye, EyeOff, ExternalLink, Paperclip, Pencil, Trash2 } from 'lucide-preact';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
TOTP_PERIOD_SECONDS,
|
||||
TOTP_RING_CIRCUMFERENCE,
|
||||
copyToClipboard,
|
||||
formatAttachmentSize,
|
||||
formatHistoryTime,
|
||||
formatTotp,
|
||||
maskSecret,
|
||||
openUri,
|
||||
parseFieldType,
|
||||
toBooleanFieldValue,
|
||||
} from '@/components/vault/vault-page-helpers';
|
||||
|
||||
interface VaultDetailViewProps {
|
||||
selectedCipher: Cipher;
|
||||
repromptApprovedCipherId: string | null;
|
||||
showPassword: boolean;
|
||||
totpLive: { code: string; remain: number } | null;
|
||||
passkeyCreatedAt: string | null;
|
||||
hiddenFieldVisibleMap: Record<number, boolean>;
|
||||
folderName: (id: string | null | undefined) => string;
|
||||
onOpenReprompt: () => void;
|
||||
onToggleShowPassword: () => void;
|
||||
onToggleHiddenField: (index: number) => void;
|
||||
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void;
|
||||
onStartEdit: () => void;
|
||||
onDelete: (cipher: Cipher) => void;
|
||||
}
|
||||
|
||||
export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
const selectedAttachments = Array.isArray(props.selectedCipher.attachments) ? props.selectedCipher.attachments : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{Number(props.selectedCipher.reprompt || 0) === 1 && props.repromptApprovedCipherId !== props.selectedCipher.id && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_master_password_reprompt_2')}</h4>
|
||||
<div className="detail-sub">{t('txt_this_item_requires_master_password_every_time_before_viewing_details')}</div>
|
||||
<div className="actions" style={{ marginTop: '10px' }}>
|
||||
<button type="button" className="btn btn-primary" onClick={props.onOpenReprompt}>
|
||||
<Eye size={14} className="btn-icon" /> {t('txt_unlock_details')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(Number(props.selectedCipher.reprompt || 0) !== 1 || props.repromptApprovedCipherId === props.selectedCipher.id) && (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{props.selectedCipher.login && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_login_credentials')}</h4>
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_username')}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={props.selectedCipher.login.decUsername || ''}>{props.selectedCipher.login.decUsername || ''}</strong>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => copyToClipboard(props.selectedCipher.login?.decUsername || '')}>
|
||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_password')}</span>
|
||||
<div className="kv-main">
|
||||
<strong>{props.showPassword ? props.selectedCipher.login.decPassword || '' : maskSecret(props.selectedCipher.login.decPassword || '')}</strong>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
<button type="button" className="btn btn-secondary small" onClick={props.onToggleShowPassword}>
|
||||
{props.showPassword ? <EyeOff size={14} className="btn-icon" /> : <Eye size={14} className="btn-icon" />}
|
||||
{props.showPassword ? t('txt_hide') : t('txt_reveal')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => copyToClipboard(props.selectedCipher.login?.decPassword || '')}>
|
||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!!props.selectedCipher.login.decTotp && (
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_totp')}</span>
|
||||
<div className="kv-main">
|
||||
<div className="totp-inline">
|
||||
<strong>{props.totpLive ? formatTotp(props.totpLive.code) : t('txt_text_3')}</strong>
|
||||
<div
|
||||
className="totp-timer"
|
||||
title={t('txt_refresh_in_seconds_s', { seconds: props.totpLive ? props.totpLive.remain : 0 })}
|
||||
aria-label={t('txt_refresh_in_seconds_s', { seconds: props.totpLive ? props.totpLive.remain : 0 })}
|
||||
>
|
||||
<svg viewBox="0 0 36 36" className="totp-ring" role="presentation" aria-hidden="true">
|
||||
<circle className="totp-ring-track" cx="18" cy="18" r="15.9155" />
|
||||
<circle
|
||||
className="totp-ring-progress"
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="15.9155"
|
||||
style={{
|
||||
strokeDasharray: `${TOTP_RING_CIRCUMFERENCE} ${TOTP_RING_CIRCUMFERENCE}`,
|
||||
strokeDashoffset: String(
|
||||
TOTP_RING_CIRCUMFERENCE -
|
||||
TOTP_RING_CIRCUMFERENCE *
|
||||
(Math.max(0, Math.min(TOTP_PERIOD_SECONDS, props.totpLive?.remain ?? 0)) / TOTP_PERIOD_SECONDS)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
<span className="totp-timer-value">{props.totpLive ? props.totpLive.remain : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => copyToClipboard(props.totpLive?.code || '')}>
|
||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!props.passkeyCreatedAt && (
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_passkey')}</span>
|
||||
<div className="kv-main">
|
||||
<strong>{t('txt_passkey_created_at_value', { value: formatHistoryTime(props.passkeyCreatedAt) })}</strong>
|
||||
</div>
|
||||
<div className="kv-actions" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(props.selectedCipher.login?.uris || []).length > 0 && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_autofill_options')}</h4>
|
||||
{(props.selectedCipher.login?.uris || []).map((uri, index) => {
|
||||
const value = uri.decUri || uri.uri || '';
|
||||
if (!value.trim()) return null;
|
||||
return (
|
||||
<div key={`view-uri-${index}`} className="kv-row">
|
||||
<span className="kv-label">{t('txt_website')}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={value}>{value}</strong>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => openUri(value)}>
|
||||
<ExternalLink size={14} className="btn-icon" /> {t('txt_open')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => copyToClipboard(value)}>
|
||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.selectedCipher.card && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_card_details')}</h4>
|
||||
<div className="kv-line"><span>{t('txt_cardholder_name')}</span><strong>{props.selectedCipher.card.decCardholderName || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_number')}</span><strong>{props.selectedCipher.card.decNumber || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_brand')}</span><strong>{props.selectedCipher.card.decBrand || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_expiry')}</span><strong>{`${props.selectedCipher.card.decExpMonth || ''}/${props.selectedCipher.card.decExpYear || ''}`}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_security_code')}</span><strong>{props.selectedCipher.card.decCode || ''}</strong></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.selectedCipher.identity && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_identity_details')}</h4>
|
||||
<div className="kv-line"><span>{t('txt_name')}</span><strong>{`${props.selectedCipher.identity.decFirstName || ''} ${props.selectedCipher.identity.decLastName || ''}`.trim()}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_username')}</span><strong>{props.selectedCipher.identity.decUsername || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_email')}</span><strong>{props.selectedCipher.identity.decEmail || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_phone')}</span><strong>{props.selectedCipher.identity.decPhone || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_company')}</span><strong>{props.selectedCipher.identity.decCompany || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_address')}</span><strong>{[props.selectedCipher.identity.decAddress1, props.selectedCipher.identity.decAddress2, props.selectedCipher.identity.decAddress3, props.selectedCipher.identity.decCity, props.selectedCipher.identity.decState, props.selectedCipher.identity.decPostalCode, props.selectedCipher.identity.decCountry].filter(Boolean).join(', ')}</strong></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.selectedCipher.sshKey && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_ssh_key')}</h4>
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_private_key')}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={maskSecret(props.selectedCipher.sshKey.decPrivateKey || '')}>
|
||||
{maskSecret(props.selectedCipher.sshKey.decPrivateKey || '')}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="kv-actions" />
|
||||
</div>
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_public_key')}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={props.selectedCipher.sshKey.decPublicKey || ''}>
|
||||
{props.selectedCipher.sshKey.decPublicKey || ''}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="kv-actions" />
|
||||
</div>
|
||||
<div className="kv-row">
|
||||
<span className="kv-label">{t('txt_fingerprint')}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={props.selectedCipher.sshKey.decFingerprint || ''}>
|
||||
{props.selectedCipher.sshKey.decFingerprint || ''}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="kv-actions" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!(props.selectedCipher.decNotes || '').trim() && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_notes')}</h4>
|
||||
<div className="notes">{props.selectedCipher.decNotes || ''}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(props.selectedCipher.fields || []).some((x) => parseFieldType(x.type) !== 3) && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_custom_fields')}</h4>
|
||||
{(props.selectedCipher.fields || [])
|
||||
.filter((x) => parseFieldType(x.type) !== 3)
|
||||
.map((field, index) => {
|
||||
const fieldType = parseFieldType(field.type);
|
||||
const fieldName = field.decName || t('txt_field');
|
||||
const rawValue = field.decValue || '';
|
||||
const isHiddenVisible = !!props.hiddenFieldVisibleMap[index];
|
||||
if (fieldType === 2) {
|
||||
const checked = toBooleanFieldValue(rawValue);
|
||||
return (
|
||||
<div key={`view-field-${index}`} className="kv-row custom-field-row">
|
||||
<span className="kv-label" title={fieldName}>{fieldName}</span>
|
||||
<div className="kv-main boolean-main">
|
||||
<label className="check-line cf-check view">
|
||||
<input type="checkbox" checked={checked} disabled />
|
||||
</label>
|
||||
<span className="boolean-text value-ellipsis" title={checked ? t('txt_checked') : t('txt_unchecked')}>
|
||||
{checked ? t('txt_checked') : t('txt_unchecked')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv-actions" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={`view-field-${index}`} className="kv-row custom-field-row">
|
||||
<span className="kv-label" title={fieldName}>{fieldName}</span>
|
||||
<div className="kv-main">
|
||||
<strong className="value-ellipsis" title={fieldType === 1 && !isHiddenVisible ? '' : rawValue}>
|
||||
{fieldType === 1 && !isHiddenVisible ? maskSecret(rawValue) : rawValue}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
{fieldType === 1 && (
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => props.onToggleHiddenField(index)}>
|
||||
{isHiddenVisible ? <EyeOff size={14} className="btn-icon" /> : <Eye size={14} className="btn-icon" />}
|
||||
{isHiddenVisible ? t('txt_hide') : t('txt_reveal')}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => copyToClipboard(rawValue)}>
|
||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAttachments.some((attachment) => String(attachment?.id || '').trim()) && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_attachments')}</h4>
|
||||
<div className="attachment-list">
|
||||
{selectedAttachments.map((attachment) => {
|
||||
const attachmentId = String(attachment?.id || '').trim();
|
||||
if (!attachmentId) return null;
|
||||
const fileName = String(attachment.decFileName || attachment.fileName || attachmentId).trim() || attachmentId;
|
||||
return (
|
||||
<div key={`view-attachment-${attachmentId}`} className="attachment-row">
|
||||
<div className="attachment-main">
|
||||
<Paperclip size={14} />
|
||||
<div className="attachment-text">
|
||||
<strong className="value-ellipsis" title={fileName}>{fileName}</strong>
|
||||
<span>{formatAttachmentSize(attachment)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kv-actions">
|
||||
<button type="button" className="btn btn-secondary small" onClick={() => props.onDownloadAttachment(props.selectedCipher, attachmentId)}>
|
||||
<Download size={14} className="btn-icon" /> {t('txt_download')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(props.selectedCipher.creationDate || props.selectedCipher.revisionDate) && (
|
||||
<div className="card">
|
||||
<h4>{t('txt_item_history')}</h4>
|
||||
<div className="detail-sub">{t('txt_last_edited_value', { value: formatHistoryTime(props.selectedCipher.revisionDate) })}</div>
|
||||
<div className="detail-sub">{t('txt_created_value', { value: formatHistoryTime(props.selectedCipher.creationDate) })}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="detail-actions">
|
||||
<div className="actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={props.onStartEdit}>
|
||||
<Pencil size={14} className="btn-icon" /> {t('txt_edit')}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="btn btn-danger" onClick={() => props.onDelete(props.selectedCipher)}>
|
||||
<Trash2 size={14} className="btn-icon" /> {t('txt_delete')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user