From f1141b05f96a795a0245262d233f5bfc858104bc Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 9 Aug 2026 07:19:18 +0000 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20edit=20a=20vault=20item=20?= =?UTF-8?q?=E2=80=94=20change=20a=20value,=20remove=20a=20field,=20rename?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer/src/components/ItemDetail.tsx | 176 +++++++++++++++--- 1 file changed, 149 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/ItemDetail.tsx b/apps/desktop/src/renderer/src/components/ItemDetail.tsx index 276bfe3..db40069 100644 --- a/apps/desktop/src/renderer/src/components/ItemDetail.tsx +++ b/apps/desktop/src/renderer/src/components/ItemDetail.tsx @@ -13,7 +13,7 @@ import { Button } from '@constructive-io/ui/button'; import { Input } from '@constructive-io/ui/input'; import { Progress } from '@constructive-io/ui/progress'; import { Separator } from '@constructive-io/ui/separator'; -import { Copy, Eye, EyeOff, Plus, Star, Trash2, X } from 'lucide-react'; +import { Check, Copy, Eye, EyeOff, Pencil, Plus, Star, Trash2, X } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; @@ -53,10 +53,16 @@ export const ItemDetail = ({ const [newFieldValue, setNewFieldValue] = useState(''); const [newTag, setNewTag] = useState(''); const [totp, setTotp] = useState(null); + /** The field being edited, and the value typed so far. */ + const [editing, setEditing] = useState<{ name: string; value: string } | null>(null); + /** Non-null while the title is being renamed. */ + const [title, setTitle] = useState(null); + const [removing, setRemoving] = useState(null); const icons = useBrandIcons([item.title]); const refresh = useCallback(async () => { setRevealed({}); + setEditing(null); setLoaded({ itemId: item.id, fields: await dcrypt.fields.list(item.id) }); setUrls(await dcrypt.organize.urls(item.id)); setTags(await dcrypt.organize.tags(item.id)); @@ -113,6 +119,35 @@ export const ItemDetail = ({ toast.success(`Copied ${name} — clipboard clears in 30s`); }; + /** Opens the editor on a field, revealing what is there to be edited. */ + const startEdit = async (field: VaultFieldMeta) => { + const value = revealed[field.name] ?? (await dcrypt.fields.reveal(item.id, field.name)); + setEditing({ name: field.name, value }); + }; + + const saveEdit = async (field: VaultFieldMeta) => { + if (!editing) return; + // same name and purpose, so the vault replaces the value in place and the + // database keeps the previous one in the item's password history + await dcrypt.fields.set(item.id, field.name, field.purpose, editing.value, field.concealed); + toast.success(`Updated ${field.name}`); + await refresh(); + }; + + const removeField = async (field: VaultFieldMeta) => { + await dcrypt.fields.remove(item.id, field.name); + setRemoving(null); + toast.success(`Removed ${field.name}`); + await refresh(); + }; + + const saveTitle = async () => { + const next = title?.trim(); + if (next && next !== item.title) await dcrypt.items.rename(item.id, next); + setTitle(null); + onChanged(); + }; + const addField = async () => { if (!newFieldName.trim() || !newFieldValue) return; await dcrypt.fields.set(item.id, newFieldName.trim(), 'text', newFieldValue); @@ -146,7 +181,28 @@ export const ItemDetail = ({

- {item.title} + {title === null ? ( + + ) : ( + setTitle(e.target.value)} + onBlur={() => void saveTitle()} + onKeyDown={(e) => { + if (e.key === 'Enter') void saveTitle(); + if (e.key === 'Escape') setTitle(null); + }} + className="h-9 text-xl font-semibold" + aria-label="Item title" + /> + )}

{KIND_LABEL[item.kind] ?? item.kind} @@ -208,24 +264,38 @@ export const ItemDetail = ({ {fields.map((field) => (
{field.name} - { - if (!field.concealed && revealed[field.name] === undefined) { - void toggleReveal(field.name); + {editing?.name === field.name ? ( + setEditing({ name: field.name, value: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') void saveEdit(field); + if (e.key === 'Escape') setEditing(null); + }} + className="font-mono" + aria-label={`${field.name} value`} + /> + ) : ( + - {field.concealed && ( + onFocus={() => { + if (!field.concealed && revealed[field.name] === undefined) { + void toggleReveal(field.name); + } + }} + className="font-mono" + /> + )} + {field.concealed && editing?.name !== field.name && ( + {editing?.name === field.name ? ( + <> + + + + ) : ( + <> + + + + + )}
))} {!fields.length && ( @@ -299,6 +403,24 @@ export const ItemDetail = ({
+ !open && setRemoving(null)}> + + + Remove {removing?.name}? + + The value is deleted from the vault. A field has no trash to go to, + so this cannot be undone. + + + + Cancel + removing && void removeField(removing)}> + Remove field + + + + +