Antes de la última revisión de etapa 4. Sprint 1. Previo al ajuste de look n feel de app, reportes, nombre y colores.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import BpmnViewer from 'bpmn-js/lib/Viewer'
|
||||
|
||||
export type BpmnElementCategory = 'activity' | 'gateway' | 'other'
|
||||
@@ -19,37 +19,117 @@ function classifyElement(type: string): BpmnElementCategory {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
// SVG inline del ícono Bot de Lucide (12×12 px, color ámbar #f59e0b).
|
||||
// Copiado de lucide.dev/icons/bot — robot con antena, cuerpo rectangular y ojos.
|
||||
const BOT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 8V4H8"/>
|
||||
<rect width="16" height="12" x="4" y="8" rx="2"/>
|
||||
<path d="M2 14h2"/>
|
||||
<path d="M20 14h2"/>
|
||||
<path d="M15 13v2"/>
|
||||
<path d="M9 13v2"/>
|
||||
</svg>`
|
||||
|
||||
// HTML del badge de automatización.
|
||||
// Fondo #fffbeb (amber-50): identidad cromática con el ícono Bot ámbar,
|
||||
// contrasta con el fondo blanco de los nodos BPMN.
|
||||
function makeBadgeHtml(): string {
|
||||
return `<div title="Actividad marcada como automatizable" style="
|
||||
width:20px;height:20px;
|
||||
border-radius:50%;
|
||||
background:#fffbeb;
|
||||
border:1px solid #e2e8f0;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,0.12);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
cursor:default;
|
||||
pointer-events:none;
|
||||
">${BOT_SVG}</div>`
|
||||
}
|
||||
|
||||
// Identificador del tipo de overlay para poder remover solo los nuestros.
|
||||
const OVERLAY_TYPE = 'automation-indicator'
|
||||
|
||||
interface BpmnCanvasProps {
|
||||
xml: string
|
||||
onElementClick?: (elementId: string, elementName: string, category: BpmnElementCategory) => void
|
||||
selectedElementId?: string | null
|
||||
automatableElementIds?: string[] // IDs de actividades marcadas como automatizables
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BpmnCanvas({ xml, onElementClick, selectedElementId, className }: BpmnCanvasProps) {
|
||||
export function BpmnCanvas({
|
||||
xml,
|
||||
onElementClick,
|
||||
selectedElementId,
|
||||
automatableElementIds = [],
|
||||
className,
|
||||
}: BpmnCanvasProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const viewerRef = useRef<InstanceType<typeof BpmnViewer> | null>(null)
|
||||
const isImportedRef = useRef(false)
|
||||
|
||||
// ─── Inicializar viewer ────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
const viewer = new BpmnViewer({ container: containerRef.current })
|
||||
viewerRef.current = viewer
|
||||
return () => {
|
||||
isImportedRef.current = false
|
||||
viewer.destroy()
|
||||
viewerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// ─── Sincronizar overlays de automatización ───────────────────────────────
|
||||
const syncAutomationOverlays = useCallback(() => {
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer || !isImportedRef.current) return
|
||||
|
||||
const overlays = viewer.get('overlays') as any
|
||||
const reg = viewer.get('elementRegistry') as any
|
||||
|
||||
// Remover overlays anteriores del tipo propio antes de re-agregar
|
||||
overlays.remove({ type: OVERLAY_TYPE })
|
||||
|
||||
for (const elementId of automatableElementIds) {
|
||||
try {
|
||||
// Leer el ancho real del elemento para posicionar el badge en la
|
||||
// esquina superior derecha con overlap: left = width - 12 pone el
|
||||
// borde derecho del badge 8px fuera del borde derecho del nodo.
|
||||
// (right: -8 en la API tiene semántica distinta: left = -right + width
|
||||
// con -8 daría left = 8 + width, enteramente fuera del nodo.)
|
||||
const el = reg?.get(elementId)
|
||||
const width = el?.width ?? 100
|
||||
overlays.add(elementId, OVERLAY_TYPE, {
|
||||
position: { top: -8, left: width - 12 },
|
||||
html: makeBadgeHtml(),
|
||||
})
|
||||
} catch {
|
||||
// El elemento puede no existir en el diagrama actual — ignorar
|
||||
}
|
||||
}
|
||||
}, [automatableElementIds])
|
||||
|
||||
// ─── Importar XML ──────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer || !xml) return
|
||||
isImportedRef.current = false
|
||||
viewer.importXML(xml).then(({ warnings }) => {
|
||||
if (warnings.length) console.warn('bpmn-js warnings:', warnings)
|
||||
const canvas = viewer.get('canvas') as any
|
||||
canvas.zoom('fit-viewport', 'auto')
|
||||
isImportedRef.current = true
|
||||
syncAutomationOverlays()
|
||||
}).catch((err: Error) => console.error('Error cargando BPMN:', err))
|
||||
}, [xml])
|
||||
}, [xml, syncAutomationOverlays])
|
||||
|
||||
// ─── Re-sincronizar overlays cuando cambia la lista de automatizables ─────
|
||||
useEffect(() => {
|
||||
syncAutomationOverlays()
|
||||
}, [syncAutomationOverlays])
|
||||
|
||||
// ─── Click handler ─────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer || !onElementClick) return
|
||||
@@ -67,6 +147,7 @@ export function BpmnCanvas({ xml, onElementClick, selectedElementId, className }
|
||||
return () => eventBus.off('element.click', handler)
|
||||
}, [onElementClick])
|
||||
|
||||
// ─── Highlight del elemento seleccionado ──────────────────────────────────
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer) return
|
||||
|
||||
Reference in New Issue
Block a user