Sprint 5. Etapa 1.
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

This commit is contained in:
2026-06-19 15:31:03 -03:00
parent 84daf82e65
commit a2763ab755
13 changed files with 612 additions and 80 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import { getSessionSafe } from '@/lib/supabase'
import { supabaseActivityRepo } from '@/persistence/supabase/activity-repo'
import { supabaseResourceRepo } from '@/persistence/supabase/resource-repo'
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
@@ -30,8 +30,8 @@ export function useReportData(processId: string): ReportData {
setIsLoading(true)
setError(null)
try {
// Barrera única: garantiza sesión restaurada antes del fan-out en paralelo
await supabase.auth.getSession()
// Barrera única con timeout: garantiza sesión restaurada antes del fan-out en paralelo
await getSessionSafe()
const [proc, sim, acts, res] = await Promise.all([
supabaseProcessRepo.getById(processId),
supabaseSimulationRepo.getLatestByProcess(processId),

View File

@@ -9,18 +9,19 @@ import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
import { Separator } from '@/components/ui/separator'
import { useToast } from '@/components/ui/use-toast'
import { BpmnCanvas, type BpmnElementCategory } from './BpmnCanvas'
import { ActivityPanel } from './ActivityPanel'
import { GatewayPanel, isXorSumValid } from './GatewayPanel'
import { ResourcesPanel } from './ResourcesPanel'
import { GlobalSettingsPanel } from './GlobalSettingsPanel'
import { WorkspaceSkeleton } from './WorkspaceSkeleton'
import { useProcessStore } from '@/store/process-store'
import { useSimulationStore } from '@/store/simulation-store'
import { useSimulate } from '../simulation/useSimulate'
import { useLibraryStore } from '@/store/library-store'
import { useAuth } from '@/auth/AuthContext'
import { AppHeader } from '@/components/AppHeader'
import { ErrorBoundary } from '@/components/ErrorBoundary'
type SelectedCategory = 'activity' | 'gateway' | null
@@ -30,7 +31,6 @@ export function WorkspacePage() {
const { currentProcess, activities, isLoading, error, loadProcess, gateways, updateProcess } = useProcessStore()
const { isRunning, resultActual, error: simError, selectActivity, loadLatestForProcess, lastPersistedExecutedAt } = useSimulationStore()
const { simulate } = useSimulate()
const { toast } = useToast()
const { user } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(true)
@@ -50,31 +50,16 @@ export function WorkspacePage() {
const [showTagSuggestions, setShowTagSuggestions] = useState(false)
const getAllTags = useLibraryStore((state) => state.getAllTags)
// Usamos un ref para saber si loadProcess ya terminó al menos una vez.
// Esto evita que el redirect se dispare en el render inicial donde
// isLoading=false y currentProcess=null (antes del primer efecto).
const hasLoadedOnce = useRef(false)
useEffect(() => { loadProcess(processId) }, [processId, loadProcess])
// Cargar el último executedAt persistido para comparar contra updatedAt (indicador stale)
// Cargar el último executedAt persistido para comparar contra updatedAt (indicador stale).
// Depende de currentProcess?.id (no del objeto) a propósito: evita re-disparar el efecto
// en cada edición de campos del proceso, solo cuando cambia de proceso.
useEffect(() => {
if (currentProcess) loadLatestForProcess(currentProcess.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProcess?.id, loadLatestForProcess])
useEffect(() => {
if (isLoading) { hasLoadedOnce.current = true; return }
if (!hasLoadedOnce.current) return // carga no iniciada aún
if ((error || !currentProcess) && processId) {
toast({
title: 'Proceso no encontrado',
description: 'El proceso que buscás no existe o fue eliminado.',
variant: 'destructive',
})
navigate({ to: '/' })
}
}, [isLoading, error, currentProcess, processId, toast, navigate])
// Validación XOR: todos los gateways exclusivos deben sumar 1.0
const xorValidation = useMemo(() => {
const invalidIds = gateways
@@ -166,16 +151,27 @@ export function WorkspacePage() {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)
if (isLoading && !currentProcess) {
return <WorkspaceSkeleton />
}
if (error || !currentProcess) {
return null // el useEffect ya inició el redirect con toast
return (
<div className="h-screen flex flex-col items-center justify-center gap-4">
<AlertCircle className="h-8 w-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground max-w-sm text-center">
{error ?? 'No se pudo cargar el proceso. Revisá tu conexión.'}
</p>
<div className="flex gap-2">
<Button variant="outline" onClick={() => loadProcess(processId)}>
Reintentar
</Button>
<Button variant="ghost" onClick={() => navigate({ to: '/' })}>
Volver a la biblioteca
</Button>
</div>
</div>
)
}
// Label del tab "elemento" según lo seleccionado
@@ -483,3 +479,11 @@ export function WorkspacePage() {
</TooltipProvider>
)
}
export function WorkspacePageWithBoundary() {
return (
<ErrorBoundary>
<WorkspacePage />
</ErrorBoundary>
)
}

View File

@@ -0,0 +1,45 @@
import { AppHeader } from '@/components/AppHeader'
// Preserva el layout real del workspace (topbar + canvas + panel lateral) mientras
// loadProcess() está en curso, en vez de un spinner genérico sobre pantalla en blanco.
export function WorkspaceSkeleton() {
return (
<div className="h-screen flex flex-col bg-slate-100 overflow-hidden">
<AppHeader />
{/* Topbar */}
<header className="min-h-12 bg-white border-b border-slate-200 flex items-center px-3 gap-3 shrink-0">
<div className="h-8 w-8 rounded-md bg-muted animate-pulse" />
<div className="h-5 w-px bg-slate-200" />
<div className="flex-1 min-w-0 space-y-1.5">
<div className="h-3.5 w-48 rounded bg-muted animate-pulse" />
<div className="h-2.5 w-28 rounded bg-muted animate-pulse" />
</div>
<div className="h-8 w-24 rounded-md bg-muted animate-pulse" />
</header>
{/* Main */}
<div className="flex-1 flex overflow-hidden">
{/* Canvas BPMN */}
<div className="flex-1 p-4">
<div className="h-full w-full rounded-lg bg-muted animate-pulse" />
</div>
{/* Panel lateral */}
<div className="shrink-0 w-72 bg-white border-l border-slate-200 p-4 space-y-4">
<div className="flex gap-2">
<div className="h-7 w-16 rounded-md bg-muted animate-pulse" />
<div className="h-7 w-16 rounded-md bg-muted animate-pulse" />
<div className="h-7 w-16 rounded-md bg-muted animate-pulse" />
</div>
{[1, 2, 3, 4].map((i) => (
<div key={i} className="space-y-1.5">
<div className="h-3 w-20 rounded bg-muted animate-pulse" />
<div className="h-2.5 w-full rounded bg-muted animate-pulse" />
</div>
))}
</div>
</div>
</div>
)
}