Rückgängig machen und Verlauf
graph.history ist eine Aufzeichnung von was die Arbeitsfläche enthält und zeigt – was hineingebracht wurde, was herausgenommen wurde, was verborgen ist – und eine Möglichkeit, etwas davon zurückzunehmen.
Es handelt sich nicht um eine Aufzeichnung aller Mutationen, die die Bibliothek durchführen kann. Die Daten eines Knotens gehören zu Ihrem Backend, daher bietet die Bibliothek nicht an, ein Feld zurückzusetzen, das sie nicht erstellt hat. Wofür es sprechen kann, ist die Zusammensetzung, über die auch ein Analyst nachdenkt: Zwölf Knoten sind angekommen, drei sind gegangen, fünf sind verborgen.
Vokabular, im Folgenden einheitlich verwendet
- Eintrag: eine umkehrbare Sache, die passiert ist, als eine Zeile im Menü
- kind: um welche der sechs Eintragsarten es sich handelt
- span: der zusammenhängende Block vom neuesten Eintrag bis zu dem, auf den Sie geklickt haben
- persisted: ein Eintrag, den der Verbraucher an ein Backend durchgeschrieben hat
- versiegelt: eine dauerhafte Löschung – aufgeführt, nie rückgängig gemacht
Die sechs Arten
Eine geschlossene Menge. Alles, was die Bibliothek aufzeichnet, gehört dazu, und nichts anderes wird aufgezeichnet.
| Freundlich | Was es ist | Wie es umgekehrt ist |
|---|---|---|
pivot | Eine Aufnahme – siehe Pivots & Enrichment | Die Bürgschaft des Laufs entfällt, und alles, wofür sonst nichts bürgt, geht |
delete | Eine vom Benutzer initiierte Löschung von Knoten oder Kanten | Die Elemente kommen zurück, Herkunft und alles |
visibility | Ein dauerhaftes Ein- oder Ausblenden (queryEngine.excludeNode / includeNode) | Der andere |
create | Ein von Hand gezeichneter Knoten oder eine Kante | Das Element gilt, es sei denn, etwas anderes bürgt dafür |
removal | Ein graph.removeBySource(source)-Anruf | Was übrig geblieben ist, kommt zurück, und was geblieben ist, erhält die Bürgschaft der Quelle zurück |
group | Eine Gruppe, die manuell erstellt, umbenannt oder entfernt wurde (siehe Vereinfachen) | Die handgefertigten Gruppen werden wieder so, wie sie waren; Es wird kein Knoten hinzugefügt oder entfernt |
Absichtlich nicht erfasst:
- Eigenschaftsänderungen. Backend-Status, wie oben.
onBeforeNodeEditCommitHier implementieren Sie Ihre eigenen. - Filter- und Legendenstatus. Einstellungen mit eigenem Löschen statt Handlungen. Das Filterformular wird bei jedem Tastendruck aktiv angewendet, sodass die Eingabe von
domainsechs Einträge verschieben würde. - Notizen. Autorisierter Text, der eher einer Bearbeitung als einer Grafikkomposition ähnelt.
graph.hideNode(). Es wird gelöscht, wenn die Filter das nächste Mal neu abgeleitet werden, sodass ein Eintrag dafür etwas rückgängig machen würde, das sich bereits selbst zurückgesetzt hat.excludeNodeist die Haut, die überlebt und aufgezeichnet wird.- Eine Gruppe öffnen, einen Knoten herausziehen, eine Vereinfachungsregel ändern. Eine Sichtweise, wie die Filter.
Programmatische Mutationen – graph.addNode(), graph.removeNode() – sind ebenfalls keine Einträge. Es gibt nur eine Geste, plus graph.removeBySource(), wodurch eine Anwendung dem Analysten die Aktion „Entfernen, was diese Quelle gebracht hat“ anbietet, und graph.simplify.groupNodes(), renameGroup() und ungroup(), die Aufrufe hinter den Gruppengesten.
Rückgängig ist zusammenhängend
Wenn Sie auf die Reihe drei nach unten zielen, werden diese drei rückgängig gemacht, da einmal dataBatchChanged und einmal erneut gerendert wird. Es gibt keine Möglichkeit, einen alten Eintrag allein rückgängig zu machen: Ein Stapel, dessen Mitte herausgezogen werden kann, beschreibt nicht mehr, wie der Graph so entstanden ist.
Für „Dieser Anbieter hat Junk zurückgegeben, werfen Sie ihn weg und behalten Sie den Rest“ verwenden Sie graph.removeBySource(source). Es löscht die Bürgschaft einer Quelle und löscht nur das, wofür nichts anderes bürgt, so dass ein Knoten, der auch bei einem zweiten Durchlauf gefunden wurde, überlebt, ein Anspruch weniger. Es handelt sich um einen Vorwärts-Vorgang, der als neuer Eintrag und nicht als Rücklauf aufgezeichnet wird.
Dieser Eintrag wird immer dann aufgezeichnet, wenn die Quelle für irgendetwas bürgt, selbst wenn jedes Element erhalten bleibt, weil eine andere Quelle ebenfalls dafür bürgt: Die Behauptung ist so oder so weg, und in der Zeile heißt esNichts verließ die Arbeitsfläche. Durch das Rückgängigmachen wird die Bürgschaft jedes Elements genau so wiederhergestellt, wie sie war, sodass der Run-Eintrag darunter immer noch vollständig ist und das nächste Rückgängigmachen diesen Run zurücknimmt.
Redo ist streng linear und wird durch jede neue Aktion geleert. Ein Lauf, der rückgängig gemacht wurde und dann durch spätere Arbeit gescheitert ist, ist verschwunden; Um es zurückzubekommen, muss der Pivot erneut ausgeführt werden.
Von der oberen Leiste
Bei jeder der beiden Schaltflächen in der Kopfzeile handelt es sich um eine geteilte Schaltfläche: Das Symbol bewegt sich einmal, die Einfügemarke öffnet den Verlauf. Ctrl+Z und Ctrl+Shift+Z (⌘Z und ⌘⇧Z auf macOS) erreichen die gleichen zwei Schritte ohne die Kopfzeile und fallen zum rückgängig machen des Browser-eigenen Textes, während ein Textfeld den Fokus hat. Beide Schaltflächen befinden sich im full- und light-Modus, in dem sich die Kopfzeile befindet, und UI.history: { enabled: false } entfernt sie, das Dropdown-Menü und die beiden Tastenkombinationen für einen Benutzer, der aus seinem eigenen Chrome heraus „Rückgängig“ fährt – graph.history fährt in beide Richtungen mit der Aufnahme fort.
Das Dropdown-Menü ist eine Liste von beiden Schaltflächen, die neueste oben, mit einer durchgehenden Linie „jetzt“: Die darüber liegenden Zeilen wurden rückgängig gemacht, die Zeilen darunter können noch rückgängig gemacht werden. Klicken Sie auf eine Zeile und die Linie verläuft daran vorbei, was in beiden Richtungen die gleiche Geste ist.
Jede Zeile trägt das Symbol ihrer Art, ihre Beschriftung und wie viele Schritte ein Klick darauf zurücklegt. Diese Nummer ist diejenige, die in der Fußzeile angegeben ist, sodass die Liste und die Fußzeile nicht übereinstimmen können. In der Fußzeile wird der genaue Nettoeffekt angegeben, bevor etwas festgeschrieben wird. Pfeiltasten zielen, Enter fährt, Escape schließt.
Die Arbeitsfläche antwortet auch
Wenn Sie den Mauszeiger über eine Zeile ** bewegen, wird die Änderung auf der Arbeitsfläche vorhergesagt**, und es ändert sich nichts anderes: Die Elemente, die der Klick entfernen würde, verschwinden dort, wo sie stehen, die Knoten, die er mit ihnen verstecken würde, und was auch immer er zurückbringen würde, ist umrissen, wo er landen würde – ein Knoten, den der Graph noch nicht enthält, wird als Ring an der Position gezeichnet, die er verlassen hat. Der letzte Teil ist das meiste, was eine Wiederholung zu sagen hat, und der Grund dafür, dass eine Prognose kein Highlight ist: Nichts wird zurückgezogen, um die markierten Elemente hervorzuheben, denn die Frage ist, was sich ändern würde, und nicht, welche Elemente eine Zeile benennt.
Die Umrisse sind keine Vermutungen. Beim Rückgängigmachen wird die Stelle vermerkt, an der sich jedes Element befand, als es von der Arbeitsfläche entfernt wurde, und beim Wiederherstellen wird es wieder dorthin zurückgesetzt. Die Prognose zeigt also, wo der Klick die Dinge landet. Ein Pivot reproduziert die Rohdaten seines Anbieters, die keine Koordinaten enthalten, sodass eine Aufnahme ohne diese verstreut an einer neuen Stelle zurückkommen würde.
preview() gibt das Gleiche zurück wie Daten unter forecast und graph.showForecast(…) / graph.clearForecast() malt es – so erhält die eigene Verlaufs-Benutzeroberfläche eines Verbrauchers oder alles andere, das „Was wäre wenn“ fragen möchte, die Canvas-Sprache kostenlos.
In ein Backend geschriebene Vorgänge
Nur der Verbraucher kann wissen, dass ein Vorgang in ein Backend geschrieben wurde, also kann nur der Verbraucher dies sagen. onBeforeNodeCreate, onBeforeEdgeCreate und onBeforeDelete akzeptieren jeweils persisted zu ihrer Entscheidung:
callbacks: {
onBeforeNodeCreate: async (ctx) => {
const values = await ctx.promptData({ fields: [{ key: 'value', label: 'Value', type: 'text' }] })
if (!values) return false
const saved = await myBackend.create(values)
return { accept: true, id: saved.uuid, data: values, persisted: true }
},
}Beim Rückgängigmachen wird kein kompensierender Schreibvorgang ausgeführt. Was mit einem solchen Eintrag geschieht, hängt also davon ab, in welche Richtung der Schreibvorgang verlaufen ist.
Eine beibehaltene Erstellung wird immer noch rückgängig gemacht. Die Arbeitsfläche verliert etwas, das das Backend behält, was durch einen erneuten Abruf rückgängig gemacht wird, sodass der Eintrag mit gespeichert markiert und nicht zurückgehalten wird und die Fußzeile ihn vor dem Klick zählt: Undoes 2 steps · 1 item saved upstream.
Eine dauerhafte Löschung ist versiegelt: Sie wird aufgelistet, weil sie Teil dessen ist, wie die Arbeitsfläche auf diese Weise entstanden ist, aber nie rückgängig gemacht wurde. Eine Wiederherstellung würde einen Knoten zurücksetzen, den das Backend nicht mehr hat, und nichts auf der Arbeitsfläche würde dies danach sagen. Eine Spanne, die eins enthält, geht darüber hinweg, anstatt dort anzuhalten, und sagt dies zuerst: Undoes 2 of 3 · 1 saved item kept.
const [newest] = graph.history.entries()
newest.persisted // the consumer wrote it through
newest.sealed // …and it was a deletion, so undo will not take it backDas Überspringen funktioniert, weil jeder Eintrag die spezifischen Elemente benennt, die er berührt, sodass es keine Statusunterschiede gibt, die die Reihenfolge beeinträchtigen könnten. Aus diesem Grund sind die sechs Arten auch geschlossen und ein Verbraucher kann keinen eigenen Eintrag aufzeichnen: Ein beliebiger Rückruf im Stapel könnte die Hälfte einer Spanne auslösen, und der Verlauf hätte keine Möglichkeit, dies herauszufinden.
Eine Einnahme zurücknehmen
Durch das Rückgängigmachen einer Aufnahme werden die Kandidaten erneut bereitgestellt, sodass die Zeilen wieder in den Überprüfungsbereich zurückkehren, aus dem sie stammen, der Bereich erneut geöffnet wird, wenn er durch die Aufnahme geschlossen wurde, frühere Ablehnungen weiterhin bestehen bleiben und kein Anbieter aufgerufen wird. Der letzte Teil ist der Punkt: Die Alternative besteht darin, zweitausend Korrelationen über eine ratenbegrenzte API erneut abzurufen, um einen Fehler zu beheben, der vor zwei Sekunden gemacht wurde.
Sie kommen ungeprüft und nicht noch markiert zurück. Beim Zurücklaufen geht es darum, ihn richtig zu durchlaufen, und nicht darum, die gleichen Zwölf mit einem Klick wieder zu landen.
Eine Ausführung, die ohne Überprüfung gelandet ist, wie z. B. ein Ein-Klick-Pivot aus dem Kontextmenü oder ein Anbieter, der autoIngest festlegt, kam aus keinem Bereich, sodass beim Rückgängigmachen nur die Knoten entfernt werden.
Dies geschieht nur, wenn die Aufnahme noch der neueste Eintrag ist. Wenn seitdem irgendetwas passiert ist, werden durch „Rückgängig“ die Knoten entfernt und es wird kein Bereich angezeigt: Ein Bereich, der sich bei späterer Arbeit wiederbelebt, wäre schlimmer als der erneute Abruf.
Was es enthält
Dreißig Einträge, die ältesten wurden geräumt. Die Räumung ist verständlich, da sie nicht symmetrisch ist: Sie verliert die Fähigkeit, einen alten Lauf zu wiederherstellen, nicht zu entfernen. Die Herkunft lebt von den Elementen selbst, daher erreicht graph.removeBySource(pivotId) immer noch einen Lauf, den die Geschichte vergessen hat.
Der Verlauf ist nur Sitzung. Es übersteht ein Nachladen nicht. Das Wiederherstellen eines früheren Zustands eines gesamten Diagramms ist eine andere Funktion und nicht diese.
Ich fahre damit
graph.history.entries() // what can be undone, newest first — the order the menu renders
graph.history.redoable() // what has been undone and can be redone, newest first
graph.history.canUndo()
graph.history.canRedo()
graph.history.undo() // the newest entry
graph.history.undo(entryId) // the span from the newest through that entry
graph.history.redo(entryId) // and back again
graph.history.preview(entryId) // what that span would do, before doing it
graph.history.on(history => …) // every change to either direction; returns a disposer
graph.history.clear() // forget everything; the canvas is left as it isDas id ** eines Pivot-Eintrags ist** sein runId, sodass ein PivotRunOutcome das Handle bereits in seine eigene Zeile trägt:
const outcome = await graph.pivots.run('correlations', graph.getSelectedNodes())
// …later
graph.history.undo(outcome.runId)preview(entryId, direction?) ist eine Notiz wert. Es fasst die Einträge nicht zusammen – es spielt die Spanne mit einer Kopie des Zustands des Diagramms ab, und zwar über denselben Code, den der Klick festschreibt, und vergleicht das Ergebnis:
const { entries, skipped, canvasOnly, nodes, edges, effect, forecast } = graph.history.preview(entryId)
// entries the span, newest first
// skipped the sealed ones inside it, which will be left alone
// canvasOnly the ones it reverses that the backend keeps a copy of
// nodes the elements it would touch that are on the canvas now
// effect { nodesRemoved, nodesRestored, edgesRemoved, edgesRestored, nodesHidden, nodesShown }
// forecast the same change as something to paint: { removing, hiding, touching, arriving }
graph.showForecast(forecast) // …and take it back down with graph.clearForecast()Da es eher gespielt als beschrieben wird, kommen die unangenehmen Fälle umsonst heraus: Ein durch ein späteres Einblenden aufgehobenes Verstecken wird auf Null gesetzt und sagt dies, und ein Knoten, für den ein zweiter Pivot ebenfalls bürgt, wird nicht als Verlassen gezählt.
Herkunft und warum das Löschen daran erinnert
Jeder Knoten und jede Kante trägt den Satz von Quellen, die dafür bürgen: 'seed' für geladene Daten, die Pivot-ID für alles, was er mitgebracht hat, und 'manual' für alles, was von Hand gezeichnet wurde. Eine Menge und kein Skalar, da zwei überlappende Pivotpunkte auf einem Knoten der Normalfall sind.
Bei einer Löschung muss daher die Garantie jedes Elements berücksichtigt werden, nicht nur seine Daten. Ohne das: Ein Durchlauf fügt zwölf hinzu, der Analyst löscht drei, macht die Löschung rückgängig und die drei kehren als vermisste Waisen zurück; Wenn man den Lauf danach rückgängig macht, bleiben sie auf der Arbeitsfläche zurück, ohne dass es eine Erklärung dafür gibt. Damit bleibt beim Rückgängigmachen des Löschvorgangs und beim anschließenden Ausführen genau das übrig, was zuvor vorhanden war.
Eine wissenswerte Konsequenz bei einem Upgrade: Ein manuell erstellter Knoten meldet ['manual'] von getSources(), nicht ['seed']. Das ist es, was graph.removeBySource('manual') zu handgezeichneten Werken macht und es ermöglicht, dass das Rückgängigmachen einer Schöpfung das Element nur dann entfernt, wenn nichts anderes mehr dafür bürgt.