Módulos ProSe aplica aSuperScheduler Pro
Dependencias y vínculos entre eventos
Pasa los vínculos como un array de objetos { id, from, to, type }, donde from y to son ids de eventos; FinishToStart es el tipo por defecto y las líneas curvas son el dibujo por defecto. Da estilo a los vínculos con color, width, style y cssClass o con los tokens --super-scheduler-link, y gestiona los clics con onLinkClick. La librería dibuja los vínculos pero no los hace cumplir: comprueba las dependencias en onEventMoving y onEventMove. Crear vínculos arrastrando de un evento a otro está reservado y no está implementado en 0.1.0.
Los pasos de producción, los rodajes y las campañas dependen unos de otros: el corte empieza cuando termina el diseño, dos equipos empiezan a la vez. SuperScheduler dibuja esas dependencias como vínculos entre eventos y los mantiene enganchados mientras los usuarios se desplazan, hacen zoom y arrastran. Lo que una dependencia significa para la planificación (si un movimiento está permitido, si los sucesores se desplazan) se queda en tu código, donde viven tus reglas.
Los vínculos requieren SuperScheduler Pro.
Dibujar vínculos a partir de datos
Un vínculo es un objeto plano dentro de la prop links. from y to son ids de eventos (el id de EventData); id identifica el propio vínculo.
import type { SuperScheduler } from 'super-scheduler'
// `from` and `to` are event ids. Links between ids that are not loaded are not drawn.
export const LINKS: SuperScheduler.LinkData[] = [
// Cutting can start once design has finished (the default type).
{ id: 'design-cut', from: 'design', to: 'cutting', type: 'FinishToStart' },
// Painting and drying start together.
{ id: 'paint-dry', from: 'painting', to: 'drying', type: 'StartToStart', style: 'dashed' },
// Inspection and packing finish together; drawn below the events, in a fixed color.
{
id: 'qa-pack',
from: 'inspection',
to: 'packing',
type: 'FinishToFinish',
color: '#7c3aed',
layer: 'Below',
},
// A heavier line, styled further by CSS through its class.
{ id: 'ship', from: 'packing', to: 'shipping', width: 3, cssClass: 'link--critical' },
]
export const LINK_OPTIONS = {
linkShape: 'Curved', // default; 'RightAngled' draws segments
linkBottomMargin: 12, // clearance for links that run below events (8 px minimum for curves)
linkLayer: 'Above', // default layer of links without their own `layer`
links: LINKS,
} satisfies SuperScheduler.SchedulerConfigDeberías ver cuatro líneas: una curva continua del diseño al corte, una discontinua entre la pintura y el secado, una violeta por debajo de los eventos de inspección y embalaje, y una más gruesa que llega al envío.
Los vínculos salen de tu modelo de datos: una columna dependsOn, una tabla de dependencias o una regla como «mismo pedido, paso siguiente». Deriva el array con useMemo para que su identidad cambie solo cuando cambien los datos. Desde código, control.links ofrece list, add(link), update(link), remove(link or id), find(id), findByFromTo(from, to) y findAllByFromTo(from, to); con estado de React, es preferible actualizar tu array.
Tipos de vínculo
type | Se dibuja desde | Hasta | Significado habitual |
|---|---|---|---|
'FinishToStart' (por defecto) | el final de from | el inicio de to | to empieza después de que termine from |
'StartToStart' | el inicio de from | el inicio de to | los dos empiezan a la vez, o to después de que empiece from |
'FinishToFinish' | el final de from | el final de to | los dos terminan a la vez |
'StartToFinish' | el inicio de from | el final de to | to termina cuando empieza from |
El tipo cambia dónde se engancha la línea y cómo se dibuja. No cambia lo que los usuarios pueden hacer; consulta hacer cumplir las dependencias.
Forma y trazado
| Opción | Por defecto | Efecto |
|---|---|---|
linkShape | 'Curved' | Curvas suaves con un pequeño anillo en el origen y una punta de flecha. 'RightAngled' dibuja segmentos rectos. |
linkBottomMargin | 17 | Margen para los vínculos que pasan por debajo de los eventos (al menos 8 px en las curvas) |
linkLayer | 'Above' | Capa por defecto de los vínculos que no tienen su propio layer: por encima o por debajo de los eventos |
linkWidth | 'Auto' | Grosor de trazo por defecto en píxeles (las curvas tienen 2 px) |
Los vínculos entre extremos del mismo lado se arquean hacia fuera con un alcance limitado, de modo que los vínculos hacia filas lejanas no forman bucles desmesurados. Los vínculos hacia atrás usan el hueco entre filas y pasan por debajo de ambos eventos cuando esas filas se solapan. Los trazados esquivan los dos eventos vinculados, no todos los eventos que hay entre ellos: no es un enrutador que evite obstáculos.
Dar estilo a los vínculos
En cada vínculo, color, width, style ('solid', 'dashed', 'dotted'), layer y cssClass cambian ese vínculo; un width explícito prevalece sobre el token CSS de grosor. Para el resto, usa tokens en cualquier ancestro o en una clase de vínculo:
.planning {
--super-scheduler-link: #8b5cf6;
--super-scheduler-link-hover: #6d28d9;
--super-scheduler-link-width: 2;
}
.dark .planning {
--super-scheduler-link: #b89afa;
--super-scheduler-link-hover: #d8c6ff;
}
/* A class from your data or from onBeforeLinkRender */
.link--late {
--super-scheduler-link: var(--super-scheduler-danger);
}Los vínculos son elementos SVG con la clase super-scheduler__link y data-super-scheduler-part="link"; la flecha y el anillo de origen toman el color del vínculo.
Para dar estilo según el estado de los eventos vinculados, usa onBeforeLinkRender. Recibe una copia del vínculo en args.data, y los dos eventos en args.from y args.to. Cambia color, width, style, cssClass, layer o type en la copia; los extremos no se pueden cambiar. El hook se ejecuta una vez por cada vínculo dibujado y solo vuelve a ejecutarse cuando uno de sus eventos cambia de aspecto.
Cuándo se dibujan los vínculos
Un vínculo se dibuja cuando los dos eventos están cargados y colocados en la misma sección de la cuadrícula (las filas con scroll, o las mismas filas fijas superiores o inferiores). No se dibuja cuando:
- alguno de los dos ids no está cargado, por ejemplo porque queda fuera del rango que pediste;
- un extremo está oculto por un filtro o está en una fila plegada del árbol;
- el nivel de zoom dibuja un extremo como una barra fina de vista general;
- queda completamente fuera del área renderizada (vuelve a aparecer al desplazarse hasta él).
Mientras se arrastra un evento, sus vínculos siguen a la vista previa con el mismo trazado.
Clic, hover y menús
onLinkClickse ejecuta al hacer clic, conargs.link(unSuperScheduler.Link:data,id(),from(),to(),type()) yargs.originalEvent;args.preventDefault()omiteonLinkClicked, que se ejecuta después.linkBubblemuestra elbubbleHtmlde un vínculo al pasar el puntero por encima.contextMenuLink, o elcontextMenupropio de un vínculo, se abre con el clic derecho.
Los vínculos solo reciben eventos del puntero cuando alguna de estas opciones está configurada, así que por defecto nunca estorban al arrastrar eventos. Su área de clic invisible es más ancha que la línea visible.
Hacer cumplir las dependencias en tu aplicación
Los vínculos son dibujos. Nada impide que un usuario mueva el corte antes de que termine el diseño, salvo que tu código lo compruebe. Escribe la regla una sola vez como una función normal:
import { SuperScheduler } from 'super-scheduler'
export interface Bounds {
/** Ticks of the civil wall clock, as SuperScheduler.Date.getTime() returns them. */
readonly start: number
/** Exclusive end. */
readonly end: number
}
export const boundsOf = (event: SuperScheduler.EventData): Bounds => ({
start: new SuperScheduler.Date(event.start).getTime(),
end: new SuperScheduler.Date(event.end).getTime(),
})
/**
* Finish-to-start rule: a task may not start before each of its predecessors ends.
* Returns the first link that `proposed` times for `taskId` would break, or null.
* Other link types need rules of their own; links only draw what the data says.
*/
export function brokenFinishToStart(
taskId: SuperScheduler.EventId,
proposed: Bounds,
links: readonly SuperScheduler.LinkData[],
find: (id: SuperScheduler.EventId) => Bounds | null,
): SuperScheduler.LinkData | null {
for (const link of links) {
if ((link.type ?? 'FinishToStart') !== 'FinishToStart') continue
if (link.to === taskId) {
const predecessor = find(link.from)
if (predecessor !== null && proposed.start < predecessor.end) return link
} else if (link.from === taskId) {
const successor = find(link.to)
if (successor !== null && successor.start < proposed.end) return link
}
}
return null
}
/** Ids of the links the current data already breaks, to draw them differently. */
export function lateLinks(
links: readonly SuperScheduler.LinkData[],
events: readonly SuperScheduler.EventData[],
): ReadonlySet<SuperScheduler.LinkData> {
const byId = new Map(events.map((event) => [event.id, boundsOf(event)]))
const late = new Set<SuperScheduler.LinkData>()
for (const link of links) {
const from = byId.get(link.from)
const to = byId.get(link.to)
if ((link.type ?? 'FinishToStart') === 'FinishToStart' && from && to && to.start < from.end)
late.add(link)
}
return late
}Después, llámala desde los hooks que se ejecutan durante un cambio y antes de aplicarlo:
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { boundsOf, brokenFinishToStart, lateLinks } from './finish-to-start'
import type { Bounds } from './finish-to-start'
export function ProductionPlan(props: {
resources: SuperScheduler.ResourceData[]
initialTasks: SuperScheduler.EventData[]
/** Dependencies come from your data: the library draws them, it does not invent them. */
initialLinks: SuperScheduler.LinkData[]
}) {
const [tasks, setTasks] = useState(props.initialTasks)
const [links, setLinks] = useState(props.initialLinks)
const owned = useMemo(() => tasks.slice(), [tasks])
// Links the data already breaks get a class; the color comes from CSS.
const drawnLinks = useMemo(() => {
const late = lateLinks(links, tasks)
return links.map((link) => (late.has(link) ? { ...link, cssClass: 'link--late' } : link))
}, [links, tasks])
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setTasks([...args.events]),
[],
)
const config = useMemo<SchedulerProps>(() => {
// The other tasks as the control holds them right now.
const finder =
(control: SuperScheduler.SchedulerApi) =>
(id: SuperScheduler.EventId): Bounds | null => {
const event = control.events.find(id)
return event === null ? null : boundsOf(event.data)
}
return {
linkShape: 'Curved',
// While dragging: refuse and explain, frame by frame.
onEventMoving(args) {
const proposed = { start: args.start.getTime(), end: args.end.getTime() }
const broken = brokenFinishToStart(args.e.id(), proposed, links, finder(this))
if (broken !== null) {
args.allowed = false
args.message = 'This breaks a finish-to-start dependency'
}
},
onEventResizing(args) {
const proposed = { start: args.start.getTime(), end: args.end.getTime() }
if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
args.allowed = false
},
// Before the commit: also covers keyboard moves.
onEventMove(args) {
const proposed = { start: args.newStart.getTime(), end: args.newEnd.getTime() }
if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
args.preventDefault()
},
// Clicking a link offers to remove the dependency.
onLinkClick(args) {
const link = args.link.data
void confirmWithUser('Remove this dependency?').then((ok) => {
if (ok) setLinks((current) => current.filter((item) => item.id !== link.id))
})
},
}
}, [links])
return (
<SuperSchedulerComponent
{...config}
events={owned}
links={drawnLinks}
onEventsChange={onEventsChange}
resources={props.resources}
startDate="2026-10-05"
days={14}
scale="Day"
/>
)
}Deberías ver:
- al arrastrar un paso antes de que termine su predecesor, la tarjeta de arrastre dice «Not allowed · This breaks a finish-to-start dependency» y se rechaza la acción de soltar;
- los vínculos que tus datos ya incumplen, dibujados en el color de peligro;
- que al hacer clic en un vínculo se pregunta si quieres eliminar esa dependencia.
onEventMoving y onEventResizing se ejecutan en cada fotograma de un arrastre, así que la comprobación tiene que ser barata: busca solo los vínculos del evento arrastrado, como hace brokenFinishToStart. onEventMove se ejecuta una vez antes de confirmar el cambio y cubre también los movimientos con teclado y los eventos soltados desde fuera.
Reservado: creación interactiva de vínculos
Para que los usuarios creen dependencias hoy, añádelas desde tu propia interfaz (por ejemplo, con «Añadir dependencia» en el panel de detalle o en el menú contextual de un evento, eligiendo el otro evento de una lista o de la selección actual) y añade el vínculo a tu array.
Relacionado
Planificación de órdenes de fabricaciónEl mantenimiento se ha adelantado. Aparta la orden y mantén sus operaciones en secuencia. Planificación de producción audiovisualUn rodaje se alarga. Mueve la edición que dependía de él, entiende por qué y vuelve atrás. Planificación de atraquesUn buque llega doce horas tarde. Mueve su ventana de atraque y lleva con ella el remolcador y las grúas.
- Arrastrar, redimensionar y reglas de negocio para
onEventMoving,onEventMovey los mensajes de rechazo. - Deshacer, rehacer e historial para que la replanificación se pueda deshacer.
Ejemplos relacionados
- ForgePlanificación de órdenes de fabricaciónEl mantenimiento se ha adelantado. Aparta la orden y mantén sus operaciones en secuencia.
- FramehousePlanificación de producción audiovisualUn rodaje se alarga. Mueve la edición que dependía de él, entiende por qué y vuelve atrás.
- HarborworksPlanificación de atraquesUn buque llega doce horas tarde. Mueve su ventana de atraque y lleva con ella el remolcador y las grúas.
- Studio NorthPlanificación de recursos en una agencia creativaUna diseñadora tiene doble reserva. Pasa dos tareas a un compañero con un solo arrastre, revisa qué desbloquean y deshazlo.