Resumen del fix — Prioridad 2:
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
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.
This commit is contained in:
@@ -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 { SupabaseAuthService } from './SupabaseAuthService'
|
||||||
import { supabase } from '@/lib/supabase'
|
import { supabase } from '@/lib/supabase'
|
||||||
import type { AuthUser } from './types'
|
import type { AuthUser } from './types'
|
||||||
@@ -67,18 +67,30 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [loading, setLoading] = useState(isOAuthRedirect)
|
const [loading, setLoading] = useState(isOAuthRedirect)
|
||||||
const [pendingPasswordReset, setPendingPasswordReset] = useState(false)
|
const [pendingPasswordReset, setPendingPasswordReset] = useState(false)
|
||||||
const [isProfileLoaded, setIsProfileLoaded] = 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<string | null>(
|
||||||
|
isOAuthRedirect ? null : (getUserFromStorage()?.id ?? null)
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Verificar y sincronizar con Supabase en background.
|
// Verificar y sincronizar con Supabase en background.
|
||||||
// Si la sesión expiró o fue revocada, onAuthStateChange llamará callback(null)
|
// Si la sesión expiró o fue revocada, onAuthStateChange llamará callback(null)
|
||||||
// y el router redirigirá a /login.
|
// y el router redirigirá a /login.
|
||||||
const unsubscribe = authService.onAuthStateChange((u) => {
|
const unsubscribe = authService.onAuthStateChange((u) => {
|
||||||
|
const prevId = lastSyncedUserIdRef.current
|
||||||
|
lastSyncedUserIdRef.current = u?.id ?? null
|
||||||
setUser(u)
|
setUser(u)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
// Si no hay usuario, no hay nada que sincronizar → marcar como cargado.
|
if (!u) {
|
||||||
// Si hay usuario, la sincronización con DB reseteará a false → true al completar.
|
// Logout → nada que sincronizar
|
||||||
if (!u) setIsProfileLoaded(true)
|
setIsProfileLoaded(true)
|
||||||
else setIsProfileLoaded(false)
|
} 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()
|
return () => unsubscribe()
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -68,12 +68,17 @@ export function WorkspacePage() {
|
|||||||
}
|
}
|
||||||
}, [user, processId, navigate])
|
}, [user, processId, navigate])
|
||||||
|
|
||||||
// Nombre de la organización dueña del proceso (para el back button contextual)
|
// Nombre de la organización dueña del proceso (para el back button contextual).
|
||||||
const [orgName, setOrgName] = useState<string | null>(null)
|
// undefined = todavía cargando, null = cargado sin resultado, string = nombre real.
|
||||||
|
const [orgName, setOrgName] = useState<string | null | undefined>(undefined)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProcess?.orgId) { setOrgName(null); return }
|
if (!currentProcess?.orgId) { setOrgName(null); return }
|
||||||
supabase.from('organizations').select('name').eq('id', currentProcess.orgId).single()
|
supabase
|
||||||
.then(({ data }) => { if (data) setOrgName(data.name as string) })
|
.from('organizations')
|
||||||
|
.select('name')
|
||||||
|
.eq('id', currentProcess.orgId)
|
||||||
|
.maybeSingle()
|
||||||
|
.then(({ data }) => setOrgName(data?.name ?? null))
|
||||||
}, [currentProcess?.orgId])
|
}, [currentProcess?.orgId])
|
||||||
|
|
||||||
// Validación XOR: todos los gateways exclusivos deben sumar 1.0
|
// 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 isClientRole = user?.platformRole === 'client_editor'
|
||||||
const backLabel = isClientRole
|
const backLabel = isClientRole
|
||||||
? 'Procesos'
|
? 'Procesos'
|
||||||
: currentProcess.orgId
|
: orgName ?? 'Biblioteca'
|
||||||
? (orgName ?? 'Volver')
|
|
||||||
: 'Biblioteca'
|
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
const orgId = currentProcess?.orgId
|
const orgId = currentProcess?.orgId
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ type PlatformRole = 'platform_admin' | 'member' | 'client_editor' | 'client_view
|
|||||||
function resolveBackButton(
|
function resolveBackButton(
|
||||||
platformRole: PlatformRole | undefined,
|
platformRole: PlatformRole | undefined,
|
||||||
orgId: string | null,
|
orgId: string | null,
|
||||||
orgName: string | null,
|
orgName: string | null | undefined, // undefined = cargando, null = sin resultado
|
||||||
): { backLabel: string; backTo: string } {
|
): { backLabel: string; backTo: string } {
|
||||||
const isClientRole = platformRole === 'client_editor' || platformRole === 'client_viewer'
|
const isClientRole = platformRole === 'client_editor' || platformRole === 'client_viewer'
|
||||||
const backLabel = isClientRole
|
const backLabel = isClientRole
|
||||||
? 'Procesos'
|
? 'Procesos'
|
||||||
: orgId
|
: orgName ?? 'Biblioteca'
|
||||||
? (orgName ?? 'Volver')
|
|
||||||
: 'Biblioteca'
|
|
||||||
const backTo = orgId && !isClientRole ? `/library?org=${orgId}` : '/library'
|
const backTo = orgId && !isClientRole ? `/library?org=${orgId}` : '/library'
|
||||||
return { backLabel, backTo }
|
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')
|
expect(backTo).toBe('/library?org=org-uuid-123')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('2. admin con proceso de cliente mientras orgName carga → muestra "Volver" como fallback', () => {
|
it('2. admin con proceso de cliente mientras orgName carga → muestra "Biblioteca" como fallback', () => {
|
||||||
const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', null)
|
const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', undefined)
|
||||||
expect(backLabel).toBe('Volver')
|
expect(backLabel).toBe('Biblioteca')
|
||||||
expect(backTo).toBe('/library?org=org-uuid-123')
|
expect(backTo).toBe('/library?org=org-uuid-123')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user