Skip to main content

jellyflow_runtime/runtime/delete/
planner.rs

1use keyboard_types::Code as KeyCode;
2
3use crate::io::{NodeGraphInteractionState, NodeGraphViewState};
4use crate::rules::{DeletePlan, plan_delete_elements_with_policy};
5use jellyflow_core::core::Graph;
6use jellyflow_core::ops::{GraphOp, GraphTransaction};
7
8use super::types::{
9    DELETE_SELECTION_TRANSACTION_LABEL, DeleteElements, DeleteSelectionError, PreDeleteRequest,
10};
11
12/// Plans deletion for the current node/edge selection without mutating the graph.
13pub fn plan_delete_selection(
14    graph: &Graph,
15    view_state: &NodeGraphViewState,
16    interaction: &NodeGraphInteractionState,
17) -> DeletePlan {
18    let elements = delete_selection_elements(view_state);
19    plan_delete_elements(graph, interaction, &elements)
20}
21
22/// Plans deletion for a keyboard event when the configured delete key matches.
23pub fn plan_delete_selection_for_key(
24    graph: &Graph,
25    view_state: &NodeGraphViewState,
26    interaction: &NodeGraphInteractionState,
27    key: KeyCode,
28) -> Option<DeletePlan> {
29    let keyboard = interaction.keyboard_interaction();
30    if keyboard.disable_keyboard_a11y || !keyboard.delete_key.matches(key) {
31        return None;
32    }
33
34    Some(plan_delete_selection(graph, view_state, interaction))
35}
36
37/// Returns the direct node/edge selection before cascaded delete planning.
38pub fn delete_selection_elements(view_state: &NodeGraphViewState) -> DeleteElements {
39    DeleteElements::new(
40        view_state.selected_nodes.iter().copied(),
41        view_state.selected_edges.iter().copied(),
42    )
43}
44
45/// Plans deleting explicit node/edge ids through normal delete policy.
46pub fn plan_delete_elements(
47    graph: &Graph,
48    interaction: &NodeGraphInteractionState,
49    elements: &DeleteElements,
50) -> DeletePlan {
51    plan_delete_elements_with_policy(
52        graph,
53        elements.nodes().iter().copied(),
54        elements.edges().iter().copied(),
55        interaction,
56    )
57}
58
59/// Builds a pre-delete request for adapter-owned `onBeforeDelete` hooks.
60pub fn prepare_delete_selection(
61    graph: &Graph,
62    view_state: &NodeGraphViewState,
63    interaction: &NodeGraphInteractionState,
64) -> Result<Option<PreDeleteRequest>, DeleteSelectionError> {
65    let requested = delete_selection_elements(view_state);
66    prepare_delete_elements(graph, interaction, requested)
67}
68
69/// Builds a key-gated pre-delete request when the configured delete key matches.
70pub fn prepare_delete_selection_for_key(
71    graph: &Graph,
72    view_state: &NodeGraphViewState,
73    interaction: &NodeGraphInteractionState,
74    key: KeyCode,
75) -> Result<Option<PreDeleteRequest>, DeleteSelectionError> {
76    let keyboard = interaction.keyboard_interaction();
77    if keyboard.disable_keyboard_a11y || !keyboard.delete_key.matches(key) {
78        return Ok(None);
79    }
80
81    prepare_delete_selection(graph, view_state, interaction)
82}
83
84/// Builds a pre-delete request for explicit node/edge ids.
85pub fn prepare_delete_elements(
86    graph: &Graph,
87    interaction: &NodeGraphInteractionState,
88    requested: DeleteElements,
89) -> Result<Option<PreDeleteRequest>, DeleteSelectionError> {
90    let plan = plan_delete_elements(graph, interaction, &requested);
91    if plan.is_reject() {
92        return Err(DeleteSelectionError::Rejected {
93            diagnostics: plan.diagnostics,
94        });
95    }
96
97    let planned = delete_elements_from_plan(&plan);
98    if planned.is_empty() {
99        return Ok(None);
100    }
101
102    Ok(Some(PreDeleteRequest::new(requested, planned)))
103}
104
105/// Extracts the actual node/edge ids removed by a delete plan, including cascaded edges.
106pub fn delete_elements_from_plan(plan: &DeletePlan) -> DeleteElements {
107    let mut nodes = Vec::new();
108    let mut edges = Vec::new();
109
110    for op in plan.ops() {
111        match op {
112            GraphOp::RemoveNode {
113                id,
114                edges: removed_edges,
115                ..
116            } => {
117                nodes.push(*id);
118                edges.extend(removed_edges.iter().map(|(edge_id, _)| *edge_id));
119            }
120            GraphOp::RemovePort {
121                edges: removed_edges,
122                ..
123            } => {
124                edges.extend(removed_edges.iter().map(|(edge_id, _)| *edge_id));
125            }
126            GraphOp::RemoveEdge { id, .. } => edges.push(*id),
127            _ => {}
128        }
129    }
130
131    DeleteElements::new(nodes, edges)
132}
133
134/// Builds a labeled transaction for an accepted, non-empty delete plan.
135pub fn delete_selection_transaction(plan: &DeletePlan) -> Option<GraphTransaction> {
136    if !plan.is_accept() || plan.ops().is_empty() {
137        return None;
138    }
139
140    Some(
141        GraphTransaction::from_ops(plan.ops().iter().cloned())
142            .with_label(DELETE_SELECTION_TRANSACTION_LABEL),
143    )
144}
145
146/// Consumes a delete plan and returns a labeled transaction when it has accepted ops.
147pub fn delete_selection_transaction_from_plan(plan: DeletePlan) -> Option<GraphTransaction> {
148    if !plan.is_accept() || plan.ops().is_empty() {
149        return None;
150    }
151
152    Some(GraphTransaction::from_ops(plan.into_ops()).with_label(DELETE_SELECTION_TRANSACTION_LABEL))
153}