fix(sprint-5): optimistic update en ProfileSheet — cierra Sheet antes del fetch, UI nunca bloqueada
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
This commit is contained in:
@@ -32,33 +32,37 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
|||||||
const { user, updateUser } = useAuth()
|
const { user, updateUser } = useAuth()
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [company, setCompany] = useState('')
|
const [company, setCompany] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && user) {
|
if (open && user) {
|
||||||
setName(user.name || user.email || '')
|
setName(user.name || user.email || '')
|
||||||
setCompany(user.company ?? '')
|
setCompany(user.company ?? '')
|
||||||
setError(null)
|
|
||||||
}
|
}
|
||||||
// Intencional: solo inicializar cuando el sheet se abre, no en cada cambio de `user`
|
// Intencional: solo inicializar cuando el sheet se abre, no en cada cambio de `user`
|
||||||
// (evita resetear el formulario mientras el usuario está editando).
|
// (evita resetear el formulario mientras el usuario está editando).
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
async function handleSave() {
|
function handleSave() {
|
||||||
if (!user || !name.trim()) return
|
if (!user || !name.trim()) return
|
||||||
setSaving(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
// Usamos fetch nativo en lugar de supabase.from().update() para evitar que esta
|
|
||||||
// operación quede en la cola del processLock compartido. En el login, upsertUserOnLogin
|
|
||||||
// y syncProfileFromDB adquieren ese lock; si el servidor tarda, la cola queda ocupada
|
|
||||||
// y el save cuelga indefinidamente. El fetch nativo es independiente del lock.
|
|
||||||
const token = getAccessToken()
|
|
||||||
if (!token) throw new Error('Sesión no disponible — recargá la página e intentá de nuevo')
|
|
||||||
|
|
||||||
const res = await fetch(
|
const savedName = name.trim()
|
||||||
|
const savedCompany = company.trim() || undefined
|
||||||
|
|
||||||
|
// Actualización optimista: aplicar cambios localmente y cerrar el Sheet de inmediato.
|
||||||
|
// El Sheet nunca queda abierto esperando la respuesta del servidor.
|
||||||
|
// La modal de Radix bloquea todo el UI mientras está abierta, así que cerramos
|
||||||
|
// ANTES del await para que el usuario pueda seguir trabajando sin demora.
|
||||||
|
updateUser({ name: savedName, company: savedCompany })
|
||||||
|
onOpenChange(false)
|
||||||
|
|
||||||
|
// Persistir en Supabase en background — fire & forget.
|
||||||
|
// Si falla (red lenta, token vencido, cold start de Supabase), el estado local
|
||||||
|
// ya tiene el nuevo nombre. En el próximo login, syncProfileFromDB recupera el DB.
|
||||||
|
const token = getAccessToken()
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
void fetch(
|
||||||
`${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`,
|
`${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -68,30 +72,16 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Prefer: 'return=minimal',
|
Prefer: 'return=minimal',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ name: savedName, company: savedCompany ?? null }),
|
||||||
name: name.trim(),
|
signal: AbortSignal.timeout(10000),
|
||||||
company: company.trim() || null,
|
|
||||||
}),
|
|
||||||
// Timeout de 8s: si Supabase no responde (cold start, red lenta, etc.),
|
|
||||||
// el catch muestra un mensaje de error y libera el botón.
|
|
||||||
// Sin esto, la modal queda abierta indefinidamente bloqueando todo el UI.
|
|
||||||
signal: AbortSignal.timeout(8000),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.then((res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) console.error('[ProfileSheet] Error guardando perfil:', res.status)
|
||||||
const detail = await res.text().catch(() => '')
|
})
|
||||||
throw new Error(`Error ${res.status}${detail ? `: ${detail}` : ''}`)
|
.catch((err) => {
|
||||||
}
|
console.error('[ProfileSheet] Error guardando perfil:', err)
|
||||||
|
})
|
||||||
updateUser({ name: name.trim(), company: company.trim() || undefined })
|
|
||||||
onOpenChange(false)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error al guardar perfil:', err)
|
|
||||||
setError(err instanceof Error ? err.message : 'Error al guardar perfil')
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,17 +115,13 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
|||||||
<Label className="text-xs">Email</Label>
|
<Label className="text-xs">Email</Label>
|
||||||
<Input value={user?.email ?? ''} readOnly className="bg-muted text-muted-foreground cursor-not-allowed" />
|
<Input value={user?.email ?? ''} readOnly className="bg-muted text-muted-foreground cursor-not-allowed" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<p className="text-xs text-destructive">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SheetFooter className="mt-6 flex gap-2">
|
<SheetFooter className="mt-6 flex gap-2">
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} disabled={!name.trim() || saving} className="gap-1.5">
|
<Button onClick={handleSave} disabled={!name.trim()} className="gap-1.5">
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
Guardar
|
Guardar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user