From 6fee592e403b7c98a1db667c590eb25e904f326f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Tue, 7 Jul 2026 14:53:39 -0300 Subject: [PATCH] =?UTF-8?q?Resumen=20del=20fix=20=E2=80=94=20Prioridad=202?= =?UTF-8?q?:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios en WorkspacePage.tsx: orgName ahora es string | null | undefined. undefined = query no completó aún, null = proceso sin org (o query sin resultado), string = nombre real. Reemplazó .single() por .maybeSingle() — .single() lanzaba PGRST116 cuando el org no existía, data quedaba en null y setOrgName nunca se llamaba, dejando el estado indefinido. setOrgName(data?.name ?? null) se llama siempre, sin condición if (data). backLabel pasa de orgId ? (orgName ?? 'Volver') : 'Biblioteca' a simplemente orgName ?? 'Biblioteca' — el nombre real si cargó, o el fallback genérico en cualquier otro caso. Ya no depende de orgId para decidir el label. handleBack() no cambió — sigue usando currentProcess?.orgId para construir el URL correcto a /library?org=xxx. Listo para validación visual en workspace con un proceso de cliente que tenga org_id en la DB. --- src/auth/AuthContext.tsx | 22 ++++++++++++++----- src/features/workspace/WorkspacePage.tsx | 17 ++++++++------ .../workspace/workspace-back-button.test.ts | 12 +++++----- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx index 32824d6..13c5f97 100644 --- a/src/auth/AuthContext.tsx +++ b/src/auth/AuthContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react' +import { createContext, useContext, useEffect, useRef, useState, useCallback, type ReactNode } from 'react' import { SupabaseAuthService } from './SupabaseAuthService' import { supabase } from '@/lib/supabase' import type { AuthUser } from './types' @@ -67,18 +67,30 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [loading, setLoading] = useState(isOAuthRedirect) const [pendingPasswordReset, setPendingPasswordReset] = useState(false) const [isProfileLoaded, setIsProfileLoaded] = useState(false) + // Ref para comparar id entre eventos de auth y NO resetear isProfileLoaded en eventos + // como TOKEN_REFRESHED que llegan con el mismo usuario. Sin esto, si el evento llega + // después de que syncProfileFromDB completó, isProfileLoaded queda en false para siempre. + const lastSyncedUserIdRef = useRef( + isOAuthRedirect ? null : (getUserFromStorage()?.id ?? null) + ) useEffect(() => { // Verificar y sincronizar con Supabase en background. // Si la sesión expiró o fue revocada, onAuthStateChange llamará callback(null) // y el router redirigirá a /login. const unsubscribe = authService.onAuthStateChange((u) => { + const prevId = lastSyncedUserIdRef.current + lastSyncedUserIdRef.current = u?.id ?? null setUser(u) setLoading(false) - // Si no hay usuario, no hay nada que sincronizar → marcar como cargado. - // Si hay usuario, la sincronización con DB reseteará a false → true al completar. - if (!u) setIsProfileLoaded(true) - else setIsProfileLoaded(false) + if (!u) { + // Logout → nada que sincronizar + setIsProfileLoaded(true) + } else if (u.id !== prevId) { + // Usuario diferente → requiere nueva sincronización con DB + setIsProfileLoaded(false) + } + // Mismo usuario (TOKEN_REFRESHED, INITIAL_SESSION, etc.) → preservar isProfileLoaded }) return () => unsubscribe() }, []) diff --git a/src/features/workspace/WorkspacePage.tsx b/src/features/workspace/WorkspacePage.tsx index 20aebff..62d3f3c 100644 --- a/src/features/workspace/WorkspacePage.tsx +++ b/src/features/workspace/WorkspacePage.tsx @@ -68,12 +68,17 @@ export function WorkspacePage() { } }, [user, processId, navigate]) - // Nombre de la organización dueña del proceso (para el back button contextual) - const [orgName, setOrgName] = useState(null) + // Nombre de la organización dueña del proceso (para el back button contextual). + // undefined = todavía cargando, null = cargado sin resultado, string = nombre real. + const [orgName, setOrgName] = useState(undefined) useEffect(() => { if (!currentProcess?.orgId) { setOrgName(null); return } - supabase.from('organizations').select('name').eq('id', currentProcess.orgId).single() - .then(({ data }) => { if (data) setOrgName(data.name as string) }) + supabase + .from('organizations') + .select('name') + .eq('id', currentProcess.orgId) + .maybeSingle() + .then(({ data }) => setOrgName(data?.name ?? null)) }, [currentProcess?.orgId]) // Validación XOR: todos los gateways exclusivos deben sumar 1.0 @@ -196,9 +201,7 @@ export function WorkspacePage() { const isClientRole = user?.platformRole === 'client_editor' const backLabel = isClientRole ? 'Procesos' - : currentProcess.orgId - ? (orgName ?? 'Volver') - : 'Biblioteca' + : orgName ?? 'Biblioteca' function handleBack() { const orgId = currentProcess?.orgId diff --git a/tests/features/workspace/workspace-back-button.test.ts b/tests/features/workspace/workspace-back-button.test.ts index bc19589..f530cf0 100644 --- a/tests/features/workspace/workspace-back-button.test.ts +++ b/tests/features/workspace/workspace-back-button.test.ts @@ -9,14 +9,12 @@ type PlatformRole = 'platform_admin' | 'member' | 'client_editor' | 'client_view function resolveBackButton( platformRole: PlatformRole | undefined, orgId: string | null, - orgName: string | null, + orgName: string | null | undefined, // undefined = cargando, null = sin resultado ): { backLabel: string; backTo: string } { const isClientRole = platformRole === 'client_editor' || platformRole === 'client_viewer' const backLabel = isClientRole ? 'Procesos' - : orgId - ? (orgName ?? 'Volver') - : 'Biblioteca' + : orgName ?? 'Biblioteca' const backTo = orgId && !isClientRole ? `/library?org=${orgId}` : '/library' return { backLabel, backTo } } @@ -28,9 +26,9 @@ describe('workspace back button — lógica de backLabel y backTo', () => { expect(backTo).toBe('/library?org=org-uuid-123') }) - it('2. admin con proceso de cliente mientras orgName carga → muestra "Volver" como fallback', () => { - const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', null) - expect(backLabel).toBe('Volver') + it('2. admin con proceso de cliente mientras orgName carga → muestra "Biblioteca" como fallback', () => { + const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', undefined) + expect(backLabel).toBe('Biblioteca') expect(backTo).toBe('/library?org=org-uuid-123') })