feat(tree): close rust family shell cutover
This commit is contained in:
@@ -0,0 +1,880 @@
|
||||
use super::drag_drop_state::TreeShellDragEffect;
|
||||
use super::filetree_runtime::{
|
||||
FileTreeIntentEvent, FileTreeRuntimeAction, FileTreeRuntimeEnvironment, FileTreeRuntimeOutput,
|
||||
FileTreeRuntimeState,
|
||||
};
|
||||
use super::page_runtime::{
|
||||
PageTreeCommandDispatchEvent, PageTreeDropFeedback, PageTreeDropPosition,
|
||||
PageTreeIntentEvent, PageTreeRuntimeAction, PageTreeRuntimeEnvironment,
|
||||
PageTreeRuntimeOutput, PageTreeRuntimeState,
|
||||
};
|
||||
use super::picker_runtime::{
|
||||
PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeOutput, PickerRuntimePickTarget,
|
||||
PickerRuntimeState,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TreeShellRuntimeMode {
|
||||
Page,
|
||||
FileTree,
|
||||
Picker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "mode",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum TreeShellRuntimeRequest {
|
||||
Page {
|
||||
request_id: String,
|
||||
environment: PageTreeRuntimeEnvironment,
|
||||
state: PageTreeRuntimeState,
|
||||
action: PageTreeRuntimeAction,
|
||||
},
|
||||
FileTree {
|
||||
request_id: String,
|
||||
environment: FileTreeRuntimeEnvironment,
|
||||
state: FileTreeRuntimeState,
|
||||
action: FileTreeRuntimeAction,
|
||||
},
|
||||
Picker {
|
||||
request_id: String,
|
||||
environment: PickerRuntimeEnvironment,
|
||||
state: PickerRuntimeState,
|
||||
action: PickerRuntimeAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TreeShellRuntimeResult {
|
||||
pub request_id: String,
|
||||
pub mode: TreeShellRuntimeMode,
|
||||
pub state: TreeShellRuntimeStateSnapshot,
|
||||
pub dom_patches: Vec<TreeShellDomPatch>,
|
||||
pub host_events: Vec<TreeShellHostEvent>,
|
||||
pub command_events: Vec<TreeShellCommandEvent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "mode", content = "state", rename_all = "camelCase")]
|
||||
pub enum TreeShellRuntimeStateSnapshot {
|
||||
Page(PageTreeRuntimeState),
|
||||
FileTree(FileTreeRuntimeState),
|
||||
Picker(PickerRuntimeState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum TreeShellDomPatch {
|
||||
PageState {
|
||||
focused_id: Option<String>,
|
||||
expanded_ids: BTreeSet<String>,
|
||||
drop_feedback: Option<PageTreeDropFeedback>,
|
||||
},
|
||||
FileTreeState {
|
||||
selected_row_ids: BTreeSet<String>,
|
||||
anchor_row_id: Option<String>,
|
||||
focused_row_id: Option<String>,
|
||||
drag_row_ids: Vec<String>,
|
||||
drag_effect: Option<TreeShellDragEffect>,
|
||||
drop_target_row_id: Option<String>,
|
||||
},
|
||||
PickerState {
|
||||
active_item_key: Option<String>,
|
||||
focus_dom: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum TreeShellHostEvent {
|
||||
PageOpen {
|
||||
node_id: String,
|
||||
},
|
||||
PageContextMenu {
|
||||
node_id: String,
|
||||
},
|
||||
FileTreeOpen {
|
||||
target: super::filetree_runtime::FileTreeOpenTarget,
|
||||
},
|
||||
FileTreeContextMenu {
|
||||
row_id: String,
|
||||
target: super::filetree_runtime::FileTreeOpenTarget,
|
||||
},
|
||||
FileTreeInternalDrop {
|
||||
target_row_id: Option<String>,
|
||||
target: Option<super::filetree_runtime::FileTreeOpenTarget>,
|
||||
row_ids: Vec<String>,
|
||||
copy: bool,
|
||||
},
|
||||
FileTreeExternalDrop {
|
||||
target_row_id: Option<String>,
|
||||
target: Option<super::filetree_runtime::FileTreeOpenTarget>,
|
||||
file_count: u32,
|
||||
},
|
||||
PickerPickRoot,
|
||||
PickerPickDocument {
|
||||
document_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum TreeShellCommandEvent {
|
||||
CreateNode {
|
||||
command_name: String,
|
||||
parent_node_id: Option<String>,
|
||||
},
|
||||
RenameNode {
|
||||
command_name: String,
|
||||
node_id: String,
|
||||
title: String,
|
||||
},
|
||||
MoveSubtree {
|
||||
command_name: String,
|
||||
source_node_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target_node_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target_parent_id: Option<String>,
|
||||
position: PageTreeDropPosition,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sort_order: Option<i64>,
|
||||
},
|
||||
CopyResource {
|
||||
command_name: String,
|
||||
source_asset_ids: Vec<String>,
|
||||
target_document_id: String,
|
||||
},
|
||||
MoveResource {
|
||||
command_name: String,
|
||||
source_asset_ids: Vec<String>,
|
||||
target_document_id: String,
|
||||
},
|
||||
UploadResource {
|
||||
command_name: String,
|
||||
target_document_id: String,
|
||||
file_count: u32,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn reduce_tree_shell_runtime(request: TreeShellRuntimeRequest) -> TreeShellRuntimeResult {
|
||||
match request {
|
||||
TreeShellRuntimeRequest::Page {
|
||||
request_id,
|
||||
environment,
|
||||
state,
|
||||
action,
|
||||
} => {
|
||||
let transition = state.reduce(&environment, action);
|
||||
let dom_patches = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
PageTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::PageState {
|
||||
focused_id: transition.state.focused_id.clone(),
|
||||
expanded_ids: transition.state.expanded_ids.clone(),
|
||||
drop_feedback: transition.state.drop_feedback.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let host_events = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::Open { node_id }) => {
|
||||
Some(TreeShellHostEvent::PageOpen {
|
||||
node_id: node_id.clone(),
|
||||
})
|
||||
}
|
||||
PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::ContextMenu { node_id }) => {
|
||||
Some(TreeShellHostEvent::PageContextMenu {
|
||||
node_id: node_id.clone(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let command_events = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
PageTreeRuntimeOutput::CommandDispatch(
|
||||
PageTreeCommandDispatchEvent::CreateNode {
|
||||
command_name,
|
||||
parent_node_id,
|
||||
},
|
||||
) => Some(TreeShellCommandEvent::CreateNode {
|
||||
command_name: command_name.to_string(),
|
||||
parent_node_id: parent_node_id.clone(),
|
||||
}),
|
||||
PageTreeRuntimeOutput::CommandDispatch(
|
||||
PageTreeCommandDispatchEvent::RenameNode {
|
||||
command_name,
|
||||
node_id,
|
||||
title,
|
||||
},
|
||||
) => Some(TreeShellCommandEvent::RenameNode {
|
||||
command_name: command_name.to_string(),
|
||||
node_id: node_id.clone(),
|
||||
title: title.clone(),
|
||||
}),
|
||||
PageTreeRuntimeOutput::CommandDispatch(
|
||||
PageTreeCommandDispatchEvent::MoveSubtree {
|
||||
command_name,
|
||||
source_node_id,
|
||||
target_node_id,
|
||||
target_parent_id,
|
||||
position,
|
||||
sort_order,
|
||||
},
|
||||
) => Some(TreeShellCommandEvent::MoveSubtree {
|
||||
command_name: command_name.to_string(),
|
||||
source_node_id: source_node_id.clone(),
|
||||
target_node_id: target_node_id.clone(),
|
||||
target_parent_id: target_parent_id.clone(),
|
||||
position: *position,
|
||||
sort_order: *sort_order,
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
TreeShellRuntimeResult {
|
||||
request_id,
|
||||
mode: TreeShellRuntimeMode::Page,
|
||||
state: TreeShellRuntimeStateSnapshot::Page(transition.state),
|
||||
dom_patches,
|
||||
host_events,
|
||||
command_events,
|
||||
}
|
||||
}
|
||||
TreeShellRuntimeRequest::FileTree {
|
||||
request_id,
|
||||
environment,
|
||||
state,
|
||||
action,
|
||||
} => {
|
||||
let transition = state.reduce(&environment, action);
|
||||
let dom_patches = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
FileTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::FileTreeState {
|
||||
selected_row_ids: transition.state.selection.selected_row_ids.clone(),
|
||||
anchor_row_id: transition.state.selection.anchor_row_id.clone(),
|
||||
focused_row_id: transition.state.selection.focused_row_id.clone(),
|
||||
drag_row_ids: transition.state.drag_row_ids.clone(),
|
||||
drag_effect: transition.state.drag_effect,
|
||||
drop_target_row_id: transition.state.drop_target_row_id.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let host_events = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::Open { target }) => {
|
||||
Some(TreeShellHostEvent::FileTreeOpen {
|
||||
target: target.clone(),
|
||||
})
|
||||
}
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ContextMenu {
|
||||
row_id,
|
||||
target,
|
||||
}) => Some(TreeShellHostEvent::FileTreeContextMenu {
|
||||
row_id: row_id.clone(),
|
||||
target: target.clone(),
|
||||
}),
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::InternalDrop {
|
||||
target_row_id,
|
||||
target,
|
||||
row_ids,
|
||||
copy,
|
||||
}) => Some(TreeShellHostEvent::FileTreeInternalDrop {
|
||||
target_row_id: target_row_id.clone(),
|
||||
target: target.clone(),
|
||||
row_ids: row_ids.clone(),
|
||||
copy: *copy,
|
||||
}),
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ExternalDrop {
|
||||
target_row_id,
|
||||
target,
|
||||
file_count,
|
||||
}) => Some(TreeShellHostEvent::FileTreeExternalDrop {
|
||||
target_row_id: target_row_id.clone(),
|
||||
target: target.clone(),
|
||||
file_count: *file_count,
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
TreeShellRuntimeResult {
|
||||
request_id,
|
||||
mode: TreeShellRuntimeMode::FileTree,
|
||||
state: TreeShellRuntimeStateSnapshot::FileTree(transition.state),
|
||||
dom_patches,
|
||||
host_events,
|
||||
command_events: Vec::new(),
|
||||
}
|
||||
}
|
||||
TreeShellRuntimeRequest::Picker {
|
||||
request_id,
|
||||
environment,
|
||||
state,
|
||||
action,
|
||||
} => {
|
||||
let transition = state.reduce(&environment, action);
|
||||
let dom_patches = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
PickerRuntimeOutput::DomPatch { focus_dom } => {
|
||||
Some(TreeShellDomPatch::PickerState {
|
||||
active_item_key: transition.state.active_item_key.clone(),
|
||||
focus_dom: *focus_dom,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let host_events = transition
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|output| match output {
|
||||
PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Root) => {
|
||||
Some(TreeShellHostEvent::PickerPickRoot)
|
||||
}
|
||||
PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Document {
|
||||
document_id,
|
||||
}) => Some(TreeShellHostEvent::PickerPickDocument {
|
||||
document_id: document_id.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
TreeShellRuntimeResult {
|
||||
request_id,
|
||||
mode: TreeShellRuntimeMode::Picker,
|
||||
state: TreeShellRuntimeStateSnapshot::Picker(transition.state),
|
||||
dom_patches,
|
||||
host_events,
|
||||
command_events: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
reduce_tree_shell_runtime, TreeShellCommandEvent, TreeShellDomPatch, TreeShellHostEvent,
|
||||
TreeShellRuntimeMode, TreeShellRuntimeRequest, TreeShellRuntimeStateSnapshot,
|
||||
};
|
||||
use crate::tree_shell::filetree_runtime::{
|
||||
FileTreeOpenTarget, FileTreeRuntimeAction, FileTreeRuntimeEnvironment, FileTreeRuntimeRow,
|
||||
FileTreeRuntimeState,
|
||||
};
|
||||
use crate::tree_shell::filetree_selection::{
|
||||
FileTreeSelectionModifiers, FileTreeSelectionState,
|
||||
};
|
||||
use crate::tree_shell::page_runtime::{
|
||||
PageTreeDropPosition, PageTreeRuntimeAction, PageTreeRuntimeEnvironment,
|
||||
PageTreeRuntimeState,
|
||||
};
|
||||
use crate::tree_shell::picker_runtime::{
|
||||
PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeItem, PickerRuntimeState,
|
||||
};
|
||||
use crate::tree_shell::drag_drop_state::TreeShellDragEffect;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
fn ids(values: &[&str]) -> Vec<String> {
|
||||
values.iter().map(|value| (*value).to_string()).collect()
|
||||
}
|
||||
|
||||
fn set(values: &[&str]) -> BTreeSet<String> {
|
||||
values.iter().map(|value| (*value).to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_request_result_are_wire_safe_and_camel_case() {
|
||||
let request = TreeShellRuntimeRequest::Page {
|
||||
request_id: "req-page".into(),
|
||||
environment: PageTreeRuntimeEnvironment {
|
||||
visible_node_ids: ids(&["doc:a", "doc:b"]),
|
||||
expandable_node_ids: set(&["doc:a"]),
|
||||
rows: Vec::new(),
|
||||
},
|
||||
state: PageTreeRuntimeState {
|
||||
focused_id: Some("doc:a".into()),
|
||||
expanded_ids: BTreeSet::new(),
|
||||
drop_feedback: None,
|
||||
},
|
||||
action: PageTreeRuntimeAction::MoveNext,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_value(&request).expect("runtime request should serialize");
|
||||
assert_eq!(
|
||||
encoded,
|
||||
json!({
|
||||
"mode": "page",
|
||||
"requestId": "req-page",
|
||||
"environment": {
|
||||
"visibleNodeIds": ["doc:a", "doc:b"],
|
||||
"expandableNodeIds": ["doc:a"]
|
||||
},
|
||||
"state": {
|
||||
"focusedId": "doc:a",
|
||||
"expandedIds": [],
|
||||
"dropFeedback": null
|
||||
},
|
||||
"action": {
|
||||
"kind": "moveNext"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let decoded: TreeShellRuntimeRequest =
|
||||
serde_json::from_value(encoded).expect("runtime request should deserialize");
|
||||
let result = reduce_tree_shell_runtime(decoded);
|
||||
|
||||
assert_eq!(result.request_id, "req-page");
|
||||
assert_eq!(result.mode, TreeShellRuntimeMode::Page);
|
||||
assert_eq!(
|
||||
result.state,
|
||||
TreeShellRuntimeStateSnapshot::Page(PageTreeRuntimeState {
|
||||
focused_id: Some("doc:b".into()),
|
||||
expanded_ids: BTreeSet::new(),
|
||||
drop_feedback: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
result.dom_patches,
|
||||
vec![TreeShellDomPatch::PageState {
|
||||
focused_id: Some("doc:b".into()),
|
||||
expanded_ids: BTreeSet::new(),
|
||||
drop_feedback: None,
|
||||
}]
|
||||
);
|
||||
assert!(result.host_events.is_empty());
|
||||
assert!(result.command_events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_api_maps_page_intents_and_commands_to_structured_events() {
|
||||
let environment = PageTreeRuntimeEnvironment {
|
||||
visible_node_ids: ids(&["doc:a", "doc:b"]),
|
||||
expandable_node_ids: set(&["doc:a"]),
|
||||
rows: Vec::new(),
|
||||
};
|
||||
let state = PageTreeRuntimeState {
|
||||
focused_id: Some("doc:b".into()),
|
||||
..PageTreeRuntimeState::default()
|
||||
};
|
||||
|
||||
let open = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
|
||||
request_id: "req-open".into(),
|
||||
environment: environment.clone(),
|
||||
state: state.clone(),
|
||||
action: PageTreeRuntimeAction::OpenFocused,
|
||||
});
|
||||
assert_eq!(
|
||||
open.host_events,
|
||||
vec![TreeShellHostEvent::PageOpen {
|
||||
node_id: "doc:b".into(),
|
||||
}]
|
||||
);
|
||||
|
||||
let create_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
|
||||
request_id: "req-create".into(),
|
||||
environment: environment.clone(),
|
||||
state: state.clone(),
|
||||
action: PageTreeRuntimeAction::DispatchCreate {
|
||||
parent_node_id: Some("doc:a".into()),
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
create_result.command_events,
|
||||
vec![TreeShellCommandEvent::CreateNode {
|
||||
command_name: "tree.node.create".into(),
|
||||
parent_node_id: Some("doc:a".into()),
|
||||
}]
|
||||
);
|
||||
|
||||
let rename_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
|
||||
request_id: "req-rename".into(),
|
||||
environment: environment.clone(),
|
||||
state: state.clone(),
|
||||
action: PageTreeRuntimeAction::DispatchRename {
|
||||
node_id: "doc:b".into(),
|
||||
title: "新标题".into(),
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
rename_result.command_events,
|
||||
vec![TreeShellCommandEvent::RenameNode {
|
||||
command_name: "tree.node.rename".into(),
|
||||
node_id: "doc:b".into(),
|
||||
title: "新标题".into(),
|
||||
}]
|
||||
);
|
||||
|
||||
let move_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
|
||||
request_id: "req-move".into(),
|
||||
environment,
|
||||
state,
|
||||
action: PageTreeRuntimeAction::DispatchMove {
|
||||
source_node_id: "doc:b".into(),
|
||||
target_parent_id: Some("doc:a".into()),
|
||||
position: PageTreeDropPosition::Inside,
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
move_result.command_events,
|
||||
vec![TreeShellCommandEvent::MoveSubtree {
|
||||
command_name: "tree.subtree.move".into(),
|
||||
source_node_id: "doc:b".into(),
|
||||
target_node_id: None,
|
||||
target_parent_id: Some("doc:a".into()),
|
||||
position: PageTreeDropPosition::Inside,
|
||||
sort_order: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_api_maps_filetree_and_picker_results_to_common_output_channels() {
|
||||
let filetree = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
|
||||
request_id: "req-filetree".into(),
|
||||
environment: FileTreeRuntimeEnvironment {
|
||||
visible_row_ids: ids(&["doc:root", "asset:image"]),
|
||||
rows: vec![
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "doc:root".into(),
|
||||
row_kind: "doc".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: None,
|
||||
},
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "asset:image".into(),
|
||||
row_kind: "asset".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: Some("image".into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
state: FileTreeRuntimeState::default(),
|
||||
action: FileTreeRuntimeAction::SelectRow {
|
||||
row_id: "asset:image".into(),
|
||||
modifiers: FileTreeSelectionModifiers::default(),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(filetree.mode, TreeShellRuntimeMode::FileTree);
|
||||
assert_eq!(
|
||||
filetree.dom_patches,
|
||||
vec![TreeShellDomPatch::FileTreeState {
|
||||
selected_row_ids: set(&["asset:image"]),
|
||||
anchor_row_id: Some("asset:image".into()),
|
||||
focused_row_id: Some("asset:image".into()),
|
||||
drag_row_ids: Vec::new(),
|
||||
drag_effect: None,
|
||||
drop_target_row_id: None,
|
||||
}]
|
||||
);
|
||||
|
||||
let drag_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
|
||||
request_id: "req-filetree-drag".into(),
|
||||
environment: FileTreeRuntimeEnvironment {
|
||||
visible_row_ids: ids(&["doc:root", "asset:image"]),
|
||||
rows: vec![
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "doc:root".into(),
|
||||
row_kind: "doc".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: None,
|
||||
},
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "asset:image".into(),
|
||||
row_kind: "asset".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: Some("image".into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
state: FileTreeRuntimeState {
|
||||
selection: FileTreeSelectionState::from_selected(&["asset:image".to_string()]),
|
||||
..FileTreeRuntimeState::default()
|
||||
},
|
||||
action: FileTreeRuntimeAction::ResolveDragRows {
|
||||
row_id: "asset:image".into(),
|
||||
has_external_files: false,
|
||||
alt_key: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
drag_result.dom_patches,
|
||||
vec![TreeShellDomPatch::FileTreeState {
|
||||
selected_row_ids: set(&["asset:image"]),
|
||||
anchor_row_id: Some("asset:image".into()),
|
||||
focused_row_id: Some("asset:image".into()),
|
||||
drag_row_ids: ids(&["asset:image"]),
|
||||
drag_effect: Some(TreeShellDragEffect::Move),
|
||||
drop_target_row_id: None,
|
||||
}]
|
||||
);
|
||||
|
||||
let drop_target_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
|
||||
request_id: "req-filetree-drop-target".into(),
|
||||
environment: FileTreeRuntimeEnvironment {
|
||||
visible_row_ids: ids(&["doc:root", "asset:image"]),
|
||||
rows: vec![
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "doc:root".into(),
|
||||
row_kind: "doc".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: None,
|
||||
},
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "asset:image".into(),
|
||||
row_kind: "asset".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: Some("image".into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
state: FileTreeRuntimeState::default(),
|
||||
action: FileTreeRuntimeAction::UpdateDropTarget {
|
||||
row_id: Some("asset:image".into()),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
drop_target_result.dom_patches,
|
||||
vec![TreeShellDomPatch::FileTreeState {
|
||||
selected_row_ids: BTreeSet::new(),
|
||||
anchor_row_id: None,
|
||||
focused_row_id: None,
|
||||
drag_row_ids: Vec::new(),
|
||||
drag_effect: None,
|
||||
drop_target_row_id: Some("asset:image".into()),
|
||||
}]
|
||||
);
|
||||
|
||||
let internal_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
|
||||
request_id: "req-filetree-internal-drop".into(),
|
||||
environment: FileTreeRuntimeEnvironment {
|
||||
visible_row_ids: ids(&["doc:root", "asset:image"]),
|
||||
rows: vec![
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "doc:root".into(),
|
||||
row_kind: "doc".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: None,
|
||||
},
|
||||
FileTreeRuntimeRow {
|
||||
row_id: "asset:image".into(),
|
||||
row_kind: "asset".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: Some("image".into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
state: FileTreeRuntimeState {
|
||||
drag_row_ids: ids(&["asset:image"]),
|
||||
drag_effect: Some(TreeShellDragEffect::Move),
|
||||
drop_target_row_id: Some("doc:root".into()),
|
||||
..FileTreeRuntimeState::default()
|
||||
},
|
||||
action: FileTreeRuntimeAction::DispatchInternalDrop {
|
||||
target_row_id: Some("doc:root".into()),
|
||||
row_ids: ids(&["asset:image"]),
|
||||
copy: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
internal_drop.host_events,
|
||||
vec![TreeShellHostEvent::FileTreeInternalDrop {
|
||||
target_row_id: Some("doc:root".into()),
|
||||
target: Some(FileTreeOpenTarget::Document {
|
||||
document_id: "root".into(),
|
||||
}),
|
||||
row_ids: ids(&["asset:image"]),
|
||||
copy: false,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
internal_drop.dom_patches,
|
||||
vec![TreeShellDomPatch::FileTreeState {
|
||||
selected_row_ids: BTreeSet::new(),
|
||||
anchor_row_id: None,
|
||||
focused_row_id: None,
|
||||
drag_row_ids: Vec::new(),
|
||||
drag_effect: None,
|
||||
drop_target_row_id: None,
|
||||
}]
|
||||
);
|
||||
|
||||
let external_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
|
||||
request_id: "req-filetree-external-drop".into(),
|
||||
environment: FileTreeRuntimeEnvironment {
|
||||
visible_row_ids: ids(&["doc:root"]),
|
||||
rows: vec![FileTreeRuntimeRow {
|
||||
row_id: "doc:root".into(),
|
||||
row_kind: "doc".into(),
|
||||
document_id: Some("root".into()),
|
||||
asset_id: None,
|
||||
}],
|
||||
},
|
||||
state: FileTreeRuntimeState::default(),
|
||||
action: FileTreeRuntimeAction::DispatchExternalDrop {
|
||||
target_row_id: Some("doc:root".into()),
|
||||
file_count: 2,
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
external_drop.host_events,
|
||||
vec![TreeShellHostEvent::FileTreeExternalDrop {
|
||||
target_row_id: Some("doc:root".into()),
|
||||
target: Some(FileTreeOpenTarget::Document {
|
||||
document_id: "root".into(),
|
||||
}),
|
||||
file_count: 2,
|
||||
}]
|
||||
);
|
||||
|
||||
let picker = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Picker {
|
||||
request_id: "req-picker".into(),
|
||||
environment: PickerRuntimeEnvironment {
|
||||
items: vec![PickerRuntimeItem {
|
||||
item_key: "doc:target".into(),
|
||||
document_id: Some("target".into()),
|
||||
pickable: true,
|
||||
}],
|
||||
excluded_ids: BTreeSet::new(),
|
||||
allow_root_pick: false,
|
||||
},
|
||||
state: PickerRuntimeState {
|
||||
active_item_key: Some("doc:target".into()),
|
||||
},
|
||||
action: PickerRuntimeAction::Pick,
|
||||
});
|
||||
|
||||
assert_eq!(picker.mode, TreeShellRuntimeMode::Picker);
|
||||
assert_eq!(
|
||||
picker.host_events,
|
||||
vec![TreeShellHostEvent::PickerPickDocument {
|
||||
document_id: "target".into(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_command_event_schema_covers_tree_and_resource_command_channels() {
|
||||
let events = vec![
|
||||
TreeShellCommandEvent::CreateNode {
|
||||
command_name: "tree.node.create".into(),
|
||||
parent_node_id: Some("doc:parent".into()),
|
||||
},
|
||||
TreeShellCommandEvent::RenameNode {
|
||||
command_name: "tree.node.rename".into(),
|
||||
node_id: "doc:target".into(),
|
||||
title: "新标题".into(),
|
||||
},
|
||||
TreeShellCommandEvent::MoveSubtree {
|
||||
command_name: "tree.subtree.move".into(),
|
||||
source_node_id: "doc:target".into(),
|
||||
target_node_id: Some("doc:sibling".into()),
|
||||
target_parent_id: Some("doc:parent".into()),
|
||||
position: PageTreeDropPosition::After,
|
||||
sort_order: Some(3),
|
||||
},
|
||||
TreeShellCommandEvent::CopyResource {
|
||||
command_name: "tree.resource.copy".into(),
|
||||
source_asset_ids: vec!["asset:a".into()],
|
||||
target_document_id: "doc:target".into(),
|
||||
},
|
||||
TreeShellCommandEvent::MoveResource {
|
||||
command_name: "tree.resource.move".into(),
|
||||
source_asset_ids: vec!["asset:a".into()],
|
||||
target_document_id: "doc:target".into(),
|
||||
},
|
||||
TreeShellCommandEvent::UploadResource {
|
||||
command_name: "tree.resource.upload".into(),
|
||||
target_document_id: "doc:target".into(),
|
||||
file_count: 2,
|
||||
},
|
||||
];
|
||||
|
||||
let encoded = serde_json::to_value(&events).expect("command events should serialize");
|
||||
assert_eq!(
|
||||
encoded,
|
||||
json!([
|
||||
{
|
||||
"kind": "createNode",
|
||||
"commandName": "tree.node.create",
|
||||
"parentNodeId": "doc:parent"
|
||||
},
|
||||
{
|
||||
"kind": "renameNode",
|
||||
"commandName": "tree.node.rename",
|
||||
"nodeId": "doc:target",
|
||||
"title": "新标题"
|
||||
},
|
||||
{
|
||||
"kind": "moveSubtree",
|
||||
"commandName": "tree.subtree.move",
|
||||
"sourceNodeId": "doc:target",
|
||||
"targetNodeId": "doc:sibling",
|
||||
"targetParentId": "doc:parent",
|
||||
"position": "after",
|
||||
"sortOrder": 3
|
||||
},
|
||||
{
|
||||
"kind": "copyResource",
|
||||
"commandName": "tree.resource.copy",
|
||||
"sourceAssetIds": ["asset:a"],
|
||||
"targetDocumentId": "doc:target"
|
||||
},
|
||||
{
|
||||
"kind": "moveResource",
|
||||
"commandName": "tree.resource.move",
|
||||
"sourceAssetIds": ["asset:a"],
|
||||
"targetDocumentId": "doc:target"
|
||||
},
|
||||
{
|
||||
"kind": "uploadResource",
|
||||
"commandName": "tree.resource.upload",
|
||||
"targetDocumentId": "doc:target",
|
||||
"fileCount": 2
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user