Pro-ModuleGilt fürSuperScheduler Pro
Rückgängig, Wiederholen und Verlauf
Erzeugen Sie mit createHistory() einen einzigen Verlauf und übergeben Sie ihn als history-Prop: Verschieben und Dauer ändern werden aufgezeichnet, und Ctrl/Cmd+Z, Ctrl/Cmd+Umschalt+Z sowie Ctrl+Y funktionieren, solange der Fokus im Planer liegt. Bei kontrollierten Ereignissen übernehmen Sie die Änderungen mit dem Grund 'history' aus onEventsChange oder geben createHistory eine apply-Funktion, die Ihren Zustand aktualisiert. Nutzen Sie push für eigene Befehle, batch zum Bündeln von Änderungen, revert, um eine vom Server abgelehnte Änderung rückgängig zu machen, und subscribe oder onHistoryChange für die Schaltflächen Rückgängig und Wiederholen.
Direkte Manipulation lädt zu Fehlern ein: ein Ereignis, das eine Zeile zu tief abgelegt wurde, eine Dauer, die einen Tag zu weit gezogen wurde. super-scheduler/history führt einen Rückgängig-Stapel für Änderungen an Ereignissen und für Ihre eigenen Befehle, mit Tastenkürzeln, Beschriftungen für Schaltflächen, Bündelung und einem Rollback für Änderungen, die Ihr Server ablehnt. Der Verlauf lebt für die Dauer der Sitzung im Speicher; etwas dauerhaft zu speichern ist Aufgabe Ihrer Anwendung.
Der Verlauf erfordert SuperScheduler Pro.
Einen Verlauf anbinden
createHistory() gibt ein Verlaufsobjekt zurück. Übergeben Sie es dem Planer als history (oder innerhalb von extensions). Erzeugen Sie es nur einmal: Props werden über ihre Identität verglichen, und ein während des Renderns erzeugter Verlauf wäre bei jedem Rendern ein neuer, leerer.
import { useEffect, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerHistoryChangeArgs, SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
import type { SchedulerHistory } from 'super-scheduler/history'
import 'super-scheduler/styles.css'
type HistoryState = Pick<
SchedulerHistoryChangeArgs,
'canUndo' | 'canRedo' | 'undoLabel' | 'redoLabel'
>
/** Mirrors the history in React: one render per change, never per gesture frame. */
export function useHistoryState(history: SchedulerHistory): HistoryState {
const [state, setState] = useState<HistoryState>(() => ({
canUndo: history.canUndo,
canRedo: history.canRedo,
undoLabel: history.undoLabel,
redoLabel: history.redoLabel,
}))
useEffect(() => history.subscribe(setState), [history])
return state
}
export function UndoablePlanner(props: {
resources: SuperScheduler.ResourceData[]
initialEvents: SuperScheduler.EventData[]
}) {
// Created once: `history` is compared by identity, like every prop.
const [history] = useState(() =>
createHistory({
limit: 100,
// Default: moves and resizes. Mod+Z, Mod+Shift+Z and Ctrl+Y work while focus is in the grid.
record: ['move', 'resize'],
keys: 'root',
labels: { move: 'move', resize: 'resize' },
}),
)
const [initial] = useState(() => props.initialEvents.slice())
const state = useHistoryState(history)
return (
<>
<div role="toolbar" aria-label="History">
<button type="button" disabled={!state.canUndo} onClick={() => history.undo()}>
{state.undoLabel === null ? 'Undo' : `Undo ${state.undoLabel}`}
</button>
<button type="button" disabled={!state.canRedo} onClick={() => history.redo()}>
{state.redoLabel === null ? 'Redo' : `Redo ${state.redoLabel}`}
</button>
</div>
<SuperSchedulerComponent
history={history}
// Uncontrolled: the control owns the events after mount.
defaultEvents={initial}
resources={props.resources}
startDate="2026-10-01"
days={31}
scale="Day"
/>
</>
)
}Ziehen Sie ein Ereignis, und die erste Schaltfläche lautet „Undo move“. Klicken Sie darauf oder drücken Sie Cmd+Z (Ctrl+Z unter Windows und Linux), nachdem Sie ein Ereignis angeklickt haben, und das Ereignis springt zurück; „Redo move“ stellt die Verschiebung wieder her.
Ein Verlauf kann mehreren Planern dienen, zum Beispiel den Bereichen einer geteilten Ansicht. Rückgängig folgt dann der Reihenfolge der Änderungen über alle Planer hinweg.
Optionen
| Option | Standard | Wirkung |
|---|---|---|
limit | 50 | Anzahl der behaltenen Einträge; darüber hinaus fällt der älteste weg. 0 zeichnet nichts auf. |
record | ['move', 'resize'] | Automatisch aufgezeichnete Änderungen: 'move', 'resize', 'create', 'remove', 'update' |
keys | 'root' | Wo Tastenkürzel greifen: im Planer, im ganzen 'document' oder mit false nirgends |
equals | Start, Ende, Ressource, Text | Zwei Ereigniszustände, die als gleich gelten, erzeugen keinen Eintrag |
fields | keine | Zusätzliche Felder für den Standardvergleich |
apply | 'control' | Wer Rückgängig und Wiederholen anwendet: das Control oder Ihre Funktion |
labels | Englisch oder Spanisch | Beschriftung pro Art: move, resize, create, remove, update, command |
Was aufgezeichnet wird:
- Gesten.
'move'und'resize'decken Verschieben und Dauer ändern per Zeiger und per Tastatur ab. Ein Eintrag entsteht, nachdem die Änderung bestätigt wurde: Eine Verschiebung, die Ihre Regeln ablehnen oder die während einer asynchronen Bestätigung abgebrochen wird, hinterlässt keinen Eintrag. - API-Änderungen.
'create','remove'und'update'zeichnen Aufrufe voncontrol.events.add(),remove()undupdate()auf. - Nie. Ladevorgänge: ein neues
events-Array aus React und Ereignisse, die beim bereichsweisen Laden eintreffen. Rückgängig und Wiederholen selbst werden nicht erneut aufgezeichnet.
Die Tastenkürzel sind Cmd+Z und Cmd+Umschalt+Z unter macOS, sonst Ctrl+Z, Ctrl+Umschalt+Z und Ctrl+Y. Sie werden ignoriert in Eingabefeldern, Textbereichen und bearbeitbaren Elementen, bei gedrückter Alt-Taste und wenn ein anderer Handler die Taste bereits verarbeitet hat. Bei keys: 'root' muss der Fokus im Planer liegen: Ein Klick auf ein Ereignis oder das Ziehen eines Ereignisses setzt ihn dorthin, ebenso die Tab-Taste, wenn die Tastatur aktiviert ist. 'document' wirkt überall auf der Seite; verwenden Sie es daher nur, wenn kein anderer Teil der Seite ein eigenes Rückgängig hat.
Den Rückgängig-Zustand anzeigen
Das Verlaufsobjekt stellt canUndo, canRedo, undoLabel und redoLabel bereit sowie undo() und redo(), die false zurückgeben, wenn es nichts zu tun gibt. Es gibt drei Wege, Änderungen zu verfolgen:
history.subscribe(listener)gibt eine Funktion zum Abmelden zurück und passt damit natürlich in einenuseEffect(dasuseHistoryStateim Snippet);- die Prop
onHistoryChangeerhält denselben Zustand, mitthisauf das Control gesetzt; useScheduler({ track: ['history'] })aussuper-scheduler/hooksstellt ihn als React-State bereit.
Jede Benachrichtigung enthält eine cause: 'record', 'undo', 'redo', 'clear' oder 'revert'. Die Beschriftungen sind Ihr label oder der Standard für die jeweilige Art, auf Englisch oder auf Spanisch, wenn die locale des Planers mit es beginnt. Für andere Sprachen übergeben Sie labels.
Kontrollierte Ereignisse
Wenn React-State die Ereignisse verwaltet (events plus onEventsChange), gibt es zwei Wege, Rückgängig anzuwenden.
Mit dem Standard apply: 'control' ändert Rückgängig die Ereignisse des Controls über control.events.*, und das Ergebnis erreicht onEventsChange mit reason: 'history'. Wenn Sie dort ohnehin jede Änderung übernehmen, funktioniert Rückgängig ohne weiteren Code.
Mit einer apply-Funktion übergibt Ihnen der Verlauf die Operationen, und Sie aktualisieren Ihren Zustand; die neue events-Prop erreicht dann das Control. Das passt zu Stores, Reducern und Apps, die jede Änderung über einen einzigen Codepfad speichern.
import { useCallback, useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
import type { HistoryOp } from 'super-scheduler/history'
type EventData = SuperScheduler.EventData
/** Applies history operations to React state. They arrive in order (reversed for undo). */
function applyOps(
events: EventData[],
ops: readonly HistoryOp[],
direction: 'undo' | 'redo',
): EventData[] {
let next = events
for (const op of ops) {
const target = direction === 'undo' ? op.before : op.after
const id = (target ?? op.before ?? op.after)?.id
if (id === undefined) continue
const index = next.findIndex((event) => event.id === id)
if (target === null) next = next.filter((event) => event.id !== id)
else if (index >= 0) next = next.map((event, i) => (i === index ? target : event))
else next = [...next, target]
}
return next
}
export function ControlledUndo(props: {
resources: SuperScheduler.ResourceData[]
initial: EventData[]
}) {
const [events, setEvents] = useState(props.initial)
// The control adopts the array it receives and mutates it: give it a copy.
const owned = useMemo(() => events.slice(), [events])
const [history] = useState(() =>
createHistory({
record: ['move', 'resize'],
// Undo and redo update React state; the new `events` prop then reaches the control.
apply: (ops, direction) => setEvents((current) => applyOps(current, ops, direction)),
}),
)
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
[],
)
const onTimeRangeSelected = useCallback(
(args: SuperScheduler.SchedulerTimeRangeSelectedArgs) => {
if (args.origin !== 'drag') return
args.control.clearSelection()
const booking: EventData = {
id: SuperScheduler.guid(),
resource: args.resource,
start: args.start,
end: args.end,
text: 'New booking',
}
setEvents((current) => [...current, booking])
// A change made through state is a data load for the control, not a gesture:
// record it explicitly so it can be undone.
history.record({
kind: 'create',
label: 'new booking',
ops: [{ before: null, after: booking }],
})
},
[history],
)
return (
<SuperSchedulerComponent
history={history}
events={owned}
onEventsChange={onEventsChange}
onTimeRangeSelected={onTimeRangeSelected}
resources={props.resources}
startDate="2026-10-01"
days={31}
scale="Day"
/>
)
}Jede Operation hat die Form { before, after }. before: null bedeutet, dass das Ereignis erzeugt wurde, after: null, dass es entfernt wurde. Die Operationen kommen in der Reihenfolge an, in der sie anzuwenden sind, für Rückgängig bereits umgekehrt.
Änderungen bündeln und Befehle ergänzen
history.batch(label, run) fasst alles, was während der Ausführung von run aufgezeichnet wird, zu einem einzigen Eintrag zusammen. Sammelaktionen lassen sich dann in einem Schritt rückgängig machen. Bündel lassen sich verschachteln; die äußere Beschriftung gewinnt.
history.push({ label, undo, redo }) fügt einen eigenen Befehl hinzu, für Änderungen außerhalb der Ereignisse des Planers: einen gesperrten Tag, eine Ressourceneinstellung, eine Abhängigkeit zwischen Ereignissen. Rückgängig und Wiederholen rufen Ihre Funktionen auf.
import { useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
export function BulkEditing(props: {
resources: SuperScheduler.ResourceData[]
initial: SuperScheduler.EventData[]
}) {
const { controlRef, control } = useSchedulerControl()
const [initial] = useState(() => props.initial.slice())
const [frozenDays, setFrozenDays] = useState<ReadonlySet<string>>(new Set())
// 'update' also records changes made through control.events.update().
const [history] = useState(() => createHistory({ record: ['move', 'resize', 'update'] }))
// One undo step for the whole operation.
const shiftSelected = (days: number) => {
if (control === null) return
const selected = control.multiselect.get()
history.batch(`shift ${selected.length} jobs`, () => {
for (const e of selected) {
// A new object: the stored one stays intact as the state undo restores.
// (The wrapper setters e.start(...) edit the stored object in place.)
control.events.update({
...e.data,
start: e.start().addDays(days),
end: e.end().addDays(days),
})
}
})
}
// A change outside the scheduler's events, undone through the same history.
const freezeDay = (day: string) => {
const add = () => setFrozenDays((current) => new Set(current).add(day))
const remove = () =>
setFrozenDays((current) => new Set([...current].filter((item) => item !== day)))
add()
history.push({ label: `freeze ${day}`, undo: remove, redo: add })
}
return (
<>
<button type="button" onClick={() => shiftSelected(1)}>
Move selection one day later
</button>
<button type="button" onClick={() => freezeDay('2026-10-12')}>
Freeze 12 October
</button>
<p>Frozen days: {[...frozenDays].join(', ') || 'none'}</p>
<SuperSchedulerComponent
controlRef={controlRef}
history={history}
defaultEvents={initial}
eventClickHandling="Select"
resources={props.resources}
startDate="2026-10-01"
days={31}
scale="Day"
/>
</>
)
}Wählen Sie zwei Ereignisse aus (Klick, dann Cmd+Klick), drücken Sie „Move selection one day later“ und dann „Freeze 12 October“. Das erste Rückgängig hebt die Sperre des Tages auf; das zweite verschiebt beide Ereignisse auf einmal zurück.
Vom Server bestätigte Abläufe
Die Bibliothek ruft nie Ihr Backend auf. Zwei Muster decken die meisten Anwendungen ab.
Optimistisch. Lassen Sie die Verschiebung geschehen und aufzeichnen, speichern Sie sie in onEventMoved und rollen Sie zurück, wenn der Server ablehnt. history.revert(eventId) stellt das Ereignis auf den Zustand vor seinem letzten Eintrag zurück und verwirft diesen Eintrag, sodass ein späteres Rückgängig die abgelehnte Änderung nicht wiederbelebt.
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
export function OptimisticPlanner(props: {
resources: SuperScheduler.ResourceData[]
initial: SuperScheduler.EventData[]
}) {
const [events, setEvents] = useState(props.initial)
const owned = useMemo(() => events.slice(), [events])
const [history] = useState(() => createHistory())
const config = useMemo<SchedulerProps>(
() => ({
history,
onEventsChange: ({ events: next }) => setEvents([...next]),
// The move is already applied and recorded: save it, and roll back if the server refuses.
onEventMoved: (args) => {
const id = args.e.id()
saveBooking({
id: String(id),
resource: String(args.newResource),
start: args.newStart.value,
end: args.newEnd.value,
}).catch(() => {
// Restores the event's previous state and drops that history entry.
history.revert(id)
args.control.message('The move could not be saved and was undone.')
})
},
}),
[history],
)
const undo = useCallback(() => history.undo(), [history])
return (
<>
<button type="button" onClick={undo}>
Undo
</button>
<SuperSchedulerComponent
{...config}
events={owned}
resources={props.resources}
startDate="2026-10-01"
days={31}
scale="Day"
/>
</>
)
}Wenn das Speichern fehlschlägt, springt das Ereignis zurück, und die Meldungsleiste erklärt den Grund.
Erst bestätigen. Fragen Sie den Server (oder den Nutzer), bevor die Änderung angewendet wird: Setzen Sie args.async = true in onEventMove und rufen Sie dann args.loaded() auf, um anzunehmen, oder args.preventDefault() und args.loaded(), um abzulehnen. Der Verlauf zeichnet die Verschiebung erst auf, wenn sie angenommen ist. Für Abläufe, in denen die bestätigte Änderung von der Geste abweicht (ein Dialog, der das Ergebnis bearbeitet), setzen Sie record: [] und fügen nach der Bestätigung durch den Server mit push einen Befehl hinzu.
Rückgängig und Wiederholen sind ebenfalls Änderungen: Speichern Sie sie auf dieselbe Weise, entweder in Ihrer apply-Funktion oder wenn onEventsChange reason: 'history' meldet. Zu erkennen, dass jemand anderes das Ereignis zwischenzeitlich geändert hat (Versionen, Konflikte), ist Aufgabe Ihres Backends.
Was der Verlauf nicht leistet
Verwandte Themen
Planung einer VideoproduktionEin Dreh dauert länger. Verschieben Sie den abhängigen Schnitt, verstehen Sie warum, und nehmen Sie es zurück. Ressourcenplanung für eine KreativagenturEine Designerin ist doppelt gebucht. Übergeben Sie zwei Aufgaben mit einem Ziehen, prüfen Sie, was sie freigeben, und nehmen Sie es zurück.
- Kontrollierte Ereignisse und Callbacks zu
onEventsChangeund seinen Gründen. - Ziehen, Dauer ändern und Geschäftsregeln zur asynchronen Bestätigung.
Verwandte Beispiele
- FramehousePlanung einer VideoproduktionEin Dreh dauert länger. Verschieben Sie den abhängigen Schnitt, verstehen Sie warum, und nehmen Sie es zurück.
- Studio NorthRessourcenplanung für eine KreativagenturEine Designerin ist doppelt gebucht. Übergeben Sie zwei Aufgaben mit einem Ziehen, prüfen Sie, was sie freigeben, und nehmen Sie es zurück.