Aller au contenu
SuperScheduler

Modules ProS’applique àSuperScheduler Pro

Dépendances et liens entre événements

Passez les liens sous forme de tableau d’objets { id, from, to, type }, où from et to sont des id d’événements ; FinishToStart est le type par défaut et les lignes courbes sont le tracé par défaut. Stylez les liens avec color, width, style et cssClass ou avec les tokens --super-scheduler-link, et traitez les clics avec onLinkClick. La bibliothèque dessine les liens mais ne les fait pas respecter : vérifiez les dépendances dans onEventMoving et onEventMove. La création de liens par glisser d’un événement à l’autre est réservée et n’est pas implémentée dans la 0.1.0.

Vérifié avec la v0.1.0 · relu le 7 octobre 2026.md

Étapes de production, tournages et campagnes dépendent les uns des autres : la découpe commence quand la conception se termine, deux équipes démarrent ensemble. SuperScheduler dessine ces dépendances sous forme de liens entre événements et les garde attachés pendant que les utilisateurs font défiler, zooment et déplacent. Ce qu’une dépendance signifie pour la planification (un déplacement est-il permis, les successeurs se décalent-ils) reste dans votre code, là où vivent vos règles.

Les liens nécessitent SuperScheduler Pro.

Un lien est un objet simple dans la prop links. from et to sont des id d’événements (l’id de EventData) ; id identifie le lien lui-même.

src/links.tsts
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.SchedulerConfig

Vous devriez voir quatre lignes : une courbe pleine de la conception à la découpe, une en pointillés entre peinture et séchage, une violette sous les événements d’inspection et d’emballage, et une plus épaisse vers l’expédition.

Les liens proviennent de votre modèle de données : une colonne dependsOn, une table de dépendances, ou une règle comme « même commande, étape suivante ». Dérivez le tableau avec useMemo pour que son identité ne change que lorsque les données changent. Depuis le code, control.links propose list, add(link), update(link), remove(link or id), find(id), findByFromTo(from, to) et findAllByFromTo(from, to) ; avec un état React, préférez mettre à jour votre tableau.

Types de liens

typeTracé depuisVersSens habituel
'FinishToStart' (défaut)la fin de fromle début de toto commence après la fin de from
'StartToStart'le début de fromle début de toles deux commencent ensemble, ou to après le début de from
'FinishToFinish'la fin de fromla fin de toles deux se terminent ensemble
'StartToFinish'le début de fromla fin de toto se termine quand from commence

Le type change l’endroit où la ligne s’attache et la façon dont elle est tracée. Il ne change pas ce que les utilisateurs ont le droit de faire ; voir faire respecter les dépendances.

Forme et tracé

OptionDéfautEffet
linkShape'Curved'Courbes douces avec un petit anneau à l’origine et une pointe de flèche. 'RightAngled' trace des segments droits.
linkBottomMargin17Dégagement pour les liens qui passent sous les événements (au moins 8 px pour les courbes)
linkLayer'Above'Couche par défaut des liens sans layer propre : au-dessus ou en dessous des événements
linkWidth'Auto'Épaisseur de trait par défaut en pixels (les courbes font 2 px)

Les liens entre deux extrémités du même côté s’arrondissent vers l’extérieur avec une amplitude limitée, si bien que les liens vers des lignes éloignées ne forment pas de boucles démesurées. Les liens vers l’arrière empruntent l’espace entre les lignes, et passent sous les deux événements quand ces lignes se chevauchent. Les tracés évitent les deux événements liés, pas tous les événements intermédiaires : ce n’est pas un routeur d’évitement d’obstacles.

Styler les liens

Lien par lien, color, width, style ('solid', 'dashed', 'dotted'), layer et cssClass modifient un seul lien ; un width explicite l’emporte sur le token CSS d’épaisseur. Pour le reste, utilisez des tokens sur n’importe quel ancêtre ou sur une classe de lien :

csscss
.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);
}

Les liens sont des éléments SVG portant la classe super-scheduler__link et data-super-scheduler-part="link" ; la flèche et l’anneau d’origine prennent la couleur du lien.

Pour styler en fonction de l’état des événements liés, utilisez onBeforeLinkRender. Il reçoit une copie du lien dans args.data, ainsi que les deux événements dans args.from et args.to. Modifiez color, width, style, cssClass, layer ou type sur la copie ; les extrémités ne peuvent pas changer. Le hook s’exécute une fois par lien tracé, puis à nouveau seulement quand l’apparence de l’un de ses événements change.

Quand les liens sont tracés

Un lien est tracé quand les deux événements sont chargés et disposés dans la même section de la grille (les lignes qui défilent, ou les mêmes lignes figées en haut ou en bas). Il n’est pas tracé quand :

  • l’un des id n’est pas chargé, par exemple hors de la plage que vous avez récupérée ;
  • une extrémité est masquée par un filtre ou se trouve dans une ligne d’arborescence repliée ;
  • le niveau de zoom dessine une extrémité comme une fine barre de vue d’ensemble ;
  • il se trouve entièrement hors de la zone rendue (il réapparaît quand il revient dans la vue).

Pendant qu’un événement est déplacé, ses liens suivent l’aperçu avec le même tracé.

Clic, survol et menus

  • onLinkClick s’exécute au clic avec args.link (un SuperScheduler.Link : data, id(), from(), to(), type()) et args.originalEvent ; args.preventDefault() empêche l’appel de onLinkClicked, qui s’exécute après.
  • linkBubble affiche le bubbleHtml d’un lien au survol.
  • contextMenuLink, ou le contextMenu propre à un lien, s’ouvre au clic droit.

Les liens ne réagissent au pointeur que si l’une de ces options est configurée ; par défaut, ils ne gênent donc jamais le déplacement des événements. Leur zone de clic invisible est plus large que la ligne visible.

Faire respecter les dépendances dans votre application

Les liens sont des dessins. Rien n’empêche un utilisateur de déplacer la découpe avant la fin de la conception, sauf si votre code le vérifie. Écrivez la règle une fois, sous forme de fonction simple :

src/finishToStart.tsts
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
}

Appelez-la ensuite depuis les hooks qui s’exécutent pendant et avant une modification :

src/ProductionPlan.tsxtsx
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"
    />
  )
}

Vous devriez voir :

  • quand vous déplacez une étape avant la fin de son prédécesseur, la carte de glissement indique « Not allowed · This breaks a finish-to-start dependency » et le dépôt est refusé ;
  • les liens que vos données enfreignent déjà, tracés dans la couleur de danger ;
  • un clic sur un lien qui demande s’il faut supprimer cette dépendance.

onEventMoving et onEventResizing s’exécutent à chaque image d’un glissement : gardez donc la vérification légère en ne consultant que les liens de l’événement déplacé, comme le fait brokenFinishToStart. onEventMove s’exécute une seule fois avant la validation et couvre aussi les déplacements au clavier et les événements déposés depuis l’extérieur.

Réservé : création interactive de liens

Pour permettre dès aujourd’hui aux utilisateurs de créer des dépendances, ajoutez-les depuis votre propre interface (par exemple « Ajouter une dépendance » dans le panneau de détail ou le menu contextuel d’un événement, en choisissant l’autre événement dans une liste ou dans la sélection courante) et ajoutez le lien à votre tableau.

Ordonnancement des ordres de fabricationLa maintenance a été avancée. Déplacez l’ordre et gardez ses opérations dans le bon ordre. Planification de production audiovisuelleUn tournage déborde. Déplacez le montage qui en dépendait, comprenez pourquoi, puis revenez en arrière. Planification des postes à quaiUn navire arrive avec douze heures de retard. Déplacez sa fenêtre d’accostage, puis entraînez son remorqueur et ses grues.