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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-05 22:40:24 +08:00
parent aeb30afbce
commit 3fe344c3a0
2 changed files with 11 additions and 11 deletions

View File

@ -126,19 +126,15 @@ def create_person_from_connection(
async def detach_umbral_contact(person: Person) -> None: 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.linked_user_id = None
person.is_umbral_contact = False person.is_umbral_contact = False
person.category = None 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: def extract_ntfy_config(settings: Settings) -> dict | None:
"""Extract ntfy config values into a plain dict safe for use after session close.""" """Extract ntfy config values into a plain dict safe for use after session close."""

View File

@ -15,7 +15,8 @@ interface ConnectionRequestCardProps {
} }
export default function ConnectionRequestCard({ request, direction }: 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); const [resolved, setResolved] = useState(false);
// Clean up invisible DOM element after fade-out transition // Clean up invisible DOM element after fade-out transition
@ -29,6 +30,7 @@ export default function ConnectionRequestCard({ request, direction }: Connection
if (hidden) return null; if (hidden) return null;
const handleRespond = async (action: 'accept' | 'reject') => { const handleRespond = async (action: 'accept' | 'reject') => {
setIsResponding(true);
try { try {
await respond({ requestId: request.id, action }); await respond({ requestId: request.id, action });
setResolved(true); setResolved(true);
@ -41,6 +43,8 @@ export default function ConnectionRequestCard({ request, direction }: Connection
} else { } else {
toast.error(getErrorMessage(err, 'Failed to respond')); toast.error(getErrorMessage(err, 'Failed to respond'));
} }
} finally {
setIsResponding(false);
} }
}; };