193 lines
7.8 KiB
Markdown
193 lines
7.8 KiB
Markdown
# Sprint 5 — Etapa 1: Fix crítico workspace loading
|
|
|
|
## Contexto
|
|
|
|
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind + Supabase.
|
|
Sprint 4 completado. La app tiene auth (Google OAuth + Supabase), PostgreSQL, RLS, catálogo de recursos global.
|
|
|
|
**Bug en producción:** al navegar desde la biblioteca (`/`) a un proceso (`/workspace/:processId`), la pantalla del workspace queda cargando indefinidamente. Solo carga correctamente después de un refresh manual de la página.
|
|
|
|
---
|
|
|
|
## Causa raíz — ya diagnosticada, no requiere investigación adicional
|
|
|
|
El diagnóstico es preciso. No es necesario "investigar el flujo" — el problema está localizado.
|
|
|
|
**`src/store/process-store.ts`** llama a `supabase.auth.getSession()` directamente como barrera de sesión antes del fetch. Esta llamada no tiene timeout: si Supabase tarda en responder (token refresh en curso, red lenta, lock interno), `getSession()` se bloquea indefinidamente → `loadProcess` nunca resuelve → `isLoading` queda `true` para siempre.
|
|
|
|
**`src/store/library-store.ts`** hace lo mismo pero usa `getSessionSafe()` — una función en `src/lib/supabase.ts` que hace race entre `getSession()` y un timeout de 4 segundos, garantizando que siempre resuelve. Por eso la biblioteca carga y el workspace no.
|
|
|
|
**La inconsistencia es sistémica:** el mismo patrón puede existir en otros stores. Esta etapa lo audita y corrige en todos.
|
|
|
|
---
|
|
|
|
## Parte 1 — Fix en process-store.ts
|
|
|
|
En `src/store/process-store.ts`, localizar la llamada a `supabase.auth.getSession()` (usada como barrera antes del query principal) y reemplazarla por `getSessionSafe()`:
|
|
|
|
```typescript
|
|
// Antes:
|
|
await supabase.auth.getSession()
|
|
|
|
// Después:
|
|
await getSessionSafe()
|
|
```
|
|
|
|
Agregar el import si no está:
|
|
```typescript
|
|
import { supabase, getSessionSafe } from '@/lib/supabase'
|
|
```
|
|
|
|
Verificar que `getSessionSafe` ya existe en `src/lib/supabase.ts`. Si por alguna razón no existe, crearla siguiendo el mismo patrón que se usa en `library-store.ts`.
|
|
|
|
---
|
|
|
|
## Parte 2 — Auditar todos los stores
|
|
|
|
Buscar en `src/store/` y `src/persistence/` cualquier archivo que llame a `supabase.auth.getSession()` directamente (sin pasar por `getSessionSafe`). Reportar qué archivos tienen este patrón.
|
|
|
|
Para **cada archivo** que lo use:
|
|
- Si es una barrera de sesión antes de un query → reemplazar por `getSessionSafe()`
|
|
- Si es otro uso (ej: verificar si hay sesión activa para tomar una decisión, no como barrera) → evaluar caso por caso y documentar la decisión
|
|
|
|
---
|
|
|
|
## Parte 3 — Estado de error en el workspace
|
|
|
|
**Problema adicional:** actualmente, si `getSessionSafe()` agota su timeout, `loadProcess` lanza un error, pero el workspace no tiene un estado `error` propio. El resultado es que `isLoading` puede quedar `true` o la pantalla queda vacía sin mensaje al usuario.
|
|
|
|
**Fix requerido:** en `process-store.ts` (y el hook/componente que lo consume), agregar manejo de estado de error:
|
|
|
|
```typescript
|
|
// En el store, el estado debe incluir:
|
|
{
|
|
isLoading: boolean
|
|
error: string | null // 🆕
|
|
process: Process | null
|
|
}
|
|
|
|
// En loadProcess, al catch:
|
|
set({ isLoading: false, error: 'No se pudo cargar el proceso. Revisá tu conexión.' })
|
|
```
|
|
|
|
**En WorkspacePage.tsx** (o el componente raíz del workspace), manejar el estado `error`:
|
|
|
|
```tsx
|
|
if (error) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
<Button variant="outline" onClick={() => loadProcess(processId)}>
|
|
Reintentar
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
No usar `alert()` ni logs crudos. El error debe ser visible, accionable, y en español.
|
|
|
|
---
|
|
|
|
## Parte 4 — Skeleton screen en el workspace
|
|
|
|
**Estado actual:** el workspace muestra un spinner genérico mientras carga. En una app moderna esto es insuficiente — el usuario no sabe qué está cargando ni cuánto ocupa la pantalla.
|
|
|
|
**Requerimiento:** reemplazar el spinner de carga del workspace por un skeleton screen que preserve el layout de la página.
|
|
|
|
El workspace tiene una estructura aproximada de: barra superior + panel BPMN (zona grande central/izquierda) + panel lateral (ActivityPanel/ResourcesPanel).
|
|
|
|
Crear un componente `WorkspaceSkeleton` que muestre:
|
|
- Header con altura igual al real (sin contenido)
|
|
- Zona del canvas BPMN: rectángulo con `animate-pulse` en gris claro, misma altura aproximada
|
|
- Panel lateral: 3-4 filas de skeleton (títulos y texto en gris)
|
|
|
|
Usar `shadcn/ui Skeleton` si ya está disponible en el proyecto, o implementar con Tailwind `animate-pulse`:
|
|
|
|
```tsx
|
|
<div className="animate-pulse bg-muted rounded-md h-full w-full" />
|
|
```
|
|
|
|
El skeleton se muestra cuando `isLoading === true` y no hay `process` todavía. Una vez que `process` está disponible, el skeleton desaparece y el workspace real se monta.
|
|
|
|
---
|
|
|
|
## Parte 5 — React Error Boundary en el workspace
|
|
|
|
Una app de nueva generación no deja que un error de renderizado en el workspace blankee la pantalla completa. Agregar un Error Boundary al nivel del workspace.
|
|
|
|
Si ya existe un `ErrorBoundary` en el proyecto (`src/components/ErrorBoundary.tsx` o similar), usarlo. Si no existe, crear uno mínimo:
|
|
|
|
```tsx
|
|
// src/components/ErrorBoundary.tsx
|
|
import { Component, ReactNode } from 'react'
|
|
|
|
interface Props { children: ReactNode; fallback?: ReactNode }
|
|
interface State { hasError: boolean; error: Error | null }
|
|
|
|
export class ErrorBoundary extends Component<Props, State> {
|
|
state: State = { hasError: false, error: null }
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
return { hasError: true, error }
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return this.props.fallback ?? (
|
|
<div className="flex flex-col items-center justify-center h-full gap-2 p-8">
|
|
<p className="text-sm font-medium text-destructive">Algo salió mal al cargar el workspace.</p>
|
|
<p className="text-xs text-muted-foreground">{this.state.error?.message}</p>
|
|
<button
|
|
className="text-xs underline text-muted-foreground"
|
|
onClick={() => window.location.reload()}
|
|
>
|
|
Recargar la página
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
return this.props.children
|
|
}
|
|
}
|
|
```
|
|
|
|
Envolver `WorkspacePage` (o el contenido del workspace) con `<ErrorBoundary>` en el router o en `App.tsx`.
|
|
|
|
---
|
|
|
|
## Qué NO entra en esta etapa
|
|
|
|
- Cambios en el diseño visual del workspace (paneles, BPMN canvas, ActivityPanel)
|
|
- Cambios en la lógica de simulación
|
|
- Cambios en recursos, reporte, o PDF
|
|
- Nuevas features de producto
|
|
- Cambios en auth (login, logout, OAuth)
|
|
|
|
---
|
|
|
|
## Orden de ejecución
|
|
|
|
1. Abrir `src/store/process-store.ts` — reemplazar `getSession()` por `getSessionSafe()`
|
|
2. Grep en `src/store/` y `src/persistence/` para `getSession()` — auditar y corregir
|
|
3. Agregar campo `error` al store y manejo en `WorkspacePage`
|
|
4. Crear `WorkspaceSkeleton` y usarlo mientras `isLoading === true`
|
|
5. Crear `ErrorBoundary` (si no existe) y envolver el workspace
|
|
6. `npm run build` — 0 errores TypeScript
|
|
7. `npm run lint` — 0 warnings en archivos tocados
|
|
8. `npm run test` — mantener conteo anterior de tests Vitest (sin regresiones)
|
|
9. Commit: `fix(sprint-5/etapa-1): workspace loading — getSessionSafe + error state + skeleton`
|
|
|
|
---
|
|
|
|
## Criterios de validación
|
|
|
|
- [ ] Navegar desde la biblioteca a cualquier proceso carga el workspace en el primer intento (sin refresh)
|
|
- [ ] Si el proceso no carga en ~5s, se muestra mensaje de error con botón "Reintentar"
|
|
- [ ] El estado de carga muestra un skeleton screen que preserva el layout (no spinner en blanco)
|
|
- [ ] Si hay un error de renderizado en el workspace, la app muestra el Error Boundary (no pantalla en blanco)
|
|
- [ ] Todos los stores auditados — ninguno usa `getSession()` como barrera de sesión sin timeout
|
|
- [ ] `npm run build` limpio
|
|
- [ ] `npm run test` — sin regresiones
|
|
- [ ] **Validación visual del director** antes del OK formal
|