From 3fe344c3a04a6d2101c7fccef95509571a8f14f5 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Thu, 5 Mar 2026 22:40:24 +0800 Subject: [PATCH] Fix QA review findings: per-card responding state, preserve data on detach C-01: ConnectionRequestCard now uses local isResponding state instead of shared hook boolean, so accepting one card doesn't disable all others. C-03: detach_umbral_contact no longer wipes person data (name, email, phone, etc.) when a connection is severed. The person becomes a standard contact with all data preserved, preventing data loss for pre-existing contacts that were linked to connections. Co-Authored-By: Claude Opus 4.6 --- backend/app/services/connection.py | 16 ++++++---------- .../connections/ConnectionRequestCard.tsx | 6 +++++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/app/services/connection.py b/backend/app/services/connection.py index e169e5a..ab6e838 100644 --- a/backend/app/services/connection.py +++ b/backend/app/services/connection.py @@ -126,19 +126,15 @@ def create_person_from_connection( async def detach_umbral_contact(person: Person) -> None: - """Convert an umbral contact back to a standard contact. Does NOT commit.""" + """Convert an umbral contact back to a standard contact. Does NOT commit. + + Preserves all person data (name, email, phone, etc.) so the user does not + lose contact information when a connection is severed. Only unlinks the + umbral association — the person becomes a standard contact. + """ person.linked_user_id = None person.is_umbral_contact = False person.category = None - # Clear all shareable fields — they were populated from the connection - # Preserve first_name from existing name so the contact is not left blank - fallback_name = person.first_name or person.name or None - for field in SHAREABLE_FIELDS: - if hasattr(person, field): - setattr(person, field, None) - person.first_name = fallback_name - # Recompute display name - person.name = fallback_name or "Removed Contact" def extract_ntfy_config(settings: Settings) -> dict | None: """Extract ntfy config values into a plain dict safe for use after session close.""" diff --git a/frontend/src/components/connections/ConnectionRequestCard.tsx b/frontend/src/components/connections/ConnectionRequestCard.tsx index a0b19bf..366fff6 100644 --- a/frontend/src/components/connections/ConnectionRequestCard.tsx +++ b/frontend/src/components/connections/ConnectionRequestCard.tsx @@ -15,7 +15,8 @@ interface ConnectionRequestCardProps { } export default function ConnectionRequestCard({ request, direction }: ConnectionRequestCardProps) { - const { respond, isResponding, cancelRequest, isCancelling } = useConnections(); + const { respond, cancelRequest, isCancelling } = useConnections(); + const [isResponding, setIsResponding] = useState(false); const [resolved, setResolved] = useState(false); // Clean up invisible DOM element after fade-out transition @@ -29,6 +30,7 @@ export default function ConnectionRequestCard({ request, direction }: Connection if (hidden) return null; const handleRespond = async (action: 'accept' | 'reject') => { + setIsResponding(true); try { await respond({ requestId: request.id, action }); setResolved(true); @@ -41,6 +43,8 @@ export default function ConnectionRequestCard({ request, direction }: Connection } else { toast.error(getErrorMessage(err, 'Failed to respond')); } + } finally { + setIsResponding(false); } };