590 lines
20 KiB
Rust
590 lines
20 KiB
Rust
use super::focus_state::TreeShellFocusState;
|
|||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::collections::BTreeSet;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
pub struct PageTreeRuntimeEnvironment {
|
||
|
|
pub visible_node_ids: Vec<String>,
|
||
|
|
pub expandable_node_ids: BTreeSet<String>,
|
||
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
|
|
pub rows: Vec<PageTreeRuntimeRow>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
pub struct PageTreeRuntimeRow {
|
||
|
|
pub node_id: String,
|
||
|
|
pub parent_node_id: Option<String>,
|
||
|
|
pub position: i64,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
pub struct PageTreeRuntimeState {
|
||
|
|
pub focused_id: Option<String>,
|
||
|
|
pub expanded_ids: BTreeSet<String>,
|
||
|
|
pub drop_feedback: Option<PageTreeDropFeedback>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
|
|
pub struct PageTreeRuntimeTransition {
|
||
|
|
pub state: PageTreeRuntimeState,
|
||
|
|
pub outputs: BTreeSet<PageTreeRuntimeOutput>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
#[serde(
|
||
|
|
tag = "kind",
|
||
|
|
rename_all = "camelCase",
|
||
|
|
rename_all_fields = "camelCase"
|
||
|
|
)]
|
||
|
|
pub enum PageTreeRuntimeAction {
|
||
|
|
Normalize,
|
||
|
|
Focus {
|
||
|
|
node_id: String,
|
||
|
|
},
|
||
|
|
MoveNext,
|
||
|
|
MovePrevious,
|
||
|
|
MoveHome,
|
||
|
|
MoveEnd,
|
||
|
|
Expand {
|
||
|
|
node_id: String,
|
||
|
|
},
|
||
|
|
Collapse {
|
||
|
|
node_id: String,
|
||
|
|
},
|
||
|
|
Toggle {
|
||
|
|
node_id: String,
|
||
|
|
},
|
||
|
|
OpenFocused,
|
||
|
|
ContextMenuFocused,
|
||
|
|
DispatchCreate {
|
||
|
|
parent_node_id: Option<String>,
|
||
|
|
},
|
||
|
|
DispatchRename {
|
||
|
|
node_id: String,
|
||
|
|
title: String,
|
||
|
|
},
|
||
|
|
UpdateDropFeedback {
|
||
|
|
feedback: Option<PageTreeDropFeedback>,
|
||
|
|
},
|
||
|
|
UpdateDropFeedbackForTarget {
|
||
|
|
source_node_id: String,
|
||
|
|
target_node_id: String,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
},
|
||
|
|
DispatchMove {
|
||
|
|
source_node_id: String,
|
||
|
|
target_parent_id: Option<String>,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
},
|
||
|
|
DispatchMoveToTarget {
|
||
|
|
source_node_id: String,
|
||
|
|
target_node_id: String,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||
|
|
pub enum PageTreeRuntimeOutput {
|
||
|
|
DomPatch,
|
||
|
|
Intent(PageTreeIntentEvent),
|
||
|
|
CommandDispatch(PageTreeCommandDispatchEvent),
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||
|
|
pub enum PageTreeIntentEvent {
|
||
|
|
Open { node_id: String },
|
||
|
|
ContextMenu { node_id: String },
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||
|
|
pub enum PageTreeCommandDispatchEvent {
|
||
|
|
CreateNode {
|
||
|
|
command_name: &'static str,
|
||
|
|
parent_node_id: Option<String>,
|
||
|
|
},
|
||
|
|
RenameNode {
|
||
|
|
command_name: &'static str,
|
||
|
|
node_id: String,
|
||
|
|
title: String,
|
||
|
|
},
|
||
|
|
MoveSubtree {
|
||
|
|
command_name: &'static str,
|
||
|
|
source_node_id: String,
|
||
|
|
target_node_id: Option<String>,
|
||
|
|
target_parent_id: Option<String>,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
sort_order: Option<i64>,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
pub struct PageTreeDropFeedback {
|
||
|
|
pub source_node_id: String,
|
||
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
|
|
pub target_node_id: Option<String>,
|
||
|
|
pub target_parent_id: Option<String>,
|
||
|
|
pub position: PageTreeDropPosition,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
pub enum PageTreeDropPosition {
|
||
|
|
Before,
|
||
|
|
Inside,
|
||
|
|
After,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PageTreeRuntimeState {
|
||
|
|
pub fn reduce(
|
||
|
|
&self,
|
||
|
|
env: &PageTreeRuntimeEnvironment,
|
||
|
|
action: PageTreeRuntimeAction,
|
||
|
|
) -> PageTreeRuntimeTransition {
|
||
|
|
match action {
|
||
|
|
PageTreeRuntimeAction::Normalize => self.with_focus(
|
||
|
|
TreeShellFocusState {
|
||
|
|
focused_id: self.focused_id.clone(),
|
||
|
|
}
|
||
|
|
.normalize(&env.visible_node_ids)
|
||
|
|
.focused_id,
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::Focus { node_id } => {
|
||
|
|
let focused_id = env
|
||
|
|
.visible_node_ids
|
||
|
|
.iter()
|
||
|
|
.any(|visible_id| visible_id == &node_id)
|
||
|
|
.then_some(node_id);
|
||
|
|
self.with_focus(focused_id)
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::MoveNext => self.with_focus(
|
||
|
|
TreeShellFocusState {
|
||
|
|
focused_id: self.focused_id.clone(),
|
||
|
|
}
|
||
|
|
.move_next(&env.visible_node_ids)
|
||
|
|
.focused_id,
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::MovePrevious => self.with_focus(
|
||
|
|
TreeShellFocusState {
|
||
|
|
focused_id: self.focused_id.clone(),
|
||
|
|
}
|
||
|
|
.move_previous(&env.visible_node_ids)
|
||
|
|
.focused_id,
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::MoveHome => self.with_focus(
|
||
|
|
TreeShellFocusState {
|
||
|
|
focused_id: self.focused_id.clone(),
|
||
|
|
}
|
||
|
|
.move_home(&env.visible_node_ids)
|
||
|
|
.focused_id,
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::MoveEnd => self.with_focus(
|
||
|
|
TreeShellFocusState {
|
||
|
|
focused_id: self.focused_id.clone(),
|
||
|
|
}
|
||
|
|
.move_end(&env.visible_node_ids)
|
||
|
|
.focused_id,
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::Expand { node_id } => self.with_expansion(env, node_id, true),
|
||
|
|
PageTreeRuntimeAction::Collapse { node_id } => self.with_expansion(env, node_id, false),
|
||
|
|
PageTreeRuntimeAction::Toggle { node_id } => {
|
||
|
|
let expanded = !self.expanded_ids.contains(&node_id);
|
||
|
|
self.with_expansion(env, node_id, expanded)
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::OpenFocused => {
|
||
|
|
self.with_focused_intent(PageTreeIntentEvent::Open {
|
||
|
|
node_id: self.focused_id.clone().unwrap_or_default(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::ContextMenuFocused => {
|
||
|
|
self.with_focused_intent(PageTreeIntentEvent::ContextMenu {
|
||
|
|
node_id: self.focused_id.clone().unwrap_or_default(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::DispatchCreate { parent_node_id } => transition(
|
||
|
|
self.clone(),
|
||
|
|
[PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::CreateNode {
|
||
|
|
command_name: "tree.node.create",
|
||
|
|
parent_node_id,
|
||
|
|
},
|
||
|
|
)],
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::DispatchRename { node_id, title } => {
|
||
|
|
let title = title.trim().to_string();
|
||
|
|
if node_id.trim().is_empty() || title.is_empty() {
|
||
|
|
return transition(self.clone(), []);
|
||
|
|
}
|
||
|
|
transition(
|
||
|
|
self.clone(),
|
||
|
|
[PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::RenameNode {
|
||
|
|
command_name: "tree.node.rename",
|
||
|
|
node_id,
|
||
|
|
title,
|
||
|
|
},
|
||
|
|
)],
|
||
|
|
)
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::UpdateDropFeedback { feedback } => {
|
||
|
|
let mut state = self.clone();
|
||
|
|
state.drop_feedback = feedback;
|
||
|
|
transition(state, [PageTreeRuntimeOutput::DomPatch])
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::UpdateDropFeedbackForTarget {
|
||
|
|
source_node_id,
|
||
|
|
target_node_id,
|
||
|
|
position,
|
||
|
|
} => {
|
||
|
|
let mut state = self.clone();
|
||
|
|
state.drop_feedback =
|
||
|
|
resolve_drop_feedback(env, &source_node_id, &target_node_id, position);
|
||
|
|
transition(state, [PageTreeRuntimeOutput::DomPatch])
|
||
|
|
}
|
||
|
|
PageTreeRuntimeAction::DispatchMove {
|
||
|
|
source_node_id,
|
||
|
|
target_parent_id,
|
||
|
|
position,
|
||
|
|
} => transition(
|
||
|
|
self.clone(),
|
||
|
|
[PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::MoveSubtree {
|
||
|
|
command_name: "tree.subtree.move",
|
||
|
|
source_node_id,
|
||
|
|
target_node_id: None,
|
||
|
|
target_parent_id,
|
||
|
|
position,
|
||
|
|
sort_order: None,
|
||
|
|
},
|
||
|
|
)],
|
||
|
|
),
|
||
|
|
PageTreeRuntimeAction::DispatchMoveToTarget {
|
||
|
|
source_node_id,
|
||
|
|
target_node_id,
|
||
|
|
position,
|
||
|
|
} => {
|
||
|
|
let Some(resolved_drop) =
|
||
|
|
resolve_drop_target(env, &source_node_id, &target_node_id, position)
|
||
|
|
else {
|
||
|
|
return transition(self.clone(), []);
|
||
|
|
};
|
||
|
|
transition(
|
||
|
|
self.clone(),
|
||
|
|
[PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::MoveSubtree {
|
||
|
|
command_name: "tree.subtree.move",
|
||
|
|
source_node_id,
|
||
|
|
target_node_id: Some(target_node_id),
|
||
|
|
target_parent_id: resolved_drop.target_parent_id,
|
||
|
|
position,
|
||
|
|
sort_order: Some(resolved_drop.sort_order),
|
||
|
|
},
|
||
|
|
)],
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn with_focus(&self, focused_id: Option<String>) -> PageTreeRuntimeTransition {
|
||
|
|
let mut state = self.clone();
|
||
|
|
state.focused_id = focused_id;
|
||
|
|
transition(state, [PageTreeRuntimeOutput::DomPatch])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn with_expansion(
|
||
|
|
&self,
|
||
|
|
env: &PageTreeRuntimeEnvironment,
|
||
|
|
node_id: String,
|
||
|
|
expanded: bool,
|
||
|
|
) -> PageTreeRuntimeTransition {
|
||
|
|
let mut state = self.clone();
|
||
|
|
if env.expandable_node_ids.contains(&node_id) {
|
||
|
|
if expanded {
|
||
|
|
state.expanded_ids.insert(node_id);
|
||
|
|
} else {
|
||
|
|
state.expanded_ids.remove(&node_id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
transition(state, [PageTreeRuntimeOutput::DomPatch])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn with_focused_intent(&self, intent: PageTreeIntentEvent) -> PageTreeRuntimeTransition {
|
||
|
|
let has_focus = !match &intent {
|
||
|
|
PageTreeIntentEvent::Open { node_id } => node_id,
|
||
|
|
PageTreeIntentEvent::ContextMenu { node_id } => node_id,
|
||
|
|
}
|
||
|
|
.is_empty();
|
||
|
|
if has_focus {
|
||
|
|
transition(self.clone(), [PageTreeRuntimeOutput::Intent(intent)])
|
||
|
|
} else {
|
||
|
|
transition(self.clone(), [])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
|
|
struct ResolvedPageDropTarget {
|
||
|
|
target_parent_id: Option<String>,
|
||
|
|
sort_order: i64,
|
||
|
|
}
|
||
|
|
|
||
|
|
fn resolve_drop_feedback(
|
||
|
|
env: &PageTreeRuntimeEnvironment,
|
||
|
|
source_node_id: &str,
|
||
|
|
target_node_id: &str,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
) -> Option<PageTreeDropFeedback> {
|
||
|
|
let resolved = resolve_drop_target(env, source_node_id, target_node_id, position)?;
|
||
|
|
Some(PageTreeDropFeedback {
|
||
|
|
source_node_id: source_node_id.to_string(),
|
||
|
|
target_node_id: Some(target_node_id.to_string()),
|
||
|
|
target_parent_id: resolved.target_parent_id,
|
||
|
|
position,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn resolve_drop_target(
|
||
|
|
env: &PageTreeRuntimeEnvironment,
|
||
|
|
source_node_id: &str,
|
||
|
|
target_node_id: &str,
|
||
|
|
position: PageTreeDropPosition,
|
||
|
|
) -> Option<ResolvedPageDropTarget> {
|
||
|
|
let source_node_id = source_node_id.trim();
|
||
|
|
let target_node_id = target_node_id.trim();
|
||
|
|
if source_node_id.is_empty() || target_node_id.is_empty() || source_node_id == target_node_id {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let source_row = env.rows.iter().find(|row| row.node_id == source_node_id)?;
|
||
|
|
let target_row = env.rows.iter().find(|row| row.node_id == target_node_id)?;
|
||
|
|
if source_row.parent_node_id != target_row.parent_node_id {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let mut siblings = env
|
||
|
|
.rows
|
||
|
|
.iter()
|
||
|
|
.filter(|row| row.parent_node_id == target_row.parent_node_id)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
siblings.sort_by(|left, right| {
|
||
|
|
left.position
|
||
|
|
.cmp(&right.position)
|
||
|
|
.then_with(|| left.node_id.cmp(&right.node_id))
|
||
|
|
});
|
||
|
|
let target_index = siblings
|
||
|
|
.iter()
|
||
|
|
.position(|row| row.node_id == target_row.node_id)? as i64;
|
||
|
|
let sort_order = match position {
|
||
|
|
PageTreeDropPosition::Before | PageTreeDropPosition::Inside => target_index,
|
||
|
|
PageTreeDropPosition::After => target_index + 1,
|
||
|
|
};
|
||
|
|
Some(ResolvedPageDropTarget {
|
||
|
|
target_parent_id: target_row.parent_node_id.clone(),
|
||
|
|
sort_order,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn transition<const N: usize>(
|
||
|
|
state: PageTreeRuntimeState,
|
||
|
|
outputs: [PageTreeRuntimeOutput; N],
|
||
|
|
) -> PageTreeRuntimeTransition {
|
||
|
|
PageTreeRuntimeTransition {
|
||
|
|
state,
|
||
|
|
outputs: outputs.into_iter().collect(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::{
|
||
|
|
PageTreeCommandDispatchEvent, PageTreeDropFeedback, PageTreeDropPosition,
|
||
|
|
PageTreeIntentEvent, PageTreeRuntimeAction, PageTreeRuntimeEnvironment, PageTreeRuntimeRow,
|
||
|
|
PageTreeRuntimeOutput, PageTreeRuntimeState,
|
||
|
|
};
|
||
|
|
|
||
|
|
fn ids(values: &[&str]) -> Vec<String> {
|
||
|
|
values.iter().map(|value| (*value).to_string()).collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn page_tree_runtime_reducer_normalizes_focus_expansion_intents_and_drag_move() {
|
||
|
|
let env = PageTreeRuntimeEnvironment {
|
||
|
|
visible_node_ids: ids(&["doc:root", "doc:child", "doc:sibling"]),
|
||
|
|
expandable_node_ids: ids(&["doc:root"]).into_iter().collect(),
|
||
|
|
rows: vec![
|
||
|
|
PageTreeRuntimeRow {
|
||
|
|
node_id: "doc:root".into(),
|
||
|
|
parent_node_id: None,
|
||
|
|
position: 0,
|
||
|
|
},
|
||
|
|
PageTreeRuntimeRow {
|
||
|
|
node_id: "doc:child".into(),
|
||
|
|
parent_node_id: Some("doc:root".into()),
|
||
|
|
position: 0,
|
||
|
|
},
|
||
|
|
PageTreeRuntimeRow {
|
||
|
|
node_id: "doc:sibling".into(),
|
||
|
|
parent_node_id: Some("doc:root".into()),
|
||
|
|
position: 1,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
let state = PageTreeRuntimeState {
|
||
|
|
focused_id: Some("doc:missing".into()),
|
||
|
|
..PageTreeRuntimeState::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let transition = state.reduce(&env, PageTreeRuntimeAction::Normalize);
|
||
|
|
assert_eq!(transition.state.focused_id.as_deref(), Some("doc:root"));
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::DomPatch));
|
||
|
|
|
||
|
|
let transition = transition
|
||
|
|
.state
|
||
|
|
.reduce(&env, PageTreeRuntimeAction::MoveNext);
|
||
|
|
assert_eq!(transition.state.focused_id.as_deref(), Some("doc:child"));
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::Expand {
|
||
|
|
node_id: "doc:root".into(),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(transition.state.expanded_ids.contains("doc:root"));
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::DomPatch));
|
||
|
|
|
||
|
|
let transition = transition
|
||
|
|
.state
|
||
|
|
.reduce(&env, PageTreeRuntimeAction::OpenFocused);
|
||
|
|
assert!(transition.outputs.contains(&PageTreeRuntimeOutput::Intent(
|
||
|
|
PageTreeIntentEvent::Open {
|
||
|
|
node_id: "doc:child".into(),
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
|
||
|
|
let transition = transition
|
||
|
|
.state
|
||
|
|
.reduce(&env, PageTreeRuntimeAction::ContextMenuFocused);
|
||
|
|
assert!(transition.outputs.contains(&PageTreeRuntimeOutput::Intent(
|
||
|
|
PageTreeIntentEvent::ContextMenu {
|
||
|
|
node_id: "doc:child".into(),
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::UpdateDropFeedback {
|
||
|
|
feedback: Some(PageTreeDropFeedback {
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_node_id: Some("doc:sibling".into()),
|
||
|
|
target_parent_id: Some("doc:root".into()),
|
||
|
|
position: PageTreeDropPosition::Inside,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
transition
|
||
|
|
.state
|
||
|
|
.drop_feedback
|
||
|
|
.as_ref()
|
||
|
|
.map(|feedback| feedback.position),
|
||
|
|
Some(PageTreeDropPosition::Inside)
|
||
|
|
);
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::UpdateDropFeedbackForTarget {
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_node_id: "doc:sibling".into(),
|
||
|
|
position: PageTreeDropPosition::Before,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
transition
|
||
|
|
.state
|
||
|
|
.drop_feedback
|
||
|
|
.as_ref()
|
||
|
|
.and_then(|feedback| feedback.target_node_id.as_deref()),
|
||
|
|
Some("doc:sibling")
|
||
|
|
);
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::DispatchMove {
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_parent_id: Some("doc:root".into()),
|
||
|
|
position: PageTreeDropPosition::After,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::MoveSubtree {
|
||
|
|
command_name: "tree.subtree.move",
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_node_id: None,
|
||
|
|
target_parent_id: Some("doc:root".into()),
|
||
|
|
position: PageTreeDropPosition::After,
|
||
|
|
sort_order: None,
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::DispatchMoveToTarget {
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_node_id: "doc:sibling".into(),
|
||
|
|
position: PageTreeDropPosition::Before,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::MoveSubtree {
|
||
|
|
command_name: "tree.subtree.move",
|
||
|
|
source_node_id: "doc:child".into(),
|
||
|
|
target_node_id: Some("doc:sibling".into()),
|
||
|
|
target_parent_id: Some("doc:root".into()),
|
||
|
|
position: PageTreeDropPosition::Before,
|
||
|
|
sort_order: Some(1),
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::DispatchCreate {
|
||
|
|
parent_node_id: Some("doc:root".into()),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::CreateNode {
|
||
|
|
command_name: "tree.node.create",
|
||
|
|
parent_node_id: Some("doc:root".into()),
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
|
||
|
|
let transition = transition.state.reduce(
|
||
|
|
&env,
|
||
|
|
PageTreeRuntimeAction::DispatchRename {
|
||
|
|
node_id: "doc:child".into(),
|
||
|
|
title: "新标题".into(),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(transition
|
||
|
|
.outputs
|
||
|
|
.contains(&PageTreeRuntimeOutput::CommandDispatch(
|
||
|
|
PageTreeCommandDispatchEvent::RenameNode {
|
||
|
|
command_name: "tree.node.rename",
|
||
|
|
node_id: "doc:child".into(),
|
||
|
|
title: "新标题".into(),
|
||
|
|
},
|
||
|
|
)));
|
||
|
|
}
|
||
|
|
}
|