This commit is contained in:
@@ -0,0 +1,191 @@
|
|||||||
|
# Sprint 5 — Etapa 4G: Revert ProfileSheet a raw fetch
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
InQ ROI — React 18 + Vite + TypeScript + Supabase v2.
|
||||||
|
|
||||||
|
El commit anterior cambió ProfileSheet.tsx para usar `supabase.from('users').update()` en lugar
|
||||||
|
de raw fetch. Eso introdujo una regresión: el save completa (204) pero la UI se congela
|
||||||
|
porque `supabase.from().update()` adquiere processLock internamente vía getSession(). Mientras
|
||||||
|
el lock está ocupado, otras queries Supabase de la página esperan → UI congelada.
|
||||||
|
|
||||||
|
La versión anterior (raw fetch con token de localStorage) fue confirmada funcionando: 204, sin
|
||||||
|
freeze. Esta etapa revierte a esa estrategia.
|
||||||
|
|
||||||
|
La función `getAccessToken()` ya no existe en el codebase — hay que recrearla inline en el archivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cambio único: `src/components/ProfileSheet.tsx`
|
||||||
|
|
||||||
|
Reemplazar el archivo completo con este contenido exacto:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Check } from 'lucide-react'
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@/lib/supabase'
|
||||||
|
import { useAuth } from '@/auth/AuthContext'
|
||||||
|
|
||||||
|
// Lee el access token directamente de localStorage sin pasar por el cliente Supabase.
|
||||||
|
// Evita contención con processLock — el save de perfil no necesita refresh de token
|
||||||
|
// porque es una operación rápida y el token está fresco en sesiones activas.
|
||||||
|
function getAccessToken(): string | null {
|
||||||
|
try {
|
||||||
|
const projectRef = new URL(SUPABASE_URL).hostname.split('.')[0]
|
||||||
|
const raw = localStorage.getItem(`sb-${projectRef}-auth-token`)
|
||||||
|
if (!raw) return null
|
||||||
|
const parsed = JSON.parse(raw) as { access_token?: string }
|
||||||
|
return parsed.access_token ?? null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileSheetProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
||||||
|
const { user, updateUser } = useAuth()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [company, setCompany] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && user) {
|
||||||
|
setName(user.name || user.email || '')
|
||||||
|
setCompany(user.company ?? '')
|
||||||
|
}
|
||||||
|
// Intencional: solo inicializar cuando el sheet se abre, no en cada cambio de `user`
|
||||||
|
// (evita resetear el formulario mientras el usuario está editando).
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!user || !name.trim()) return
|
||||||
|
|
||||||
|
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 fetch para que el usuario pueda seguir trabajando sin demora.
|
||||||
|
updateUser({ name: savedName, company: savedCompany })
|
||||||
|
onOpenChange(false)
|
||||||
|
|
||||||
|
// Persistir en Supabase en background — fire & forget.
|
||||||
|
// Usamos raw fetch (no el cliente Supabase) para evitar contención con processLock.
|
||||||
|
// getSession() interno de supabase.from().update() adquiere processLock y bloquea
|
||||||
|
// otras queries de la página mientras espera. Raw fetch lee el token de localStorage
|
||||||
|
// directamente — sin lock, sin overhead de auth.
|
||||||
|
const token = getAccessToken()
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
void fetch(
|
||||||
|
`${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
apikey: SUPABASE_ANON_KEY,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Prefer: 'return=minimal',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: savedName, company: savedCompany ?? null }),
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) console.error('[ProfileSheet] Error guardando perfil:', res.status)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[ProfileSheet] Error guardando perfil:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Editar perfil</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 mt-6">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Nombre</Label>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Tu nombre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Empresa</Label>
|
||||||
|
<Input
|
||||||
|
value={company}
|
||||||
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
|
placeholder="InQuality"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Email</Label>
|
||||||
|
<Input value={user?.email ?? ''} readOnly className="bg-muted text-muted-foreground cursor-not-allowed" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter className="mt-6 flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!name.trim()} className="gap-1.5">
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Orden de ejecución
|
||||||
|
|
||||||
|
1. Leer `src/components/ProfileSheet.tsx` para confirmar estado actual
|
||||||
|
2. Reemplazar con el contenido exacto de arriba
|
||||||
|
3. `npm run build` — debe dar 0 errores TypeScript
|
||||||
|
4. `npm run test` — todos los Vitest verdes
|
||||||
|
5. `git add src/components/ProfileSheet.tsx`
|
||||||
|
6. `git commit -m "fix(sprint-5/etapa-4g): revert ProfileSheet a raw fetch — evita processLock contention"`
|
||||||
|
7. `git push origin main`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lo que NO entra
|
||||||
|
|
||||||
|
- NO tocar ningún otro archivo
|
||||||
|
- NO agregar logs de debug
|
||||||
|
- NO cambios en supabase.ts, AuthContext, stores, ni ningún otro módulo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterios de validación
|
||||||
|
|
||||||
|
- [ ] `npm run build` limpio (0 errores TypeScript)
|
||||||
|
- [ ] `npm run test` — Vitest verdes
|
||||||
|
- [ ] `git push origin main` ejecutado
|
||||||
|
- [ ] **Validación visual del director antes del OK formal**
|
||||||
|
|
||||||
|
Validación funcional (post-deploy, a cargo del director):
|
||||||
|
- [ ] Abrir ProfileSheet → editar empresa → Guardar → Sheet cierra inmediatamente
|
||||||
|
- [ ] UI responde inmediatamente después de cerrar (todos los botones funcionan)
|
||||||
|
- [ ] Refrescar página → cambio persiste
|
||||||
Reference in New Issue
Block a user