Pular para o conteúdo

Desfazer e histórico ​

graph.history é um registro de o que a tela contém e mostra — o que foi trazido, o que foi retirado, o que está escondido — e uma maneira de recuperar tudo isso.

Não é um registro de todas as mutações que a biblioteca pode realizar. Os dados de um nó pertencem ao seu back-end, portanto a biblioteca não oferecerá a reversão de um campo que não foi criado. O que ele pode falar é a composição, que é também o que um analista raciocina: doze nós chegaram, três foram, cinco estão ocultos.

Vocabulário, usado consistentemente abaixo

  • entrada: uma coisa reversível que aconteceu, como uma linha no menu
  • kind: qual dos seis tipos de entrada é
  • span: o bloco contíguo da entrada mais recente até aquela em que você clicou
  • persistiu: uma entrada que o consumidor escreveu em um backend
  • sealed: uma deletion persistente — listada, nunca revertida

Os seis tipos ​

Um conjunto fechado. Tudo o que a biblioteca registra é um destes, e nada mais é registrado.

GentilO que é issoComo é revertido
pivotUma ingestão — consulte Pivots e enriquecimentoA garantia da corrida é abandonada, e tudo o que nada mais garante vai
deleteUma exclusão de nós ou arestas iniciada pelo usuárioOs elementos voltam, procedência e tudo
visibilityUma ocultação ou exibição durável (queryEngine.excludeNode / includeNode)O outro
createUm nó ou aresta desenhado à mãoO elemento desaparece, a menos que algo mais ateste isso
removalUma chamada graph.removeBySource(source)O que sobrou volta, e o que ficou recebe a garantia da fonte de volta
groupUm grupo criado, renomeado ou removido manualmente (veja Simplificar)Os grupos feitos à mão voltam a ser como eram; nenhum nó é adicionado ou removido

Não registrado, propositalmente:

  • Edições de propriedade. Estado de back-end, como acima. onBeforeNodeEditCommit é onde você implementa o seu próprio.
  • Estado de filtro e legenda. Configurações com seus próprios Limpar em vez de ações. O formulário de filtro se aplica ao vivo a cada pressionamento de tecla, portanto, digitar domain enviaria seis entradas.
  • Notas. Texto de autoria, mais próximo de uma edição do que de uma composição gráfica.
  • graph.hideNode(). Ele será apagado na próxima vez que os filtros forem rederivados, portanto, uma entrada para ele reverteria algo que já havia sido revertido. excludeNode é a pele que sobrevive e aquela que fica gravada.
  • Abrir um grupo, retirar um nó, trocar uma regra Simplificar. Uma forma de olhar, como os filtros.

Mutações programáticas — graph.addNode(), graph.removeNode() — também não são entradas. Apenas um gesto é, mais graph.removeBySource(), que é como uma aplicação oferece ao analista uma ação “remover o que esta fonte trouxe”, e graph.simplify.groupNodes(), renameGroup() e ungroup(), as chamadas por trás dos gestos do Grupo.

Desfazer é contíguo ​

Mirar na linha três para baixo desfaz esses três, como um dataBatchChanged e um re-renderizado. Não há como reverter uma entrada antiga por si só: uma pilha cujo meio pode ser retirado não descreve mais como o gráfico ficou assim.

Para "aquele provedor retornou lixo, descarte-o e guarde o resto", use graph.removeBySource(source). Ele descarta a garantia de uma fonte e exclui apenas o que nada mais garante, de modo que um nó que uma segunda execução também encontre sobrevive, uma reivindicação mais leve. É uma operação forward, registrada como uma nova entrada em vez de retroceder.

Essa entrada é registrada sempre que a fonte atestou alguma coisa, mesmo quando cada elemento permaneceu porque outra fonte também atestou: a reivindicação desapareceu de qualquer maneira e a linha diz Nada saiu da tela. Desfazê-lo restaura a garantia de cada elemento exatamente como estava, de modo que a entrada de execução abaixo dele ainda esteja inteira e o próximo Desfazer recupera essa execução.

Refazer é estritamente linear e qualquer nova ação o esvazia. Uma corrida que foi desfeita e depois interrompida por trabalho posterior desapareceu; recuperá-lo significa executar o pivô novamente.

Da barra superior ​

Cada um dos dois botões no cabeçalho é um botão dividido: o ícone avança uma vez, o cursor abre o histórico. Ctrl+Z e Ctrl+Shift+Z (⌘Z e ⌘⇧Z em macOS) atingem as mesmas duas etapas sem o cabeçalho e passam para o desfazer de texto do próprio navegador enquanto um campo de texto está em foco. Ambos os botões ficam nos modos full e light, que é onde está o cabeçalho, e UI.history: { enabled: false } os leva, o menu suspenso e os dois atalhos para um consumidor que desfaz de seu próprio cromo - graph.history continua gravando de qualquer maneira.

O menu suspenso é uma lista de qualquer botão, o mais novo no topo, com uma linha now: as linhas acima foram desfeitas, as linhas abaixo são o que ainda pode ser desfeito. Clique em uma linha e a linha passa por ela, o que é o mesmo gesto em ambas as direções.

Cada linha carrega o ícone de seu tipo, seu rótulo e quantos passos um clique percorre. Esse número é aquele indicado no rodapé, portanto, a lista e o rodapé não podem discordar. O rodapé indica o efeito líquido exato antes de qualquer coisa ser confirmada. As teclas de seta apontam, Enter viaja, Escape fecha.

A tela também responde ​

Passar o mouse sobre uma linha prevê a mudança na tela e não altera mais nada: os elementos que o clique retiraria drenam onde estão, os nós que ele esconderia com eles e tudo o que traria de volta é delineado onde pousaria - um nó que o gráfico ainda não contém, desenhado como um anel na posição em que saiu. Essa última parte é muito do que um refazer tem a dizer, e a razão pela qual uma previsão não é um destaque: nada recua para destacar os elementos marcados, porque a questão é o que mudaria, e não quais elementos uma linha nomeia.

Os contornos não são suposições. Um desfazer indica onde cada elemento estava quando foi retirado da tela, e um refazer o coloca de volta lá – então o que a previsão desenha é onde o clique leva as coisas. Um pivô é reproduzido a partir dos dados brutos de seu provedor, que não carregam coordenadas, portanto, sem isso, uma ingestão voltaria espalhada em algum lugar novo.

preview() retorna a mesma coisa que os dados, em forecast, e graph.showForecast(…) / graph.clearForecast() pinta-os - portanto, a interface do histórico do próprio consumidor, ou qualquer outra coisa que queira perguntar "e se", obtém a linguagem da tela gratuitamente.

Operações gravadas em um back-end ​

Somente o consumidor pode saber que uma operação foi gravada em um back-end, portanto, somente o consumidor pode informar isso. onBeforeNodeCreate, onBeforeEdgeCreate e onBeforeDelete aceitam persisted em sua decisão:

js
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 }
    },
}

Desfazer não emite nenhuma gravação compensatória; portanto, o que ele faz com essa entrada depende do caminho que a gravação ocorreu.

Uma criação persistente ainda é revertida. A tela perde algo que o backend mantém, o que uma nova busca desfaz, então a entrada é marcada como saved em vez de retida e o rodapé conta antes do clique: Undoes 2 steps · 1 item saved upstream.

Uma deletion persistente é sealed: está listada, porque é parte de como a tela ficou assim, mas nunca revertida. Restaurá-lo colocaria de volta um nó que o back-end não possui mais, e nada na tela posteriormente indicaria isso. Um trecho contendo alguém passa por cima dele em vez de parar nele, e diz isso primeiro: Undoes 2 of 3 · 1 saved item kept.

js
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 back

Ignorar funciona porque cada entrada nomeia os elementos específicos tocados, portanto não há diferenças de estado para sair da ordem. É também por isso que os seis tipos são fechados e um consumidor não pode registrar uma entrada própria: um retorno de chamada arbitrário na pilha poderia ser lançado no meio de um período e o histórico não teria como saber.

Recuperando uma ingestão ​

Desfazendo uma ingestão re-estagia seus candidatos, para que as linhas retornem ao painel Revisão de onde vieram, o painel reabra se a ingestão o tiver fechado, as rejeições anteriores ainda serão mantidas e nenhum provedor será chamado. A questão é a última parte: a alternativa é buscar novamente duas mil correlações por meio de uma API com taxa limitada para corrigir um erro cometido há dois segundos.

Eles voltam sem triagem, em vez de ainda marcados. Retornar é para passar por isso corretamente, não para pousar novamente os mesmos doze com um clique.

Uma execução que chegou sem revisão, como um pivô de um clique no menu de contexto ou um provedor que define autoIngest, não veio de nenhum painel, portanto, desfazê-la apenas remove seus nós.

Isso acontece somente quando a ingestão ainda é a entrada mais recente. Se alguma coisa aconteceu desde então, desfazer remove os nós e nenhum painel aparece: um painel que se ressuscitasse em um trabalho posterior seria pior do que a nova busca.

O que isso contém ​

Trinta entradas, a mais antiga despejada. Vale a pena entender o despejo, porque não é simétrico: ele perde a capacidade de restaurar uma execução antiga, e não de remover uma. A proveniência vive dos próprios elementos, então graph.removeBySource(pivotId) ainda alcança uma corrida que a história esqueceu.

O histórico é somente sessão. Não sobrevive a uma recarga. Restaurar um estado anterior de um gráfico inteiro é um recurso diferente, e não este.

Dirigindo ​

js
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 is

O id ** de uma entrada dinâmica é ** é runId, então um PivotRunOutcome já carrega o identificador para sua própria linha:

js
const outcome = await graph.pivots.run('correlations', graph.getSelectedNodes())
// …later
graph.history.undo(outcome.runId)

preview(entryId, direction?) vale uma nota. Ele não resume as entradas - ele reproduz o intervalo em uma cópia do estado do gráfico, por meio do mesmo código que o clique confirma, e compara o resultado:

js
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()

Como é tocado em vez de descrito, os casos embaraçosos dão certo em vão: um hide cancelado por um unhide posterior é zerado e diz isso, e um nó que um segundo pivô também atesta não é contado como saída.

Proveniência e por que a exclusão lembra dela ​

Cada nó e borda carrega o conjunto de fontes que atestam isso: 'seed' para dados que foram carregados, um ID de pivô para qualquer coisa que ele trouxe e 'manual' para qualquer coisa desenhada à mão. Um conjunto em vez de um escalar, porque dois pivôs sobrepostos em um nó são o caso normal.

Uma exclusão, portanto, deve lembrar a comprovação de cada elemento, não apenas seus dados. Sem isso: uma corrida soma doze, o analista exclui três, desfaz a exclusão e os três voltam como órfãos desaparecidos; desfazer a execução depois os deixa na tela sem nada para explicá-los. Com ele, desfaz-se a exclusão e depois a execução deixa exatamente o que estava lá antes.

Uma consequência que vale a pena saber ao atualizar: um nó criado manualmente reporta ['manual'] de getSources(), não ['seed']. É isso que faz graph.removeBySource('manual') chegar ao trabalho desenhado à mão, e o que permite desfazer uma criação e remover o elemento apenas quando nada mais o atesta.