From 0f48b90adfcc18ec95f1c2cd399beda507971176 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Thu, 4 Sep 2025 19:58:32 +0000 Subject: [PATCH] fix: webhook header inputs losing focus after each keystroke (addresses #412) Refactored header management to use a separate array with stable IDs instead of iterating over object entries. This prevents React/SolidJS from losing track of which input has focus when the headers object changes. --- .../src/components/Alerts/WebhookConfig.tsx | 116 +++++++++++++----- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/frontend-modern/src/components/Alerts/WebhookConfig.tsx b/frontend-modern/src/components/Alerts/WebhookConfig.tsx index 679adb918..39246248a 100644 --- a/frontend-modern/src/components/Alerts/WebhookConfig.tsx +++ b/frontend-modern/src/components/Alerts/WebhookConfig.tsx @@ -35,6 +35,37 @@ export function WebhookConfig(props: WebhookConfigProps) { const [templates, setTemplates] = createSignal([]); const [showServiceDropdown, setShowServiceDropdown] = createSignal(false); + // Track header inputs separately to avoid focus loss + const [headerInputs, setHeaderInputs] = createSignal>([]); + + // Sync headerInputs with formData.headers + createEffect(() => { + const headers = formData().headers || {}; + const currentInputs = headerInputs(); + + // Only update if we're starting fresh (not already editing headers) + if (currentInputs.length === 0 && Object.keys(headers).length > 0) { + setHeaderInputs( + Object.entries(headers).map(([key, value], index) => ({ + id: `header-${Date.now()}-${index}`, + key, + value + })) + ); + } + }); + + // Update formData.headers when headerInputs change + const updateHeadersInFormData = () => { + const headers: Record = {}; + headerInputs().forEach(input => { + if (input.key) { + headers[input.key] = input.value; + } + }); + setFormData({ ...formData(), headers }); + }; + // Load webhook templates createEffect(async () => { try { @@ -46,6 +77,9 @@ export function WebhookConfig(props: WebhookConfigProps) { }); const saveWebhook = () => { + // First sync headers from inputs to formData + updateHeadersInFormData(); + const data = formData(); if (!data.name || !data.url) return; @@ -58,6 +92,7 @@ export function WebhookConfig(props: WebhookConfigProps) { }); setEditingId(null); setAdding(false); + setHeaderInputs([]); } else { // onAdd expects a webhook without id, but with service const newWebhook: Omit = { @@ -80,6 +115,7 @@ export function WebhookConfig(props: WebhookConfigProps) { enabled: true, payloadTemplate: '' }); + setHeaderInputs([]); setAdding(false); } }; @@ -96,6 +132,7 @@ export function WebhookConfig(props: WebhookConfigProps) { enabled: true, payloadTemplate: '' }); + setHeaderInputs([]); }; const editWebhook = (webhook: Webhook) => { @@ -105,6 +142,15 @@ export function WebhookConfig(props: WebhookConfigProps) { service: webhook.service || 'generic', payloadTemplate: webhook.template || '' }); + // Set up header inputs for editing + const headers = webhook.headers || {}; + setHeaderInputs( + Object.entries(headers).map(([key, value], index) => ({ + id: `header-${Date.now()}-${index}`, + key, + value + })) + ); setAdding(true); }; @@ -121,6 +167,15 @@ export function WebhookConfig(props: WebhookConfigProps) { // Only generic service should have custom payloads payloadTemplate: service === 'generic' ? formData().payloadTemplate : '' }); + // Update header inputs when switching services + const headers = template.headers || {}; + setHeaderInputs( + Object.entries(headers).map(([key, value], index) => ({ + id: `header-${Date.now()}-${index}`, + key, + value + })) + ); } setShowServiceDropdown(false); }; @@ -376,38 +431,29 @@ export function WebhookConfig(props: WebhookConfigProps) {
- - {([key, value]) => ( + + {(header) => (
{ const newKey = e.currentTarget.value; - if (key !== newKey) { - const headers = { ...formData().headers }; - // Preserve order by rebuilding the headers object - const newHeaders: Record = {}; - for (const [k, v] of Object.entries(headers)) { - if (k === key) { - newHeaders[newKey] = v; - } else { - newHeaders[k] = v; - } - } - setFormData({ ...formData(), headers: newHeaders }); - } + setHeaderInputs(inputs => + inputs.map(h => h.id === header.id ? { ...h, key: newKey } : h) + ); }} placeholder="Header Name" class="flex-1 px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" /> { - const headers = { ...formData().headers }; - headers[key] = e.currentTarget.value; - setFormData({ ...formData(), headers }); + const newValue = e.currentTarget.value; + setHeaderInputs(inputs => + inputs.map(h => h.id === header.id ? { ...h, value: newValue } : h) + ); }} placeholder="Header Value" class="flex-1 px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" @@ -415,9 +461,7 @@ export function WebhookConfig(props: WebhookConfigProps) {