import type { RefObject } from 'preact'; import { CheckCheck, Download, GripVertical, Paperclip, Plus, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact'; import { useEffect, useRef, useState } from 'preact/hooks'; import { closestCenter, DndContext, type DragEndEvent, type DragStartEvent, PointerSensor, TouchSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types'; import { t } from '@/lib/i18n'; import { CREATE_TYPE_OPTIONS, cipherTypeLabel, createEmptyLoginUri, formatAttachmentSize, toBooleanFieldValue, WEBSITE_MATCH_OPTIONS } from '@/components/vault/vault-page-helpers'; interface VaultEditorProps { draft: VaultDraft; isCreating: boolean; busy: boolean; folders: Folder[]; selectedCipher: Cipher | null; editExistingAttachments: Array; removedAttachmentIds: Record; removedAttachmentCount: number; attachmentQueue: File[]; attachmentInputRef: RefObject; localError: string; downloadingAttachmentKey: string; attachmentDownloadPercent: number | null; uploadingAttachmentName: string; attachmentUploadPercent: number | null; onUpdateDraft: (patch: Partial) => void; onSeedSshDefaults: (force?: boolean) => void; onUpdateSshPublicKey: (value: string) => void; onUpdateDraftLoginUri: (index: number, value: string) => void; onUpdateDraftLoginUriMatch: (index: number, value: number | null) => void; onReorderDraftLoginUri: (fromIndex: number, toIndex: number) => void; onQueueAttachmentFiles: (list: FileList | null) => void; onToggleExistingAttachmentRemoval: (attachmentId: string) => void; onRemoveQueuedAttachment: (index: number) => void; onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void; onPatchDraftCustomField: (index: number, patch: Partial) => void; onUpdateDraftCustomFields: (fields: VaultDraftField[]) => void; onOpenFieldModal: () => void; onSave: () => void; onCancel: () => void; onDeleteSelected: () => void; } interface SortableWebsiteRowProps { id: string; uriEntry: VaultDraft['loginUris'][number]; index: number; canRemove: boolean; isDragging: boolean; onUpdateUri: (index: number, value: string) => void; onUpdateMatch: (index: number, value: number | null) => void; onRemove: (index: number) => void; } function SortableWebsiteRow(props: SortableWebsiteRowProps) { const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({ id: props.id, }); const style = { transform: CSS.Transform.toString(transform), transition, }; return (
props.onUpdateUri(props.index, (e.currentTarget as HTMLInputElement).value)} /> {props.canRemove && ( )}
); } export default function VaultEditor(props: VaultEditorProps) { const uriIdSeedRef = useRef(0); const [uriItemIds, setUriItemIds] = useState([]); const [activeUriId, setActiveUriId] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6, }, }), useSensor(TouchSensor, { activationConstraint: { delay: 120, tolerance: 8, }, }), ); const createUriId = () => `login-uri-${uriIdSeedRef.current++}`; useEffect(() => { setUriItemIds((prev) => { if (prev.length === props.draft.loginUris.length) return prev; if (prev.length < props.draft.loginUris.length) { return [...prev, ...Array.from({ length: props.draft.loginUris.length - prev.length }, () => createUriId())]; } return prev.slice(0, props.draft.loginUris.length); }); }, [props.draft.loginUris.length]); useEffect(() => { setUriItemIds(props.draft.loginUris.map(() => createUriId())); setActiveUriId(null); }, [props.draft.id, props.isCreating]); const formatDownloadLabel = (attachmentId: string) => { const downloadKey = `${props.selectedCipher?.id || ''}:${attachmentId}`; if (props.downloadingAttachmentKey !== downloadKey) return t('txt_download'); return props.attachmentDownloadPercent == null ? t('txt_downloading') : t('txt_downloading_percent', { percent: props.attachmentDownloadPercent }); }; const uploadLabel = props.attachmentUploadPercent == null ? t('txt_uploading_attachment_named', { name: props.uploadingAttachmentName || t('txt_attachment') }) : t('txt_uploading_attachment_named_percent', { name: props.uploadingAttachmentName || t('txt_attachment'), percent: props.attachmentUploadPercent, }); const addLoginUri = () => { setUriItemIds((prev) => [...prev, createUriId()]); props.onUpdateDraft({ loginUris: [...props.draft.loginUris, createEmptyLoginUri()] }); }; const removeLoginUri = (index: number) => { setUriItemIds((prev) => prev.filter((_, itemIndex) => itemIndex !== index)); props.onUpdateDraft({ loginUris: props.draft.loginUris.filter((_, itemIndex) => itemIndex !== index) }); }; const handleWebsiteDragStart = (event: DragStartEvent) => { setActiveUriId(String(event.active.id)); }; const handleWebsiteDragEnd = (event: DragEndEvent) => { const activeId = String(event.active.id); const overId = event.over ? String(event.over.id) : null; setActiveUriId(null); if (!overId || activeId === overId) return; const fromIndex = uriItemIds.indexOf(activeId); const toIndex = uriItemIds.indexOf(overId); if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return; setUriItemIds((prev) => arrayMove(prev, fromIndex, toIndex)); props.onReorderDraftLoginUri(fromIndex, toIndex); }; return ( <>

{props.isCreating ? t('txt_new_type_header', { type: cipherTypeLabel(props.draft.type) }) : t('txt_edit_type_header', { type: cipherTypeLabel(props.draft.type) })}

{props.draft.type === 1 && (

{t('txt_login_credentials')}

{t('txt_websites')}

{props.draft.loginUris.map((uriEntry, index) => ( 1} isDragging={activeUriId === uriItemIds[index]} onUpdateUri={props.onUpdateDraftLoginUri} onUpdateMatch={props.onUpdateDraftLoginUriMatch} onRemove={removeLoginUri} /> ))}
)} {props.draft.type === 3 && (

{t('txt_card_details')}

)} {props.draft.type === 4 && (

{t('txt_identity_details')}

)} {props.draft.type === 5 && (

{t('txt_ssh_key')}