Annuler et historique
graph.history est un enregistrement de ce que la toile contient et montre — ce qui a été apporté, ce qui a été retiré, ce qui est caché — et un moyen de récupérer tout cela.
Il ne s’agit pas d’un enregistrement de toutes les mutations que la bibliothèque peut effectuer. Les données d'un nœud appartiennent à votre backend, la bibliothèque ne proposera donc pas de restaurer un champ qu'elle n'a pas créé. Ce dont il peut parler, c'est de la composition, et c'est aussi ce sur quoi raisonne un analyste : douze nœuds sont arrivés, trois sont partis, cinq sont cachés.
Vocabulaire, utilisé systématiquement ci-dessous
- entry : une chose réversible qui s'est produite, comme une ligne dans le menu
- kind : de quel type d'entrée il s'agit parmi les six
- span : le bloc contigu depuis l'entrée la plus récente jusqu'à celle sur laquelle vous avez cliqué
- persisted : une entrée que le consommateur a écrite via un backend
- sealed : une deletion persistante — répertoriée, jamais annulée
Les six sortes
Un ensemble fermé. Tout ce que la bibliothèque enregistre en fait partie, et rien d'autre n'est enregistré.
| Genre | Qu'est-ce que c'est | Comment c'est inversé |
|---|---|---|
pivot | Une seule acquisition — voir Pivots et enrichissement | Le garant de la course est abandonné, et tout ce dont rien d'autre ne se porte garant disparaît |
delete | Une suppression de nœuds ou de bords initiée par l'utilisateur | Les éléments reviennent, la provenance et tout |
visibility | Un masquage ou un affichage durable (queryEngine.excludeNode / includeNode) | L'autre |
create | Un nœud ou une arête dessinée à la main | L'élément disparaît, à moins que quelque chose d'autre ne s'en porte garant |
removal | Un appel graph.removeBySource(source) | Ce qui reste revient, et ce qui reste obtient le soutien de la source |
group | Un groupe créé, renommé ou supprimé à la main (voir Simplifier) | Les groupes fabriqués à la main redeviennent tels qu'ils étaient ; aucun nœud n'est ajouté ou supprimé |
Non enregistré, volontairement :
- Modifications de propriétés. État du backend, comme ci-dessus.
onBeforeNodeEditCommitest l'endroit où vous implémentez le vôtre. - État du filtre et de la légende. Paramètres avec leur propre clair plutôt que des actes. Le formulaire de filtre s'applique en direct à chaque frappe, donc taper
domainpousserait six entrées. - Notes. Texte rédigé, plus proche d'une édition que d'une composition graphique.
graph.hideNode(). Il est effacé la prochaine fois que les filtres sont redirigés, donc une entrée pour cela annulerait quelque chose qui s'était déjà inversé.excludeNodeest la peau qui survit et celle qui est enregistrée.- Ouvrir un groupe, retirer un nœud, changer de règle Simplifier. Une façon de regarder, comme les filtres.
Les mutations programmatiques — graph.addNode(), graph.removeNode() — ne sont pas non plus des entrées. Seul un geste l'est, plus graph.removeBySource(), qui est ainsi qu'une application propose à l'analyste une action "supprimer ce que cette source a apporté", et graph.simplify.groupNodes(), renameGroup() et ungroup(), les appels derrière les gestes du Groupe.
L'annulation est contiguë
Viser la troisième rangée vers le bas annule ces trois, comme un dataBatchChanged et un nouveau rendu. Il n'y a aucun moyen d'inverser une ancienne entrée à elle seule : une pile dont le milieu peut être retiré ne décrit plus comment le graphique est arrivé ainsi.
Pour "ce fournisseur a renvoyé des fichiers indésirables, supprimez-les et conservez le reste", utilisez graph.removeBySource(source). Il supprime la garantie d'une source et supprime uniquement ce dont rien d'autre ne garantit, de sorte qu'un nœud qu'une deuxième exécution a également trouvé survit, une réclamation plus légère. Il s'agit d'une opération forward, enregistrée comme une nouvelle entrée plutôt que comme un retour en arrière.
Cette entrée est enregistrée chaque fois que la source s'est portée garante de quelque chose, même lorsque chaque élément est resté parce qu'une autre source s'en porte également garante : la réclamation a disparu dans un sens ou dans l'autre, et la ligne indique Rien n'a quitté le canevas. L'annulation restaure le garant de chaque élément exactement tel qu'il était, de sorte que l'entrée d'exécution en dessous est toujours entière et la prochaine annulation reprend cette exécution.
Refaire est strictement linéaire et toute nouvelle action le vide. Une exécution qui a été annulée puis bloquée par des travaux ultérieurs a disparu ; le récupérer signifie exécuter à nouveau le pivot.
De la barre supérieure
Chacun des deux boutons de l'en-tête est un bouton partagé : l'icône avance une fois, le curseur ouvre l'historique. Ctrl+Z et Ctrl+Shift+Z (⌘Z et ⌘⇧Z sur macOS) atteignent les deux mêmes étapes sans l'en-tête et passent à l'annulation du texte du navigateur pendant qu'un champ de texte a le focus. Les deux boutons sont en mode full et light, où se trouve l'en-tête, et UI.history: { enabled: false } les prend, la liste déroulante et les deux raccourcis sont éloignés pour un utilisateur qui annule à partir de son propre chrome - graph.history continue d'enregistrer dans les deux sens.
La liste déroulante est une liste de chaque bouton, la plus récente en haut, avec une ligne now à travers elle : les lignes au-dessus ont été annulées, les lignes en dessous sont ce qui peut encore être annulé. Cliquez sur une ligne et la ligne la dépasse, ce qui constitue le même geste dans les deux sens.
Chaque ligne porte l'icône de son genre, son étiquette etcombien de pas parcourt un clic dessus. Ce numéro est celui indiqué dans le pied de page, donc la liste et le pied de page ne peuvent pas être en désaccord. Le pied de page indique l’effet net exact avant que quoi que ce soit ne soit commis. Les touches fléchées visent, entrez les voyages, l'évasion se ferme.
La toile répond aussi
Le survol d'une ligne prédit le changement sur le canevas et ne change rien d'autre : les éléments que le clic retirerait s'écouleraient là où ils se trouvent, les nœuds qu'il cacherait avec eux, et tout ce qu'il ramènerait est indiqué où il atterrirait — un nœud que le graphique ne tient pas encore, dessiné comme un anneau à la position qu'il a laissée. Cette dernière partie représente l'essentiel de ce qu'une refonte a à dire, et la raison pour laquelle une prévision n'est pas un point culminant : rien ne recule pour faire ressortir les éléments marqués, car la question est de savoir ce qui changerait, et non quels éléments une ligne nomme.
Les contours ne sont pas des suppositions. Une annulation note l'endroit où se trouvait chaque élément lorsqu'il l'a retiré du canevas, et une restauration le remet là - donc ce que la prévision dessine est l'endroit où le clic amène les choses. Un pivot relit à partir des données brutes de son fournisseur, qui ne contiennent aucune coordonnée, donc sans cela, une ingestion reviendrait dispersée dans un nouvel endroit.
preview() renvoie la même chose que les données, sous forecast, et graph.showForecast(…) / graph.clearForecast() les peignent — ainsi la propre interface utilisateur de l'historique d'un consommateur, ou tout autre élément qui souhaite demander « et si », obtient le langage du canevas gratuitement.
Opérations écrites via un backend
Seul le consommateur peut savoir qu'une opération a été écrite sur un backend, donc seul le consommateur peut le dire. onBeforeNodeCreate, onBeforeEdgeCreate et onBeforeDelete acceptent chacun persisted sur leur décision :
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 }
},
}L'annulation ne génère aucune écriture compensatoire, donc ce qu'elle fait avec une telle entrée dépend de la direction dans laquelle l'écriture s'est déroulée.
Une création persistante s'inverse toujours. Le canevas perd quelque chose que le backend conserve, qu'une nouvelle récupération annule, donc l'entrée est marquée saved plutôt que retenue et le pied de page le compte avant le clic : Undoes 2 steps · 1 item saved upstream.
Une deletion persistante est sealed : elle est répertoriée, car elle fait partie de la façon dont le canevas est arrivé de cette façon, mais n'a jamais été inversée. Le restaurer reviendrait à restaurer un nœud que le backend n'a plus, et rien sur le canevas par la suite ne le dirait. Un travée en contenant un passe dessus plutôt que de s'y arrêter, et le dit d'abord : 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 backSauter fonctionne car chaque entrée nomme les éléments spécifiques qu'elle a touchés, il n'y a donc pas de différences d'état à perturber. C'est aussi pour cela que les six types sont fermés et qu'un consommateur ne peut pas enregistrer sa propre entrée : un rappel arbitraire dans la pile pourrait se produire à mi-chemin d'une période, et l'historique n'aurait aucun moyen de le savoir.
Reprendre une ingestion
L'annulation d'une ingestion re-stage ses candidats, de sorte que les lignes retournent dans le volet Révision d'où elles proviennent, le volet se rouvre si l'ingestion l'avait fermé, les rejets antérieurs sont toujours valables et aucun fournisseur n'est appelé. C'est cette dernière partie qui est importante : l'alternative consiste à récupérer deux mille corrélations via une API à débit limité pour corriger une erreur commise il y a deux secondes.
Ils reviennent sans tri plutôt que toujours marqués. Revenir en arrière, c'est pour le parcourir correctement, pas pour réatterrir les mêmes douze en un seul clic.
Une exécution qui a atterri sans examen, comme un pivot en un clic dans le menu contextuel ou un fournisseur qui définit autoIngest, ne provenait d'aucun volet, donc son annulation ne fait que supprimer ses nœuds.
Cela se produit uniquement lorsque l'ingestion est toujours l'entrée la plus récente. Si quelque chose s'est produit depuis, l'annulation supprime les nœuds et aucun volet n'apparaît : un volet ressuscitant au cours d'un travail ultérieur serait pire que la récupération.
Ce qu'il contient
Trente entrées, la plus ancienne expulsée. L'expulsion mérite d'être comprise, car elle n'est pas symétrique : elle perd la capacité de restaurer une ancienne exécution, et non de supprimer une. La provenance vit des éléments eux-mêmes, donc graph.removeBySource(pivotId) atteint toujours une course que l'histoire a oubliée.
L'historique est session uniquement. Il ne survit pas à un rechargement. La restauration d'un état antérieur d'un graphe entier est une fonctionnalité différente, et non celle-ci.
Le conduire
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 isLe id is d'une entrée pivot est son runId, donc un PivotRunOutcome porte déjà le handle vers sa propre ligne :
const outcome = await graph.pivots.run('correlations', graph.getSelectedNodes())
// …later
graph.history.undo(outcome.runId)preview(entryId, direction?) vaut une note. Il ne résume pas les entrées — il rejoue l'étendue par rapport à une copie de l'état du graphique, via le même code que le clic valide, et compare le résultat :
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()Parce qu'il est joué plutôt que décrit, les cas délicats ne sont pas justifiés : un hide annulé par un affichage ultérieur revient à zéro et le dit, et un nœud dont un deuxième pivot se porte également garant n'est pas compté comme partant.
Provenance et pourquoi la suppression s'en souvient
Chaque nœud et bord contient l'ensemble des sources qui en témoignent : 'seed' pour les données qui ont été chargées, l'identifiant d'un pivot pour tout ce qu'il a apporté et 'manual' pour tout ce qui a été dessiné à la main. Un ensemble plutôt qu'un scalaire, car deux pivots se chevauchant sur un nœud est le cas normal.
Une suppression doit donc mémoriser le garant de chaque élément, pas seulement ses données. Sans cela : une analyse en ajoute douze, l'analyste en supprime trois, annule la suppression, et les trois reviennent comme orphelins disparus ; annuler la course par la suite, puis les laisser sur la toile sans rien pour les expliquer. Avec cela, l'annulation de la suppression, puis l'exécution laisse exactement ce qui existait auparavant.
Une conséquence à connaître lors de la mise à niveau : un nœud créé manuellement rapporte ['manual'] à partir de getSources(), et non ['seed']. C'est ce qui fait que graph.removeBySource('manual') atteint un travail dessiné à la main, et ce qui permet d'annuler une création de supprimer l'élément uniquement lorsque rien d'autre ne le garantit encore.