Behalten Sie das Löschen, Bearbeiten und Erstellen bei
Wenn es sich bei dem Diagramm um eine Ansicht persistenter Datensätze handelt, kann kein Schreibvorgang optimistisch sein. Drei Before-Hooks sorgen dafür, dass jeder vom Benutzer initiierte Vorschlag einen Vorschlag schreibt, über den sich der Verbraucher entscheidet, sodass Canvas und Datenbank niemals unterschiedlicher Meinung sein können:
onBeforeDelete– wird für jedes Löschangebot (die Massenzeile der Seitenleiste, die Knoten-/Kanten-/Notiz-Kontextmenüeinträge) ausgelöst, sobald der gesamte Zielsatz aufgelöst ist, einschließlichcascadingEdges: die Beziehungen, die durch das Entfernen eines Knotens zerstört würden. Geben Siefalsezurück, um ein Veto einzulegen, oder{ accept: true, nodes, edges }, um nur das zu behalten, was der Server tatsächlich gelöscht hat.ctx.confirm()ist das integrierte Bestätigungsmodal.onBeforeNodeCreate– wird ausgelöst für Erstellen ▸ Knoten hinzufügen und Knoten hier hinzufügen im Canvas-Menü mit dem Diagrammbereichpositionund einempromptData()-Formular. Was Sie zurückgeben, wird in die Grafik eingegeben.onBeforeEdgeEditCommit– wird ausgelöst, wenn eine Kantenbearbeitung festgeschrieben wird (Rechtsklick auf eine Kante ▸ Kante bearbeiten). Wenn Sie es ablehnen, behält der Edge seine Daten.
Alle drei können asynchron sein und eine zweite Geste wird ignoriert, während eine aussteht. Programmatische graph.removeNode() / addNode() werden nie gegatet, sodass Ihr eigener Code nicht durch Ihren eigenen Hook blockiert wird.
Versuchen Sie es: Wählen Sie Knoten aus und klicken Sie in der Seitenleiste auf Löschen (der Eintrag veröffentlicht wird abgelehnt), klicken Sie mit der rechten Maustaste auf eine Kante, um ihre Beziehung zu ändern, oder verwenden Sie Erstellen ▸ Knoten hinzufügen. Legen Sie editors.deletion.enabled: false fest, um die Löschmöglichkeiten vollständig zu entfernen, anstatt jeden Klick zu blockieren.
const options = {
// Label each attribute with its value, so an edit's effect is visible.
render: {
defaultNodeStyle: {
size: 16,
textColor: '#334155',
text: (node) => node.getData()?.label,
textVerticalShift: -1.6
}
},
UI: {
mode: 'full',
// Off: `full` mode brings a minimap and the data dock, and this card is about
// neither (see the Minimap and Data table cards for those).
minimap: false,
table: false,
// Give the edge editor a relationship dropdown instead of a free-text field.
editors: {
edgeEditor: {
fields: [{
key: 'label',
label: 'Relationship',
type: 'select',
options: RELATIONSHIPS.map((value) => ({ value, label: value }))
}]
}
}
},
callbacks: {
// Nothing leaves the model until the server agrees. The library resolves the
// whole target set first — including `cascadingEdges`, the relationships that
// removing a node would take with it — so you can persist the real consequence.
onBeforeDelete: async ({ nodes, edges, cascadingEdges, origin, confirm }) => {
const links = edges.length + cascadingEdges.length
const ok = await confirm({
title: 'Delete from event?',
body: `${nodes.length} attribute(s) and ${links} relationship(s) will be `
+ `deleted server-side. (asked from the ${origin})`,
confirmLabel: 'Delete'
})
if (!ok) return false
graph.notifier.info('Saving…', 'POST /events/delete')
await wait(700) // a backend call that could fail
// The server refused the published record: keep exactly what it deleted.
const refused = nodes.filter((node) => node.getData().published)
if (refused.length) {
graph.notifier.warning('Kept', `${refused[0].getData().label} is published — not deleted.`)
}
return { accept: true, nodes: nodes.filter((node) => !node.getData().published) }
},
// Create ▸ Add node, or the canvas menu's "Add Node Here". `promptData` opens a
// form; the id/data you return is what enters the graph.
onBeforeNodeCreate: async ({ position, promptData }) => {
const values = await promptData({
title: 'New attribute',
fields: [
{ key: 'label', label: 'Value', type: 'text' },
{
key: 'type', label: 'Type', type: 'select', defaultValue: 'ip-src',
options: ['ip-src', 'domain', 'md5', 'url'].map((v) => ({ value: v, label: v }))
}
]
})
if (!values) return false // cancelled
graph.notifier.info('Saving…', 'POST /attributes/add')
await wait(500)
graph.notifier.success('Created', `${values.label} at ${Math.round(position.x)},${Math.round(position.y)}`)
return { accept: true, id: `attr-${values.label}`, data: values }
},
// Right-click an edge ▸ Edit Edge. The commit is a proposal too: refuse it and
// the edge keeps its data, with the modal still open to correct.
onBeforeEdgeEditCommit: async ({ previousData, nextData }) => {
if (!RELATIONSHIPS.includes(nextData.label)) {
graph.notifier.warning('Rejected', `"${nextData.label}" is not a known relationship.`)
return false
}
graph.notifier.info('Saving…', 'PUT /object_references')
await wait(500)
graph.notifier.success('Updated', `${previousData.label} → ${nextData.label}`)
return true
}
}
}const data = {
nodes: [
{ id: 'ip-8888', data: { label: '8.8.8.8', type: 'ip-src' } },
{ id: 'dom-evil', data: { label: 'evil.example.com', type: 'domain' } },
{ id: 'md5', data: { label: 'd41d8cd9…f8427e', type: 'md5' } },
// The server refuses to delete this one — a published record.
{ id: 'file-obj', data: { label: 'invoice.doc', type: 'file', published: true } }
],
edges: [
{ id: 'r1', from: 'file-obj', to: 'md5', data: { label: 'contains' } },
{ id: 'r2', from: 'dom-evil', to: 'ip-8888', data: { label: 'resolves-to' } }
]
}