Aller au contenu
SuperScheduler

InteractionS’applique àSuperScheduler Pro

Arborescences de ressources, colonnes et sélection

Activez treeEnabled et imbriquez les ressources dans children ; un parent reste replié sauf s’il a expanded: true. Ajoutez des colonnes d’en-tête de ligne avec rowHeaderColumns, en remplissant les cellules à partir des champs de ressource via display. Filtrez avec control.rows.filter() et control.events.filter() plus onRowFilter et onEventFilter, et sélectionnez avec eventClickHandling: 'Select', la sélection rectangulaire, rowClickHandling: 'Select' et les API multiselect et multirange.

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

Les vrais plannings ont rarement une liste plate de ressources. Les chambres appartiennent à des étages, les techniciens à des régions, les machines à des lignes de production. Ce guide présente les fonctionnalités Pro qui organisent les lignes et permettent d’agir sur plusieurs éléments à la fois : arborescences de ressources, colonnes d’en-tête de ligne, filtres, et les trois types de sélection (événements, lignes et plages de temps).

Tout ce qui suit nécessite SuperScheduler Pro (super-scheduler). L’édition Lite affiche une liste plate et refuse les children et columns de ressource.

Grouper les ressources en arborescence

Une arborescence, ce sont de simples données : une ressource avec un tableau children devient une ligne parente. Deux réglages l’activent :

  • treeEnabled: true sur le planificateur. Sans lui, les children sont ignorés et l’en-tête de ligne n’a pas de bouton de dépliage.
  • expanded: true sur chaque parent qui doit être ouvert au départ. Les parents sont repliés sauf si expanded vaut exactement true.
src/TeamPlanner.tsxtsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
  {
    id: 'north',
    name: 'North region',
    expanded: true,
    children: [
      { id: 'ana', name: 'Ana Ruiz' },
      { id: 'ben', name: 'Ben Ortiz' },
    ],
  },
  {
    id: 'south',
    name: 'South region',
    children: [
      { id: 'cai', name: 'Cai Moreno' },
      { id: 'dee', name: 'Dee Patel' },
    ],
  },
]

const JOBS: SuperScheduler.EventData[] = [
  {
    id: 'job-1',
    resource: 'ana',
    start: '2026-10-05T00:00:00',
    end: '2026-10-07T00:00:00',
    text: 'Panel upgrade',
  },
  {
    id: 'job-2',
    resource: 'cai',
    start: '2026-10-06T00:00:00',
    end: '2026-10-09T00:00:00',
    text: 'Boiler service',
  },
]

export function TeamPlanner() {
  const { controlRef, control } = useSchedulerControl()
  // The control adopts the events array and mutates it: hand it a copy.
  const events = useMemo(() => JOBS.slice(), [])

  return (
    <section aria-labelledby="team-plan-title">
      <h2 id="team-plan-title">Field teams</h2>
      <div role="toolbar" aria-label="Rows">
        <button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
          Expand all
        </button>
        <button
          type="button"
          disabled={control === null}
          onClick={() => control?.rows.collapseAll()}
        >
          Collapse all
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        cellWidth={56}
        treeEnabled
        treeIndent={16}
        // Region rows group technicians: no jobs and no time selection on them.
        treePreventParentUsage
        resources={RESOURCES}
        events={events}
      />
    </section>
  )
}

Vous devriez voir « North region » ouvert avec ses deux techniciens, « South region » fermé, et les deux boutons fonctionnels une fois le planificateur monté (control vaut null jusque-là).

Options associées :

OptionDéfautEffet
treeEnabledfalseConstruit les lignes à partir de children.
treeIndent20Indentation par niveau, en pixels.
treePreventParentUsagefalseLes lignes parentes n’acceptent ni événements ni sélection de temps.
treeAutoExpandtrueUn parent replié s’ouvre quand un événement est glissé au-dessus de lui pendant une demi-seconde.
rowFilterParentsAlwaysVisibletrueConserve les ancêtres d’une ligne qui correspond à un filtre.

Les parents peuvent aussi porter des événements (une réservation pour « toute l’équipe »), sauf si vous activez treePreventParentUsage. Une ressource isolée refuse les événements déposés avec preventUsage: true, et les ressources de premier niveau peuvent être épinglées au-dessus ou en dessous des lignes défilantes avec frozen: 'top' ou frozen: 'bottom', ce qui convient aux lignes de synthèse.

Déplier et replier depuis le code

Le contrôle expose l’arborescence via control.rows :

  • control.rows.expandAll() et control.rows.collapseAll() ;
  • control.rows.expand(level) ouvre les parents situés au-dessus d’un niveau (1 par défaut, -1 pour tous les niveaux) ;
  • control.rows.find(id) renvoie une ligne ; row.expand(), row.collapse() et row.toggle() la modifient.

onResourceExpand et onResourceCollapse signalent un dépliage ou un repliage une fois qu’il a eu lieu, avec la ligne dans args.resource. Utilisez-les pour mémoriser ce que l’utilisateur a ouvert ou pour synchroniser d’autres parties de votre interface.

Ajouter des colonnes d’en-tête de ligne

Sans rowHeaderColumns, l’en-tête de ligne affiche le name de chaque ressource. Avec cette option, l’en-tête devient un petit tableau :

  • text est le titre de la colonne (name et title sont aussi acceptés) ;
  • display désigne un champ de la ressource dont la valeur remplit la cellule (un champ de même nom dans tags est lu en premier) ;
  • la première colonne affiche le name de la ressource quand elle n’a pas de display ;
  • width définit la largeur en pixels (par défaut rowHeaderColumnDefaultWidth, soit 80) ;
  • sort rend le titre cliquable : il trie les lignes selon ce champ, par ordre croissant puis décroissant, et définit aria-sort sur le titre.

Une ressource peut aussi porter ses propres columns: [{ text }, { html }]. Ces entrées sont associées par position, une par colonne.

src/TechnicianColumns.tsxtsx
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Status = 'available' | 'off'

const TECHNICIANS: SuperScheduler.ResourceData[] = [
  { id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
  { id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
  { id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]

// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
  { text: 'Technician', width: 150, sort: 'name' },
  { text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
  { text: 'Shift', display: 'shift', width: 70, nonresizable: true },
  { text: 'Status', display: 'status', width: 100 },
]

const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }

// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
  this: SuperScheduler.SchedulerApi,
  args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
  const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
  const cell = args.row.columns[index]
  const status = args.row.data.status
  if (cell === undefined || (status !== 'available' && status !== 'off')) return
  // With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
  cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}

export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
  // Kept in state so a parent re-render never hands the control the original order again.
  const [columns, setColumns] = useState(INITIAL_COLUMNS)

  return (
    <SuperSchedulerComponent
      startDate="2026-10-05"
      days={14}
      scale="Day"
      cellWidth={56}
      resources={TECHNICIANS}
      events={events}
      rowHeaderColumns={columns}
      rowHeaderColumnsReorderable
      onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
      onBeforeRowHeaderRender={decorateStatus}
    />
  )
}

Pour décorer une cellule, utilisez onBeforeRowHeaderRender. Avec des colonnes, le html propre à la ligne est ignoré : écrivez plutôt args.row.columns[i].html. Ce balisage est du HTML de confiance : échappez donc le texte saisi par les utilisateurs avec SuperScheduler.Util.escapeHtml. Pour du contenu React dans l’en-tête de ligne, voir Slots de rendu React.

Redimensionner et réordonner les colonnes

Les colonnes sont redimensionnables par défaut (rowHeaderColumnsResizable: true) ; nonresizable: true le désactive pour une colonne. Après un redimensionnement, le contrôle écrit la nouvelle largeur dans l’objet colonne correspondant de votre tableau rowHeaderColumns et appelle onRowHeaderColumnResized({ column }).

Le réordonnancement est désactivé par défaut. Avec rowHeaderColumnsReorderable, les utilisateurs font glisser les titres de colonnes ; les bords de redimensionnement restent prioritaires sur le glisser. En keyboardMode: 'Full', Alt+Maj+Flèche gauche et Alt+Maj+Flèche droite déplacent le titre qui a le focus et annoncent le changement. Depuis le code, control.moveRowHeaderColumn(from, to) déplace une colonne d’après son index dans l’ordre actuel.

Chaque redimensionnement et chaque réordonnancement appellent onRowHeaderColumnsChange avec :

  • columns : les colonnes dans leur nouvel ordre, avec leurs largeurs ;
  • order : order[i] est l’index d’origine (dans la prop que vous avez passée) de la colonne désormais en position i ;
  • widths et reason ('resize' ou 'reorder').

Gardez les colonnes dans le state React, comme le fait le snippet, et adoptez args.columns. Sinon, un parent qui refait son rendu avec un nouveau littéral de tableau redonnerait l’ordre d’origine au contrôle.

Filtrer lignes et événements

Un filtre, c’est un paramètre plus un callback. control.rows.filter(param) stocke le paramètre et appelle onRowFilter pour chaque ligne ; définissez args.visible = false pour la masquer. control.events.filter(param) et onEventFilter fonctionnent de la même façon pour les événements. Un paramètre falsy ('', null, 0) efface le filtre.

src/FilteredJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }

// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
  onRowFilter: (args) => {
    const query = String(args.filterParam).toLowerCase()
    args.visible = args.row.name.toLowerCase().includes(query)
  },
  onEventFilter: (args) => {
    const job = args.e.data as SuperScheduler.EventData<Job>
    args.visible = job.status === args.filterParam
  },
}

export function FilteredJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [query, setQuery] = useState('')
  const [status, setStatus] = useState<JobStatus | ''>('')
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // A falsy parameter ('' or null) clears the filter.
  useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
  useEffect(() => control?.events.filter(status || null), [control, status])

  return (
    <>
      <label>
        Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
      </label>
      <label>
        Status
        <select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
          <option value="">All</option>
          <option value="scheduled">Scheduled</option>
          <option value="done">Done</option>
          <option value="cancelled">Cancelled</option>
        </select>
      </label>
      <SuperSchedulerComponent
        {...FILTERS}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        treeEnabled
        // Keeps the region row of every matching technician (default true).
        rowFilterParentsAlwaysVisible
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Saisir « ben » laisse « North region » et « Ben Ortiz » : rowFilterParentsAlwaysVisible conserve les ancêtres de chaque ligne correspondante. Choisir un statut masque les autres événements sans toucher aux lignes.

Les filtres s’exécutent dans le navigateur, sur les données que détient le contrôle. Une recherche portant sur des données non chargées (un autre mois, un autre site) est une requête à votre backend, suivie de nouvelles resources ou de nouveaux events.

Sélectionner des événements

Par défaut, un clic sur un événement exécute seulement onEventClick. La sélection nécessite eventClickHandling: 'Select' :

  • un clic sélectionne ou désélectionne l’événement et désélectionne les autres ;
  • Ctrl+clic (Cmd+clic sur macOS) le sélectionne ou le désélectionne en conservant les autres, tant que allowMultiSelect vaut true (par défaut) ;
  • avec eventMultiSelectRange, Maj+clic sélectionne les événements situés entre le dernier événement cliqué et celui-ci, à travers les lignes visibles ; Ctrl/Cmd+Maj les ajoute.

onEventSelect s’exécute avant le changement et peut l’annuler avec args.preventDefault() ; onEventSelected s’exécute après.

L’API control.multiselect lit et modifie la sélection : get(), add(e), remove(e), clear(), isSelected(e) et selectAll({ scope, filter }). selectAll sélectionne par défaut les événements visibles ; scope: 'all' parcourt tous les événements chargés, pour un coût proportionnel à la taille du stockage.

src/SelectableJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'

type Job = { status: 'scheduled' | 'done' }

export function SelectableJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
  onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [count, setCount] = useState(0)
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // Rectangle selections and multiselect calls (selectAll, clear) publish the
  // 'selection' topic; clicks are reported by onEventSelected below.
  useEffect(() => {
    if (control === null) return
    return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
  }, [control])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
      eventClickHandling: 'Select',
      // Shift+click selects every event between the last clicked one and this one.
      eventMultiSelectRange: true,
      // Shift+drag over the grid selects the events inside the rectangle.
      rectangleSelectHandling: 'EventSelect',
      // With the rectangle: Shift adds to the selection, Alt removes from it.
      rectangleSelectModifiers: true,
      onEventSelected() {
        setCount(this.multiselect.get().length)
      },
      // Marks selected events for CSS. The hook runs again for every event whose
      // selection changes.
      onBeforeEventRender: (args) => {
        const e = args.control.events.find(args.data.id)
        if (e !== null && args.control.multiselect.isSelected(e)) {
          args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
        }
      },
    }),
    [],
  )

  const selectScheduled = () =>
    control?.multiselect.selectAll({
      scope: 'view',
      filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
    })

  const reschedule = () => {
    const ids = control?.multiselect.get().map((e) => e.id()) ?? []
    if (ids.length > 0) props.onReschedule(ids)
  }

  return (
    <>
      <div role="toolbar" aria-label="Selection">
        <button type="button" onClick={selectScheduled}>
          Select scheduled in view
        </button>
        <button type="button" onClick={() => control?.multiselect.clear()}>
          Clear
        </button>
        <button type="button" disabled={count === 0} onClick={reschedule}>
          Reschedule {count} {count === 1 ? 'job' : 'jobs'}
        </button>
      </div>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Le compteur du bouton devrait suivre les clics, les rectangles et les actions de la barre d’outils, et les interventions sélectionnées devraient être entourées d’un contour grâce à ce CSS :

csscss
.super-scheduler__event.is-selected {
  z-index: 2;
  box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}

Avec allowMultiMove, faire glisser un événement sélectionné déplace toute la sélection ; chaque événement passe par les mêmes règles de déplacement (voir glisser, redimensionner et règles métier). En mode clavier complet, Ctrl/Cmd+A sélectionne tous les événements visibles.

Sélection rectangulaire

rectangleSelectHandling détermine ce que fait un rectangle tracé au glisser :

  • 'EventSelect' sélectionne les événements qu’il contient (et exécute les callbacks) ;
  • 'Enabled' exécute seulement les callbacks, pour les applications qui agissent elles-mêmes sur le rectangle ;
  • 'Disabled' (par défaut) le désactive.

Les utilisateurs tracent un rectangle avec Maj+glisser sur la grille. Pour en démarrer un sans Maj, définissez args.action = 'RectangleSelect' dans onGridMouseDown, ou appelez control.multiselect.startRectangle() avant l’appui suivant. rectangleSelectMode: 'Row' maintient le rectangle dans la ligne où l’appui a commencé. Avec rectangleSelectModifiers, Maj ajoute à la sélection courante et Alt en retire ; sans l’une ni l’autre touche, le rectangle remplace la sélection.

Les callbacks sont onRectangleSelecting (à chaque frame ; args.visible = false masque le rectangle), onRectangleSelect (annulable, avec events, start, end et resources) et onRectangleSelected.

Sélectionner des lignes

rowClickHandling: 'Select' fait qu’un clic sur un en-tête de ligne sélectionne la ligne. Ctrl/Cmd sélectionne ou désélectionne une ligne, et Maj sélectionne une plage depuis la dernière ligne cliquée. Les lignes sélectionnées portent aria-selected="true".

src/RosterRows.tsxtsx
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

export function RosterRows(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
      rowClickHandling: 'Select',
      onRowSelected() {
        setSelected(this.rows.selection.get().map((row) => row.id))
      },
    }),
    [],
  )

  return (
    <>
      <button
        type="button"
        disabled={selected.length === 0}
        onClick={() => props.onNotify([...selected])}
      >
        Notify {selected.length} technicians
      </button>
      <button
        type="button"
        onClick={() => {
          control?.rows.selection.clear()
          setSelected([])
        }}
      >
        Clear rows
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

control.rows.selection propose get(), add(row), remove(row), isSelected(row) et clear() ; ces appels mettent les lignes en évidence immédiatement et ne déclenchent aucun callback, c’est pourquoi le snippet réinitialise son propre state après clear(). onRowSelect (annulable) et onRowSelected s’exécutent autour de chaque sélection faite par un clic.

La prop selectedRows contient les ids des ressources sélectionnées. Passez-la pour démarrer avec une sélection ; le contrôle met à jour ce même tableau quand la sélection change.

Sélectionner des plages de temps

Faire glisser sur des cellules vides sélectionne une plage de temps et appelle onTimeRangeSelected une seule fois, au relâchement, avec start, end (exclusif), resource et origin: 'drag'. Autres origines :

  • 'click' : un simple clic sur une cellule vide sélectionne cette cellule ;
  • 'keyboard' : avec le clavier activé, Entrée sur une cellule qui a le focus, ou Maj+Flèche gauche/droite pour étendre une plage ;
  • 'api' : control.selectTimeRange(start, end, resource).

Vérifiez args.origin quand seul un glisser doit créer quelque chose. La sélection reste dessinée après le relâchement, jusqu’à la sélection suivante ou jusqu’à control.clearSelection(). Un clic ailleurs l’efface aussi, sauf avec timeRangeSelectedHandling: 'HoldForever' ; 'Disabled' désactive la sélection de plages.

onTimeRangeSelecting s’exécute à chaque changement pendant le glisser. Définissez args.allowed = false pour refuser une plage (l’ombre est marquée et le relâchement ne sélectionne rien), ou réécrivez args.start et args.end pour la borner. Les cellules désactivées et les parents soumis à treePreventParentUsage refusent les sélections sans aucun code.

Plusieurs plages à la fois

Avec allowMultiRange, un nouveau glisser peut ajouter une plage au lieu de remplacer la plage courante : tant que Ctrl/Cmd est maintenu (multiRangeMode: 'CtrlOrMeta', par défaut) ou toujours ('Always'). args.multirange liste toutes les plages sélectionnées, et control.multirange les lit ou les modifie : get(), clear() et add(new SuperScheduler.Selection(start, end, resource)).

src/RangeBlocker.tsxtsx
import { useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'

export interface BlockRequest {
  readonly resource: SuperScheduler.ResourceId
  /** ISO wall-clock values; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

export function RangeBlocker(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onBlock: (requests: BlockRequest[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // Ctrl/Cmd + drag adds a range instead of replacing the current one.
      allowMultiRange: true,
      multiRangeMode: 'CtrlOrMeta',
      onTimeRangeSelecting: (args) => {
        // Past days cannot be blocked: the shadow turns red and the drop is refused.
        args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
      },
      onTimeRangeSelected: (args) => {
        // A plain click on an empty cell also selects it (origin 'click').
        setRanges([...args.multirange])
      },
    }),
    [],
  )

  const block = () => {
    props.onBlock(
      ranges.map((range) => ({
        resource: range.resource,
        start: range.start.value,
        end: range.end.value,
      })),
    )
    control?.clearSelection()
    setRanges([])
  }

  return (
    <>
      <button type="button" disabled={ranges.length === 0} onClick={block}>
        Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Faites glisser sur des jours libres d’une chambre, puis d’une autre en maintenant Ctrl (Cmd sur macOS) : le bouton indique « Block 2 selected ranges ». Les plages parviennent à onBlock sous forme de valeurs ISO en heure locale avec fins exclusives, prêtes à être envoyées à votre backend.

Ce que possède votre application

La bibliothèque conserve arborescences, filtres et sélections dans le navigateur, pour la session en cours. Votre application décide :

  • d’où vient l’arborescence : les lignes sont les resources que vous passez, et le contrôle ne demande jamais d’enfants de lui-même (onLoadNode est réservé) ;
  • ce que signifie un filtre quand les données ne sont pas chargées, et comment la recherche atteint le backend ;
  • ce que fait une sélection : actions groupées, permissions pour les exécuter et persistance ;
  • si les lignes dépliées et la disposition des colonnes survivent à un rechargement (voir vues enregistrées).

Les sélections ne sont enregistrées nulle part. L’état déplié vit dans les objets ressources que vous avez passés, jusqu’à ce que vous les remplaciez.

Planning des chambres d’hôtelLa douche de la 104 fuit. Relogez le prochain client, bloquez la chambre pour le plombier et repérez les nuits déjà complètes. Planification de salles de formationLes inscriptions dépassent la salle. Sélectionnez les deux sessions, voyez ce qui est libre pour les deux, déplacez-les ensemble et gardez la vue. Répartition des interventions techniquesUne intervention urgente arrive. Trouvez l’équipe qui peut la prendre avant l’échéance.