From 000e97e1bd5ff3a3b66a33435ce9733ca83fe945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Tue, 9 Jun 2026 10:55:45 +0200 Subject: [PATCH] feat: implement inline editing for custom fields in ticket sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show CF values as read-only text with edit affordance (pencil icon on hover) - Click to enter edit mode: inline input (free-text) or select (choice fields) - Save on blur or Enter, cancel on Escape — reverts to original value - Auto-save for select fields on change - Loading spinner while saving - Remove now-unused customFieldValue helper Co-Authored-By: Claude Opus 4.8 --- web/src/app/tickets/[id]/page.tsx | 144 +++++++++++++++++++----------- 1 file changed, 92 insertions(+), 52 deletions(-) diff --git a/web/src/app/tickets/[id]/page.tsx b/web/src/app/tickets/[id]/page.tsx index b51899c..e0a75e3 100644 --- a/web/src/app/tickets/[id]/page.tsx +++ b/web/src/app/tickets/[id]/page.tsx @@ -233,6 +233,7 @@ export default function TicketDetailPage({ const [fieldSaving, setFieldSaving] = useState<"subject" | "owner" | null>(null); const [customFieldDrafts, setCustomFieldDrafts] = useState>({}); const [customFieldSaving, setCustomFieldSaving] = useState(null); + const [editingFieldId, setEditingFieldId] = useState(null); const fetchData = useCallback(async () => { setLoading(true); @@ -401,11 +402,14 @@ export default function TicketDetailPage({ } }; - const handleCustomFieldSave = async (fieldId: string) => { + const handleCustomFieldSave = async (fieldId: string, valueOverride?: string) => { if (!ticket || customFieldSaving) return; - const value = customFieldDrafts[fieldId]?.trim() ?? ""; + const value = (valueOverride ?? customFieldDrafts[fieldId] ?? "").trim(); const currentValue = ticket.custom_fields?.find((item) => item.custom_field_id === fieldId)?.value ?? ""; - if (value === currentValue) return; + if (value === currentValue) { + setEditingFieldId(null); + return; + } const field = queueFields.find((assignment) => assignment.custom_field_id === fieldId)?.custom_field; if (value && field?.pattern) { const regex = new RegExp(field.pattern); @@ -440,11 +444,9 @@ export default function TicketDetailPage({ ); } if (txRes.data) setTransactions(txRes.data); + setEditingFieldId(null); }; - const customFieldValue = (fieldId: string) => - ticket?.custom_fields?.find((item) => item.custom_field_id === fieldId)?.value ?? ""; - const handleSendComment = async () => { if (!replyText.trim() || sending) return; setSending(true); @@ -950,66 +952,104 @@ export default function TicketDetailPage({ const options = Array.isArray(field?.values) ? field.values.map((value) => String(value)) : []; - const fieldType = field?.field_type.toLowerCase() ?? ""; - const currentDraft = customFieldDrafts[fieldId] ?? customFieldValue(fieldId); - const dirty = currentDraft !== customFieldValue(fieldId); + const currentValue = + ticket?.custom_fields?.find((item) => item.custom_field_id === fieldId)?.value ?? ""; + const isEditing = editingFieldId === fieldId; + const draftValue = customFieldDrafts[fieldId] ?? currentValue; const isSaving = customFieldSaving === fieldId; return (
-
- {(fieldType.includes("select") || options.length > 0) && options.length > 0 ? ( - - ) : ( - - setCustomFieldDrafts((current) => ({ ...current, [fieldId]: event.target.value })) - } - onKeyDown={(event) => { - if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { - void handleCustomFieldSave(fieldId); + + {isEditing ? ( +
+ {options.length > 0 ? ( + + ) : ( + + setCustomFieldDrafts((c) => ({ ...c, [fieldId]: event.target.value })) } - }} - placeholder={field?.pattern ? field.pattern : "Not set"} - className="h-8 min-w-0 flex-1 rounded-md border border-input bg-card px-2 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-ring" - /> - )} - + )} +
+ ) : ( + -
+ )}
); })}