Modules ProS’applique àSuperScheduler Pro
Volets coordonnés et vues enregistrées
Remplacez SuperSchedulerComponent par SchedulerPanes, importé de super-scheduler/panes, et décrivez chaque volet avec un id plus resources ou un rowFilter ; les volets partagent le défilement horizontal, le zoom et la largeur de l’en-tête de ligne, défilent verticalement chacun de leur côté, et les événements peuvent être glissés de l’un à l’autre. Pour les vues enregistrées, getViewState(control) renvoie un objet sérialisable en JSON avec le zoom, la position de défilement, la densité, les lignes repliées et les colonnes, et applyViewState le restaure ; l’endroit où il est stocké relève de votre application.
Deux besoins reviennent dans tout grand écran de planification. Le premier : garder une partie des lignes visible pendant que le reste défile, comme une zone « à affecter » sous les chambres ou une équipe au-dessus de ses machines. Le second : retrouver plus tard la même vue, c’est-à-dire le zoom, la date et les lignes que l’utilisateur regardait. super-scheduler/panes et super-scheduler/views y répondent, et tous deux nécessitent SuperScheduler Pro.
Découper une frise en volets
SchedulerPanes affiche plusieurs planificateurs empilés sur une même frise. Ils partagent la position de défilement horizontal, le zoom et la largeur de l’en-tête de ligne ; chaque volet défile verticalement de son côté et a sa propre hauteur. Des séparateurs entre les volets permettent de les redimensionner.
Il remplace SuperSchedulerComponent : vous passez une seule fois les mêmes props de planificateur, plus un tableau panes et une hauteur totale height.
import { useCallback, useMemo, useRef, useState } from 'react'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SchedulerPanes } from 'super-scheduler/panes'
import type { SchedulerPane, SchedulerPanesHandle } from 'super-scheduler/panes'
import 'super-scheduler/styles.css'
const isTray = (resource: SuperScheduler.ResourceData) => resource.kind === 'tray'
// Module-level (or memoized): a new `panes` array resets the sizes the user dragged.
const PANES: SchedulerPane[] = [
{ id: 'rooms', rowFilter: (resource) => !isTray(resource), minSize: 200 },
{
id: 'tray',
rowFilter: isTray,
size: 140,
minSize: 96,
// Per-pane overrides: smaller events in the unassigned tray.
props: { eventHeight: 28 },
},
]
export function RoomsWithTray(props: {
resources: SuperScheduler.ResourceData[]
initial: SuperScheduler.EventData[]
}) {
const [events, setEvents] = useState(props.initial)
const panesRef = useRef<SchedulerPanesHandle>(null)
// One list for every pane: each event appears in the pane that holds its resource.
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
[],
)
const shared = useMemo<Partial<SchedulerProps>>(
() => ({
onEventMove: (args) => {
// `pane` is where the event lands; `sourcePane` is set only for a move between panes.
if (args.sourcePane !== 'rooms' || args.pane !== 'tray') return
// Unassigning a booking asks first; the drop waits for the answer.
args.async = true
void confirmWithUser(`Unassign ${args.e.text()}?`).then((ok) => {
if (!ok) args.preventDefault()
args.loaded()
})
},
}),
[],
)
return (
<>
<button type="button" onClick={() => panesRef.current?.scrollTo('2026-10-01', 'left')}>
Go to 1 October
</button>
<SchedulerPanes
{...shared}
panesRef={panesRef}
panes={PANES}
// Total height of every pane, the splitter and the shared header.
height={640}
resources={props.resources}
events={events}
onEventsChange={onEventsChange}
splitter={{ size: 6, step: 8 }}
startDate="2026-10-01"
days={60}
scale="Day"
cellWidth={44}
/>
</>
)
}Vous devriez voir les chambres en haut et, en dessous, une zone « à affecter » de 140 px, avec un seul en-tête de temps tout en haut. Faites défiler l’un des volets latéralement : l’autre suit. Glissez une réservation de la zone vers une chambre : elle s’y déplace ; glissez-en une d’une chambre vers la zone : l’application demande d’abord confirmation.
Options des volets
| Champ | Défaut | Effet |
|---|---|---|
id | obligatoire | Identifie le volet dans les handlers (args.pane), dans panesRef et dans le DOM (data-pane) |
resources | Les lignes de ce volet | |
rowFilter | Choisit les lignes de ce volet parmi les resources partagées ; utilisez soit cette option, soit resources | |
size | 'auto' | Des pixels, un pourcentage de la hauteur libre ('30%'), ou 'auto' pour une part de ce qui reste |
minSize | 48 | Hauteur minimale en pixels ; les minimums l’emportent quand le total est trop petit |
hidden | false | Masque le volet mais le garde monté : le réafficher ne coûte rien |
props | Props propres à ce volet ; les handlers définis ici remplacent les handlers partagés |
Les lignes sont réparties par ressource de premier niveau : un parent emmène ses enfants dans son volet. Le premier volet qui n’a ni resources ni rowFilter reçoit toutes les ressources de premier niveau que les autres volets n’ont pas prises.
Disposition et séparateur
| Prop | Défaut | Effet |
|---|---|---|
height | obligatoire | Hauteur totale en pixels : tous les volets, les séparateurs et l’en-tête partagé |
timeHeader | 'first' | 'first' n’affiche l’en-tête de temps que sur le premier volet visible ; 'all' sur chaque volet |
scrollbar | 'last' | Barre de défilement horizontale sur le dernier volet seulement, ou 'all' |
splitter | true | { size, step } définit son épaisseur (6 px) et son pas au clavier (8 px) ; false le supprime |
onPaneResize | { sizes } par id de volet, après validation d’un redimensionnement |
Le séparateur est focalisable, avec role="separator" ; sa valeur est la hauteur du volet situé en dessous. Haut et Bas le déplacent de step, Maj+Haut et Maj+Bas de 40 px, Début et Fin l’amènent aux limites, et Entrée ou un double-clic rétablit les tailles déclarées. Pendant le glissement, les volets sont prévisualisés ; leurs hauteurs changent au relâchement. Dans la 0.1.0, son nom accessible est l’anglais « Pane size », sans option pour le traduire.
Les événements dans les volets
Passez tous les événements une seule fois. Chaque volet affiche les événements dont la resource est l’une de ses lignes, et un événement passe dans un autre volet quand sa ressource y passe.
- Contrôlé :
eventsplusonEventsChange. Le handler reçoit la liste complète et fusionnée dansargs.events, ainsi queargs.panepour le volet où la modification a eu lieu. Adoptez-la comme dans l’état contrôlé. - Non contrôlé :
defaultEvents, et les volets gèrent la liste eux-mêmes.
Modifier directement le control.events.list d’un volet n’est pas répercuté sur les autres volets ; passez par l’état ou par l’API control.events.
Déplacements entre volets
Le glissement entre volets est activé par défaut (crossPaneMove: true) ; false garde chaque événement dans son volet. Avec la valeur par défaut eventMoveHandling: 'Update', un déplacement entre volets est signalé une seule fois, comme une modification 'move' dans onEventsChange.
Chaque handler partagé reçoit args.pane. Lors d’un déplacement entre volets, onEventMove et onEventMoved reçoivent aussi args.sourcePane, si bien qu’une règle peut dépendre du sens : l’extrait ne demande confirmation que pour les déplacements des chambres vers la zone, avec args.async et args.loaded(). Un déplacement annulé ou refusé laisse les données inchangées.
Accéder au contrôle de chaque volet
SchedulerPanes crée les planificateurs : il vous donne donc leurs contrôles via panesRef :
controls: une map de l’id du volet vers son contrôle, etcontrol(id)pour l’un d’eux ;forEach(run)pour appeler quelque chose sur chaque volet ;scrollTo(date, position)pour les faire défiler ensemble ;update(options)pour appliquer des options à chaque volet.
Pour utiliser les slots de rendu React dans les volets, passez le composant de ce point d’entrée : component={SuperSchedulerComponent}, importé de super-scheduler/react-render. Le module des volets ne l’importe que si vous le faites.
Relier des planificateurs que vous placez vous-même
Quand les planificateurs ne sont pas empilés (un planning du personnel en haut de la page et un planning des salles plus bas), gardez vos propres composants et reliez leurs contrôles avec linkPanes :
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { linkPanes } from 'super-scheduler/panes'
// Two schedulers placed by your own layout (here, a page section apart) that move together.
export function LinkedBoards(props: {
staff: SuperScheduler.ResourceData[]
rooms: SuperScheduler.ResourceData[]
shifts: SuperScheduler.EventData[]
bookings: SuperScheduler.EventData[]
}) {
const staff = useSchedulerControl()
const rooms = useSchedulerControl()
const shifts = useMemo(() => props.shifts.slice(), [props.shifts])
const bookings = useMemo(() => props.bookings.slice(), [props.bookings])
useEffect(() => {
if (staff.control === null || rooms.control === null) return
// Horizontal scroll always; zoom and row header width too unless turned off.
const link = linkPanes([staff.control, rooms.control], { zoom: true, rowHeaderWidth: true })
return () => link.dispose()
}, [staff.control, rooms.control])
return (
<>
<h2>Staff</h2>
<SuperSchedulerComponent
controlRef={staff.controlRef}
startDate="2026-10-01"
days={30}
scale="Day"
resources={props.staff}
events={shifts}
/>
<h2>Rooms</h2>
<SuperSchedulerComponent
controlRef={rooms.controlRef}
startDate="2026-10-01"
days={30}
scale="Day"
resources={props.rooms}
events={bookings}
/>
</>
)
}Le défilement horizontal est toujours partagé. Le zoom et la largeur de l’en-tête de ligne le sont aussi, sauf si vous passez zoom: false ou rowHeaderWidth: false. Appelez dispose() pour défaire le lien.
Enregistrer et restaurer une vue
Une vue, c’est la façon dont l’utilisateur regarde les données, pas les données elles-mêmes. getViewState(control, include?) la capture sous forme d’un petit objet sérialisable en JSON ; applyViewState(control, state, options?) la restaure.
Clé de include | Champs enregistrés | Remarques |
|---|---|---|
'zoom' | cellWidth, zoomLevel | zoomLevel est l’index du niveau actif dans zoomLevels : gardez leur ordre stable |
'scroll' | anchorDate, topRowId, topOffset | La date au bord gauche et la ligne du haut, par id, avec le décalage à l’intérieur de celle-ci |
'density' | density | Seulement si vous définissez la prop density |
'collapsed' | collapsed | Id des parents d’arborescence repliés |
'columns' | columnWidths, columnOrder | Largeur et ordre des colonnes de l’en-tête de ligne |
Chaque état comporte v: 1. Sans include, les cinq clés sont capturées.
import type { SuperScheduler } from 'super-scheduler'
import { applyViewState, getViewState } from 'super-scheduler/views'
import type { SchedulerViewState, ViewStateKey } from 'super-scheduler/views'
// What this application restores from the view. Density and columns stay in React state here.
const KEYS: readonly ViewStateKey[] = ['zoom', 'scroll', 'collapsed']
const storageKey = (user: string, view: string) => `planning-view:${user}:${view}`
/** Saves the current view. The application owns storage: here localStorage, per user. */
export function saveView(control: SuperScheduler.Scheduler, user: string, view: string): void {
const state = getViewState(control, KEYS)
try {
localStorage.setItem(storageKey(user, view), JSON.stringify(state))
} catch {
// Storage can be full or disabled; a view is a convenience, not data.
}
}
/** Stored values are untrusted input: check the shape before using them. */
function isViewState(value: unknown): value is SchedulerViewState {
return typeof value === 'object' && value !== null && (value as { v?: unknown }).v === 1
}
/** Restores a saved view. Resolves false when nothing was saved or the rows never appeared. */
export async function restoreView(
control: SuperScheduler.Scheduler,
user: string,
view: string,
): Promise<boolean> {
let saved: unknown = null
try {
saved = JSON.parse(localStorage.getItem(storageKey(user, view)) ?? 'null')
} catch {
return false
}
if (!isViewState(saved)) return false
// Waits (up to 5 s) for the rows and the saved top row, for data that loads after mount.
return applyViewState(control, saved, { when: 'rows', timeout: 5000 })
}import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { restoreView, saveView } from './saved-view'
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{ id: 'weeks', properties: { scale: 'Week', cellWidth: 120 } },
{ id: 'days', properties: { scale: 'Day', cellWidth: 44 } },
]
export function PlannerWithViews(props: {
user: string
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
}) {
const { controlRef, control } = useSchedulerControl()
const events = useMemo(() => props.events.slice(), [props.events])
// Restore once the control exists; keep row ids stable so the top row can be found again.
useEffect(() => {
if (control !== null) void restoreView(control, props.user, 'default')
}, [control, props.user])
return (
<>
<button
type="button"
disabled={control === null}
onClick={() => control && saveView(control, props.user, 'default')}
>
Save this view
</button>
<SuperSchedulerComponent
controlRef={controlRef}
startDate="2026-01-01"
days={365}
zoomLevels={ZOOM_LEVELS}
zoom="days"
treeEnabled
resources={props.resources}
events={events}
/>
</>
)
}Faites défiler jusqu’à une date, repliez un étage, appuyez sur « Save this view » et rechargez la page : le planning revient à la même date et à la même ligne, avec l’étage replié.
Comment se passe la restauration :
when: 'rows'(par défaut) attend que les lignes, et la ligne du haut enregistrée, existent ; cela couvre les données qui arrivent après le montage. Si elles n’apparaissent pas dans le délaitimeout(5 000 ms par défaut), la promesse se résout avecfalse.when: 'now'applique immédiatement ; si la ligne du haut enregistrée a disparu, le décalage enregistré sert de position de défilement absolue.animate: trueanime le changement de zoom.- Les parents listés dans
collapsedsont repliés et tous les autres parents sont dépliés. - Si les colonnes enregistrées ne correspondent plus à
rowHeaderColumns(un nombre de colonnes différent), rien n’est appliqué et la promesse se résout avecfalse. Un état d’une version autre que 1 se résout aussi avecfalse. - La restauration ne déplace pas le focus clavier.
Avec des volets, enregistrez et restaurez via le contrôle d’un seul volet (panesRef.current?.control('rooms')) : le zoom et le défilement horizontal sont partagés, tandis que le défilement vertical et les lignes repliées appartiennent à ce volet.
Ce qui revient à votre application
- Le stockage.
localStoragepour un seul navigateur, ou votre backend pour suivre l’utilisateur d’un appareil à l’autre. La bibliothèque ne stocke jamais rien. - Le nommage et le partage. Vues nommées, vues par défaut par équipe, liens qui ouvrent une vue.
- La validation. Les vues stockées sont des entrées non fiables : vérifiez leur forme et
vavant de les appliquer, et écartez celles qui échouent. - Des id stables. Les id de lignes doivent désigner les mêmes lignes d’une session à l’autre pour que
topRowIdetcollapsedfonctionnent. - Tailles des volets et sélections. Ni les unes ni les autres ne font partie d’une vue ; stockez les tailles des volets depuis
onPaneResizesi vous voulez les retrouver.
Voir aussi
- Arbres de ressources, colonnes et sélection pour les lignes repliées et les colonnes qu’une vue enregistre.
- Échelles de temps et zoom pour les niveaux de zoom qu’une vue restaure.